背景 By 魯迅 By 高爾基 說明: 1. Kernel版本:4.14 2. ARM64處理器,Contex A53,雙核 3. 使用工具:Source Insight 3.5, Visio 1. 概述 上篇文章分析到 函數中,內核實現只是在進程的地址空間建立好了 區域,並沒有實際的虛擬地址到物理 ...
背景
Read the fucking source code!
--By 魯迅A picture is worth a thousand words.
--By 高爾基
說明:
- Kernel版本:4.14
- ARM64處理器,Contex-A53,雙核
- 使用工具:Source Insight 3.5, Visio
1. 概述
上篇文章分析到malloc/mmap
函數中,內核實現只是在進程的地址空間建立好了vma
區域,並沒有實際的虛擬地址到物理地址的映射操作。這部分就是在Page Fault
異常錯誤處理中實現的。
Linux內核中的Page Fault
異常處理很複雜,涉及的細節也很多,malloc/mmap
的物理記憶體映射只是它的一個子集功能,下圖大概涵蓋了出現Page Fault
的情況:
下邊就開始來啃啃硬骨頭吧。
2. Arm64處理
Page Fault
的異常處理,依賴於體繫結構,因此有必要來介紹一下Arm64
的處理。
代碼主要參考:arch/arm64/kernel/entry.S
。
Arm64在取指令或者訪問數據時,需要把虛擬地址轉換成物理地址,這個過程需要進行幾種檢查,在不滿足的情況下都能造成異常:
- 地址的合法性,比如以39有效位地址為例,內核地址的高25位為全1,用戶進程地址的高25位為全0;
- 地址的許可權檢查,這裡邊的許可權位都位於頁表條目中;
從上圖中可以看到,最後都會調到do_mem_abort
函數,這個函數比較簡單,直接看代碼,位於arch/arm64/mm/fault.c
:
/*
* Dispatch a data abort to the relevant handler.
*/
asmlinkage void __exception do_mem_abort(unsigned long addr, unsigned int esr,
struct pt_regs *regs)
{
const struct fault_info *inf = esr_to_fault_info(esr);
struct siginfo info;
if (!inf->fn(addr, esr, regs))
return;
pr_alert("Unhandled fault: %s (0x%08x) at 0x%016lx\n",
inf->name, esr, addr);
mem_abort_decode(esr);
info.si_signo = inf->sig;
info.si_errno = 0;
info.si_code = inf->code;
info.si_addr = (void __user *)addr;
arm64_notify_die("", regs, &info, esr);
}
該函數中關鍵的處理:根據傳進來的esr
獲取fault_info
信息,從而去調用函數。struct fault_info
用於錯誤狀態下對應的處理方法,而內核中也定義了全局結構fault_info
,存放了所有的情況。
主要的錯誤狀態和處理函數對應如下:
static const struct fault_info fault_info[] = {
{ do_bad, SIGBUS, 0, "ttbr address size fault" },
{ do_bad, SIGBUS, 0, "level 1 address size fault" },
{ do_bad, SIGBUS, 0, "level 2 address size fault" },
{ do_bad, SIGBUS, 0, "level 3 address size fault" },
{ do_translation_fault, SIGSEGV, SEGV_MAPERR, "level 0 translation fault" },
{ do_translation_fault, SIGSEGV, SEGV_MAPERR, "level 1 translation fault" },
{ do_translation_fault, SIGSEGV, SEGV_MAPERR, "level 2 translation fault" },
{ do_translation_fault, SIGSEGV, SEGV_MAPERR, "level 3 translation fault" },
{ do_bad, SIGBUS, 0, "unknown 8" },
{ do_page_fault, SIGSEGV, SEGV_ACCERR, "level 1 access flag fault" },
{ do_page_fault, SIGSEGV, SEGV_ACCERR, "level 2 access flag fault" },
{ do_page_fault, SIGSEGV, SEGV_ACCERR, "level 3 access flag fault" },
{ do_bad, SIGBUS, 0, "unknown 12" },
{ do_page_fault, SIGSEGV, SEGV_ACCERR, "level 1 permission fault" },
{ do_page_fault, SIGSEGV, SEGV_ACCERR, "level 2 permission fault" },
{ do_page_fault, SIGSEGV, SEGV_ACCERR, "level 3 permission fault" },
...
};
從代碼中可以看出:
- 出現0/1/2/3級頁表轉換錯誤時,會調用
do_translation_fault
,實際中do_translation_fault
最終也會調用到do_page_fault
; - 出現1/2/3級頁表訪問許可權的時候,會調用
do_page_fault
; - 其他的錯誤則調用
do_bad
,其中未列出來的部分還包括do_sea
等操作函數;
do_translation_fault
do_page_fault
do_page_fault
函數為頁錯誤異常處理的核心函數,與體繫結構相關,上圖中的handle_mm_fault
函數為通用函數,也就是不管哪種處理器結構,最終都會調用到該函數。
3. handle_mm_fault
handle_mm_fault
用於處理用戶空間的頁錯誤異常:
- 進程在用戶模式下訪問用戶虛擬地址,觸發頁錯誤異常;
- 進程在內核模式下訪問用戶虛擬地址,觸發頁錯誤異常;
從do_page_fault
函數的流程圖中也能看出來,當觸發異常的虛擬地址屬於某個vma
,並且擁有觸發頁錯誤異常的許可權時,會調用到handle_mm_fault
函數,而handle_mm_fault
函數的主要邏輯是通過__handle_mm_fault
來實現的。
流程如下圖:
3.1 do_fault
do_fault
函數用於處理文件頁異常,包括以下三種情況:
- 讀文件頁錯誤;
- 寫私有文件頁錯誤;
- 寫共用文件頁錯誤;
3.2 do_anonymous_page
匿名頁的缺頁異常處理調用本函數,在以下情況下會觸發:
- malloc/mmap分配了進程地址空間區域,但是沒有進行映射處理,在首次訪問時觸發;
- 用戶棧不夠的情況下,進行棧區的擴大處理;
3.3 do_swap_page
如果訪問Swap頁面
出錯(頁面不在記憶體中),則從Swap cache
或Swap文件
中讀取該頁面。
由於在4.14內核
版本中,do_swap_page
調用的很多函數都是空函數,無法進一步的瞭解,大體的流程如下圖:
3.4 do_wp_page
do_wp_page
函數用於處理寫時複製(copy on write
),會在以下兩種情況處理:
- 創建子進程時,父子進程會以只讀方式共用私有的匿名頁和文件頁,當試圖寫的時候,觸發頁錯誤異常,從而複製物理頁,並創建映射;
- 進程創建私有文件映射,讀訪問後觸發異常,將文件頁讀入到
page cache
中,並以只讀模式創建映射,之後發生寫訪問後,觸發COW
;
關鍵的複製工作是由wp_page_copy
完成的: