爲什麼 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 內核在筆者平臺上造成此問題的根本原因,原因由這幾個要素構成:

  1. mmap 在使用 ION 的時候,需要用參數 MAP_SHARED
  1. 根據 ARMv8 手冊,PTE_RDONLY | PTE_DBM 兩個屬性疊加,根據另一個系統寄存器的不同配置,會有兩種不同硬件處理的情況:二選一
  1. 在筆者的內核編譯中,系統寄存器 TCR_EL1.HD=0,也就是走的 B 路線。

綜上,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