爲什麼 mmap 之後訪問地址仍然發生了缺頁異常?
作者簡介:
viho he,ARM64 專家,現供職於某芯片公司,專注於 Linux 內核、BSP、ARM64 虛擬化以及與 ARM64 SoC 相關的各種底軟技術
問題簡述
在筆者的開發平臺上,應用程序使用 ION 申請 cma 內存,並用 mmap 映射到用戶地址空間去做寫操作。
重點代碼摘要如下:
客戶希望提高
node->var = some_value;
這裏的訪問效率(實際代碼要複雜些,是申請了一個大數組並往裏循環讀寫數據)。
第一輪分析
首先用 perf 分析應用程序行爲,發現程序在運行時產生了不少 page fault,感覺是 mmap 之後內核並沒有建立映射,而是在第一次訪問此地址時,產生 fault in 所致。
第一個想法
首先想到優化點是:用 MAP_POPULATE 強制在 mmap 系統調用裏面把所有的頁面都 fault in,這樣後面訪問就不會產生 page fault 而耽誤時間了。因爲 page fault 不管是在前還是在後總會發生,這個優化思路其實只是讓這段時間集中挪到了訪問數據之前。但看了一下 mmap 的手冊,說到 MAP_POPULATE 只能針對於 MAP_PRIVATE 映射(見 mmap 手冊),比如用匿名頁建立的映射,而此處因爲是 MAP_SHARED,無法用 MAP_POPULATE, 故此方案作罷。
第二個想法
用 ftrace 跟蹤了一下 ion 代碼,發現 mmap 已經調用了 remap_pfn_range 來建立頁面映射,代碼路徑如下:
mmap => el0_sync => el0_sync_handler => el0_svc => do_el0_svc => el0_svc_common.constprop.4 => __arm64_sys_mmap => ksys_mmap_pgoff => vm_mmap_pgoff => do_mmap => mmap_region => dma_buf_mmap_internal => ion_dma_buf_mmap => ion_heap_map_user => remap_pfn_range
也就是說,從代碼路徑看,在 mmap 系統調用中,用戶頁已經全都建立好了,所謂的 fault in 其實並不存在。那麼問題來了,既然不存在 fault in, 爲什麼還是會產生 page fault 呢?
轉機
因爲此問題是在換了內核到 5.10 之後暴露出來的,嘗試在舊內核 4.19 上嘗試同樣的代碼。
同樣代碼運行在 4.19 內核上,該寫操作的效率明顯高於 5.10 內核。對比由 perf stat 觀測得出:
也就是說,4.19 上面代碼如預期運行,mmap 時後就不再有 page fault,但 5.10 上卻發生了 page fault。
問題就轉變成了:爲什麼 remap_pfn_range 之後仍發生了 page fault?
分析 remap_pfn_range 行爲發現應該不是這裏的問題,那麼,**或許這個 page fault 不是缺頁異常,而是別的 page fault?**想到這一層後,繼續對比 4.19 與 5.10 內核行爲。
問題原因的要素分析
跳過冗長的 debug 過程,直接說 5.10 內核在筆者平臺上造成此問題的根本原因,原因由這幾個要素構成:
- mmap 在使用 ION 的時候,需要用參數 MAP_SHARED
-
此參數最終會傳導至 ARM64 arch 層在設置相應頁表時,採用 PAGE_SHARED 聯合屬性
-
PAGE_SHARED 聯合屬性,包含這樣兩個頁表基本屬性:PTE_RDONLY | PTE_DBM
- 根據 ARMv8 手冊,PTE_RDONLY | PTE_DBM 兩個屬性疊加,根據另一個系統寄存器的不同配置,會有兩種不同硬件處理的情況:二選一
-
系統寄存器 TCR_EL1.HD=1 產生的效果是:在第一次寫操作訪問該頁面時,CPU 會自動更新頁面的 RO 屬性爲 RW,並不產生 permission fault
-
系統寄存器 TCR_EL1.HD=0 產生的效果是:在第一次寫操作訪問該頁面時,CPU 不會自動更新頁面的 RO 屬性爲 RW,而會產生一次 permission fault,這個 fault 需要由操作系統去軟件更新爲 RW,它的主要目的,是爲了讓操作系統跟蹤頁表第一次寫訪問。
- 在筆者的內核編譯中,系統寄存器 TCR_EL1.HD=0,也就是走的 B 路線。
-
之所以編譯會讓該位配置爲 0,是因爲需要實施一個 CPU erratum 的軟件規避 :Cortex-A55: 1024718: Update of DBM/AP bits without break before make might result in incorrect update,表現爲內核配置項 CONFIG_ARM64_ERRATUM_1024718
-
CPU erratum 是那些 ARM core IP 在設計時的缺陷所致的 CPU 硬件 bug,因爲 IP 已經集成在市面上的 SoC 裏流片,不可能重新設計,只能通過軟件做一些規避,這些規避方法,均記錄在 ARM 的官方 Errata 文檔(CPU 勘誤手冊)裏。
-
以 A55 來說,此 Erratum 編號爲 1024718,影響範圍是所有版本號 <=r2p0 的 Cortex-A55 core IP,規避辦法是設置 TCR_EL1.HD=0
-
筆者平臺的 CPU 版本號,正好是 r2p0,恰好在此 Erratum 的範圍內,必須實施 CONFIG_ARM64_ERRATUM_1024718
綜上,5.10 內核,因爲上述的全部要素聯合,就產生了這個 permission page fault。
相關疑問與解答
問:爲什麼 4.19 沒有此問題
答:
4.19 具備其餘所有要素,只缺要素 1.b PAGE_SHARED 聯合屬性,包含這樣兩個頁表基本屬性:PTE_RDONLY | PTE_DBM
4.19 內核裏,PAGE_SHARED 聯合屬性,缺少 PTE_RDONLY。
以下爲 4.19 與 5.10 的 PAGE_SHARED 宏對比:
4.19 內核 #define PAGE_SHARED __pgprot(_PAGE_DEFAULT | PTE_USER | PTE_NG | PTE_PXN | PTE_UXN | PTE_WRITE)
5.10 內核 #define PAGE_SHARED __pgprot(_PAGE_DEFAULT | PTE_USER | PTE_RDONLY | PTE_NG | PTE_PXN | PTE_UXN | PTE_WRITE)
5.10 內核多出來的 PTE_RDONLY,再加上使得上述 permission page fault 觸發了。
問:爲什麼 5.10 要引入 PTE_RDONLY 屬性?
答:
5.10 對 PTE_RDONLY 引入,是 ARM 官方 maintainer 的刻意行爲,源於 commit:
https://github.com/torvalds/linux/commit/aa57157be69fb599bd4c38a4b75c5aad74a60ec0 arm64: Ensure VM_WRITE|VM_SHARED ptes are clean by default
從該 commit 描述看,對此的引入是合理的,因爲引入者預期配置了 TCR_EL1.HD=1,即問題要素 2.A)的路徑,由 CPU 硬件自動處理頁表項 RO 轉 RW。只有當諸如 CONFIG_ARM64_ERRATUM_1024718 這樣的 CPU erratum 軟件規避開啓時,因爲系統寄存器 TCR_EL1.HD=0,纔會落入到 permission page fault 的情景中。而這個是 commit 原作者可能未注意到的情況。實測發現,去掉此 erratum,page fault 也確實消失了。
問題解決方案
目前採用的辦法是一個 workaround(而非 fix):單把 PAGE_SHARED 屬性回退爲與 4.19 內核一致(參考前文提到的那個 commit)。實測性能與 4.19 幾乎無差別了:
因爲那個 ARM 官方的 commit 是社區深思熟慮的刻意行爲,回退並不現實,根本解決辦法,應該是給出一個綜合的 patch,使得方方面面的情況都照顧到。
問題回顧
這個 bug 卡筆者最長的時間,其實是在這一條:爲什麼 remap_pfn_range 之後仍發生了 page fault?因爲筆者先入爲主的觀念,把所有 page fault 都當成了缺頁異常,而沒有第一時間想到 permission fault 的可能,導致浪費了大量時間來分析 remap_pfn_range 的行爲,雖然代碼邏輯整理了不少,但一直沒能進入核心要點。這一點突破以後,後面的分析和解決就是水到渠成的事情了。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/1Qo5H892mI2AKxp6qFSl5g