在取指令或者數(shù)據(jù)的時候,處理器的MMU單元需要把虛擬地址轉(zhuǎn)換成物理地址漓滔。如果虛擬頁沒有映射到物理頁编饺,或者沒有訪問權(quán)限,處理器將生成頁錯誤異常响驴。
缺頁異常透且,虛擬頁沒有映射到物理頁,有以下幾種情況:
(1)訪問用戶棧的時候豁鲤,超出了當(dāng)前用戶棧的范圍秽誊,需要擴(kuò)大用戶棧。
(2)當(dāng)進(jìn)程申請?zhí)摂M內(nèi)區(qū)域的時候琳骡,通常是沒有分配物理頁锅论,進(jìn)程第一次訪問的時候觸發(fā)頁錯誤異常。
(3)內(nèi)存不足的時候楣号,內(nèi)核把進(jìn)程的匿名頁換出到交換區(qū)最易。
(4)一個文件頁被映射到進(jìn)程的虛擬地址空間,內(nèi)存不足的時候炫狱,內(nèi)核回收這個文件頁藻懒,在進(jìn)程的頁表中刪除這個文件頁的映射。
(5)程序錯誤视译,訪問沒有分配給進(jìn)程的虛擬內(nèi)存區(qū)域嬉荆。
針對前四種情況,如果頁錯誤異常處理程序成功地把虛擬頁映射到物理頁酷含,處理程序返回后鄙早,處理器重新執(zhí)行觸發(fā)異常的指令。第五種異常椅亚,發(fā)送段違法信號(SIGSEGV)殺死進(jìn)程限番。
沒有訪問權(quán)限,有以下兩種情況:
(1)可能是軟件有意造成的什往,典型的例子是寫時復(fù)制:進(jìn)程分叉成子進(jìn)程的時候扳缕,為了避免復(fù)制物理頁,子進(jìn)程和父進(jìn)程以只讀的方式共享所有私有的匿名頁和文件頁别威。當(dāng)其中一個進(jìn)程試圖寫只讀頁時躯舔,觸發(fā)頁錯誤異常 ,頁錯誤異常處理程序分配新的物理頁省古,把舊的物理頁的數(shù)據(jù)復(fù)制到新的物理頁粥庄,然后把虛擬頁映射到新的物理頁。
(2)程序錯誤豺妓,例如試圖寫只讀的代碼段所在的物理頁惜互。
第一種情況布讹,如果頁錯誤異常處理程序成功地把虛擬頁映射到物理頁,處理程序返回后训堆,處理器重新執(zhí)行觸發(fā)異常的指令描验。第二種情況,頁錯誤異常處理程序?qū)l(fā)送段違法(SIGSEGV)信號殺死進(jìn)程坑鱼。
不同處理器架構(gòu)實現(xiàn)的頁錯誤異常不同膘流,頁錯誤異常處理程序的前面一部分是各種處理器架構(gòu)自定義的部分,后面從函數(shù)handle_mm_fault開始的部分是所有處理器架構(gòu)共用的部分鲁沥。
1 處理器架構(gòu)特定部分
1.1 處理頁異常錯誤
ARM64架構(gòu)的內(nèi)核定義了一個異常向量表呼股,起始地址是vectors(arch/arm64/kernel/entry.S),每個異常向量的長度是128字節(jié)画恰,但是在Linux內(nèi)核的每個異常向量只有一條指令:跳轉(zhuǎn)到對應(yīng)的處理器程序彭谁。異常向量表的虛擬地址存放在異常級別1的向量基準(zhǔn)地址寄存器VBAR_EL1中。
處理器生成頁錯誤異常允扇,頁錯誤異常屬于同步異常缠局,處理器立即處理,從向量基準(zhǔn)地址寄存器得到異常向量表的虛擬地址考润,然后根據(jù)異常類型選擇對應(yīng)的異常向量甩鳄。
(1)如果異常類型是異常級別1生成的同步異常,異常向量的偏移是0x200额划,跳轉(zhuǎn)到函數(shù)el1_sync。
(2)如果異常類型是異常級別0的64bit用戶態(tài)程序生成的同步異常档泽,異常向量的偏移是0x400俊戳,跳轉(zhuǎn)到el0_sync。
(3)如果異常類型是異常級別0的32bit用戶態(tài)程序生成的同步異常馆匿,異常向量的偏移是0x600抑胎,跳轉(zhuǎn)到el0_sync_compat。
以el0_sync為例渐北,函數(shù)el0_sync根據(jù)異常級別1的異常癥狀寄存器的異常類別字段處理阿逃。
(1)如果異常類型是異常級別0生成的數(shù)據(jù)中止,即在異常級別0訪問數(shù)據(jù)時生成頁錯誤異常赃蛛,那么調(diào)用函數(shù)el0_da恃锉。
(2)如果異常類型是異常級別0生成的指令中止,即在異常級別0取指令時生成頁錯誤異常呕臂,那么調(diào)用函數(shù)el0_ia破托。
對于ARM64處理器,異常級別1的異常癥狀寄存器(ESR_EL1)用來存放異常的癥狀信息歧蒋。
EC:ESR_EL1[26:31]土砂,異常類別州既,指示引起異常的原因。
ISS:ESR_EL1[0:24]萝映,每種異常類別獨立定義這個字段吴叶。
頁錯誤異常處理程序最終都會執(zhí)行到函數(shù)do_mem_abort,該函數(shù)根據(jù)異常癥狀寄存器的指令特定癥狀字段的指令錯誤狀態(tài)碼(0~5)序臂,調(diào)用數(shù)組fault_info中的處理函數(shù)蚌卤。
static struct fault_info {
int (*fn)(unsigned long addr, unsigned int esr, struct pt_regs *regs);
int sig;
int code;
const char *name;
} 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" },
{ do_bad, SIGBUS, 0, "synchronous external abort" },
{ do_bad, SIGBUS, 0, "unknown 17" },
{ do_bad, SIGBUS, 0, "unknown 18" },
{ do_bad, SIGBUS, 0, "unknown 19" },
{ do_bad, SIGBUS, 0, "synchronous abort (translation table walk)" },
{ do_bad, SIGBUS, 0, "synchronous abort (translation table walk)" },
{ do_bad, SIGBUS, 0, "synchronous abort (translation table walk)" },
{ do_bad, SIGBUS, 0, "synchronous abort (translation table walk)" },
{ do_bad, SIGBUS, 0, "synchronous parity error" },
{ do_bad, SIGBUS, 0, "unknown 25" },
{ do_bad, SIGBUS, 0, "unknown 26" },
{ do_bad, SIGBUS, 0, "unknown 27" },
{ do_bad, SIGBUS, 0, "synchronous parity error (translation table walk)" },
{ do_bad, SIGBUS, 0, "synchronous parity error (translation table walk)" },
{ do_bad, SIGBUS, 0, "synchronous parity error (translation table walk)" },
{ do_bad, SIGBUS, 0, "synchronous parity error (translation table walk)" },
{ do_bad, SIGBUS, 0, "unknown 32" },
{ do_bad, SIGBUS, BUS_ADRALN, "alignment fault" },
{ do_bad, SIGBUS, 0, "unknown 34" },
{ do_bad, SIGBUS, 0, "unknown 35" },
{ do_bad, SIGBUS, 0, "unknown 36" },
{ do_bad, SIGBUS, 0, "unknown 37" },
{ do_bad, SIGBUS, 0, "unknown 38" },
{ do_bad, SIGBUS, 0, "unknown 39" },
{ do_bad, SIGBUS, 0, "unknown 40" },
{ do_bad, SIGBUS, 0, "unknown 41" },
{ do_bad, SIGBUS, 0, "unknown 42" },
{ do_bad, SIGBUS, 0, "unknown 43" },
{ do_bad, SIGBUS, 0, "unknown 44" },
{ do_bad, SIGBUS, 0, "unknown 45" },
{ do_bad, SIGBUS, 0, "unknown 46" },
{ do_bad, SIGBUS, 0, "unknown 47" },
{ do_bad, SIGBUS, 0, "TLB conflict abort" },
{ do_bad, SIGBUS, 0, "unknown 49" },
{ do_bad, SIGBUS, 0, "unknown 50" },
{ do_bad, SIGBUS, 0, "unknown 51" },
{ do_bad, SIGBUS, 0, "implementation fault (lockdown abort)" },
{ do_bad, SIGBUS, 0, "implementation fault (unsupported exclusive)" },
{ do_bad, SIGBUS, 0, "unknown 54" },
{ do_bad, SIGBUS, 0, "unknown 55" },
{ do_bad, SIGBUS, 0, "unknown 56" },
{ do_bad, SIGBUS, 0, "unknown 57" },
{ do_bad, SIGBUS, 0, "unknown 58" },
{ do_bad, SIGBUS, 0, "unknown 59" },
{ do_bad, SIGBUS, 0, "unknown 60" },
{ do_bad, SIGBUS, 0, "section domain fault" },
{ do_bad, SIGBUS, 0, "page domain fault" },
{ do_bad, SIGBUS, 0, "unknown 63" },
};
虛擬頁沒有映射到物理頁的情況:
(1)如果在0級、1級或2級轉(zhuǎn)換表中的匹配的表項是無效描述符贸宏,調(diào)用函數(shù)do_translation_fault來處理造寝;如果是在3級轉(zhuǎn)換表中匹配的表項是無效描述符,調(diào)用函數(shù)do_page_fault來處理吭练。
(2)如果在1級诫龙、2級或3級轉(zhuǎn)換表中匹配的表項是塊描述符或頁描述符,但是沒有設(shè)置訪問標(biāo)志鲫咽,那么調(diào)用函數(shù)do_page_fault將會為頁表項設(shè)置訪問標(biāo)志签赃。頁回收算法需要根據(jù)頁表項的訪問標(biāo)志判斷物理頁是不是剛剛被訪問過。
(3)如果是權(quán)限錯誤:在1級分尸,2級或3級轉(zhuǎn)換表中匹配的表項是塊描述符或頁描述符锦聊,但是沒有訪問權(quán)限,那么調(diào)用do_page_fault箩绍。
do_translation_fault判斷是如果觸發(fā)異常的虛擬地址是用戶虛擬地址孔庭,調(diào)用函數(shù)do_page_fault來處理;如果觸發(fā)異常的虛擬地址是內(nèi)核虛擬地址或不規(guī)范地址材蛛,調(diào)用函數(shù)do_bad_area來處理圆到。
do_page_fault中檢查不同是否在原子上下文中,以及觸發(fā)地址的權(quán)限問題卑吭。正常情況下芽淡,通過__do_page_fault處理頁錯誤異常。
禁止執(zhí)行頁錯誤異常處理程序的情況:有些系統(tǒng)調(diào)用傳入用戶空間的緩沖區(qū)豆赏,內(nèi)核使用用戶虛擬地址訪問緩沖區(qū)挣菲,可能生成頁錯誤異常;然而頁錯誤異常處理程序可能睡眠掷邦,但內(nèi)核在原子上下文中白胀,并不能睡眠;所以在原子上下文中抚岗,使用用戶虛擬地址訪問緩沖區(qū)之前纹笼,調(diào)用函數(shù)pagefault_disable禁止執(zhí)行頁錯誤異常處理程序。
__do_page_fault根據(jù)觸發(fā)異常的虛擬地址在進(jìn)程的虛擬內(nèi)存區(qū)域的紅黑樹中查找一個滿足條件的虛擬內(nèi)存區(qū)域:觸發(fā)異常的虛擬地址小于虛擬內(nèi)存區(qū)域的結(jié)束地址苟跪;沒找到虛擬內(nèi)存區(qū)域說明虛擬地址是非法的廷痘,返回VM_FAULT_BADMAP蔓涧;判斷這個區(qū)域是否是棧,是棧的話調(diào)用expand_stack笋额,擴(kuò)大棧的虛擬內(nèi)存區(qū)域元暴,擴(kuò)大成功,檢測權(quán)限兄猩,然后調(diào)用函數(shù)handle_mm_fault處理頁錯誤異常茉盏;非棧的情況下,檢查訪問權(quán)限枢冤,如果虛擬內(nèi)存區(qū)域沒有授予觸發(fā)頁錯誤異常的訪問權(quán)限鸠姨,然后調(diào)用handle_mm_fault。
最終在虛擬地址合法淹真,權(quán)限正常的情況下都會調(diào)用到handle_mm_fault函數(shù)處理頁錯誤異常讶迁。
2 用戶空間頁錯誤異常
從函數(shù)handle_mm_fault開始的部分是所有處理器架構(gòu)共用的部分,函數(shù)handle_mm_fault負(fù)責(zé)處理用戶空間的頁錯誤異常核蘸。用戶空間頁錯誤異常是指進(jìn)程訪問用戶虛擬地址生成的頁錯誤異常巍糯,分兩種情況:
(1)進(jìn)程在用戶模式下訪問用戶虛擬地址,生成頁錯誤異常客扎。
(2)進(jìn)程在內(nèi)核模式下訪問用戶虛擬地址祟峦,生成頁錯誤異常。
handle_mm_fault主要流程:創(chuàng)建觸發(fā)異常的虛擬地址對應(yīng)的各級頁表的頁表項徙鱼,然后調(diào)用handle_pte_fault宅楞。
2.1 handle_pte_fault
handle_pte_fault執(zhí)行流程如下:
handle_pte_fault
-->頁表項無效的情況下:處理頁表項無效
-->私有匿名映射 -->do_anonymous_page
-->文件映射/共享匿名映射 -->do_fault
-->頁不在內(nèi)存中-->do_swap_page
-->頁在內(nèi)存中:處理頁在內(nèi)存中的情況
-->處理寫訪問的情況
-->頁表項沒有設(shè)置寫權(quán)限位-->執(zhí)行寫時復(fù)制(do_wp_page)
-->pte_mkdirty
-->pte_mkyoung
-->ptep_set_access_flags
-->頁表項是否變化
-->是:update_mmu_cache
-->否:flush_tlb_fix_spurious_fault
2.2 匿名頁的缺頁異常
以下三種情況下會觸發(fā)匿名頁的缺頁異常:
(1)函數(shù)的局部變量比較大,或者函數(shù)調(diào)用的層次比較深袱吆,導(dǎo)致當(dāng)前棧不夠用咱筛,需要擴(kuò)大棧。
(2)進(jìn)程調(diào)用malloc杆故,從堆申請了內(nèi)存塊,只分配虛擬內(nèi)存區(qū)域溉愁,還沒有映射到物理頁处铛,第一次訪問時觸發(fā)缺頁異常。
(3)進(jìn)程直接調(diào)用mmap拐揭,創(chuàng)建匿名的內(nèi)存映射撤蟆,只分配了虛擬內(nèi)存區(qū)域,還沒映射到物理頁堂污,第一次訪問時觸發(fā)缺頁異常家肯。
2.3 文件頁的缺頁異常
以下兩種情況下會觸發(fā)文件頁的缺頁異常:
(1)啟動程序的時候,內(nèi)核為程序的代碼段和數(shù)據(jù)段創(chuàng)建私有的文件映射盟猖,映射到進(jìn)程的虛擬地址空間讨衣,第一次訪問時换棚,觸發(fā)文件頁的缺頁異常。
(2)進(jìn)程使用mmap創(chuàng)建文件映射反镇,把文件的一個區(qū)間映射到進(jìn)程的虛擬地址空間固蚤,第一次訪問時,觸發(fā)文件的缺頁異常歹茶。
do_fault的執(zhí)行流程:
do_fault
-->沒有提供vma->vm_ops->fault夕玩,返回VM_FAULT_SIGBUS
-->讀文件頁錯誤,do_read_fault
-->寫私有文件頁錯誤惊豺,do_cow_fault
-->寫共享文件頁錯誤燎孟,do_shared_fault
2.3.1處理讀文件頁錯誤
(1)把文件頁從存儲設(shè)備上的文件系統(tǒng)讀到文件的頁緩存(每個文件有一個緩存,因為以頁為單位尸昧,所以稱為頁緩存)揩页。
(2)設(shè)置進(jìn)程的頁表項彻磁,把虛擬頁映射到文件的頁緩存中的物理頁碍沐。
2.3.2處理寫私有文件錯誤
(1)把文件從存儲設(shè)備上的文件系統(tǒng)讀到文件的頁緩存中。
(2)執(zhí)行寫時復(fù)制衷蜓,為文件的頁緩存中的物理頁創(chuàng)建一個副本斋陪,這個副本是進(jìn)程的私有匿名頁无虚,和文件脫離關(guān)系友题,修改副本不會導(dǎo)致文件變化度宦。
(3)設(shè)備進(jìn)程的頁表項,把虛擬頁映射到副本告匠。
2.3.3處理寫共享文件錯誤
(1)把文件頁從存儲設(shè)備上的文件系統(tǒng)讀到文件的頁緩存中戈抄。
(2)設(shè)置進(jìn)程的頁表項,把虛擬頁映射到文件的頁緩存中的物理頁后专。
2.3.4 寫時復(fù)制
寫時復(fù)制的兩個場景:
(1)進(jìn)程分叉成子進(jìn)程的時候划鸽,為了避免復(fù)制物理頁,子進(jìn)程和父進(jìn)程以只讀的方式共享所有私有的匿名頁和文件頁。當(dāng)其中一個進(jìn)程試圖寫只讀頁時裸诽,觸發(fā)頁錯誤異常嫂用,頁錯誤異常處理程序分配新的物理頁,把舊的物理頁的數(shù)據(jù)復(fù)制到新的物理頁崭捍,然后把虛擬頁映射到新的物理頁上尸折。
(2)進(jìn)程創(chuàng)建私有的文件映射,然后讀訪問殷蛇,觸發(fā)頁錯誤異常实夹,異常處理程序把文件讀到頁緩存,然后以只讀模式把虛擬頁映射到文件的頁緩存中的物理頁粒梦。接著執(zhí)行寫訪問亮航,觸發(fā)頁錯誤異常,異常處理程序執(zhí)行寫時復(fù)制匀们,為文件的頁緩存中的物理頁創(chuàng)建一個副本缴淋,把虛擬頁映射到副本。這個副本是進(jìn)程的私有匿名頁泄朴,和文件脫離關(guān)系重抖,修改副本不會導(dǎo)致文件變化。
3 內(nèi)核模式頁錯誤異常
內(nèi)核訪問內(nèi)核虛擬地址祖灰,正常情況下不會出現(xiàn)虛擬頁沒有映射到物理頁的狀況钟沛,內(nèi)核使用線性映射區(qū)域的虛擬地址,在內(nèi)存管理子系統(tǒng)初始化的時候就會把虛擬地址映射到物理地址局扶;運行過程中恨统,可能使用vmalloc函數(shù)從vmalloc區(qū)域分配虛擬內(nèi)存區(qū)域,vmalloc函數(shù)會分配并且映射到物理頁三妈。如果出現(xiàn)虛擬頁沒有映射到物理頁的情況畜埋,一定是程序錯誤,內(nèi)核將會崩潰畴蒲。
內(nèi)核可能訪問用戶虛擬地址悠鞍,進(jìn)程通過系統(tǒng)調(diào)用進(jìn)入內(nèi)核模式,有些系統(tǒng)調(diào)用會傳入用戶空間的緩沖區(qū)模燥,內(nèi)核必須使用頭文件uaccess.h定義的專用函數(shù)訪問用戶空間的緩沖區(qū)咖祭,這些專用函數(shù)在異常表中添加了可能觸發(fā)異常的指令地址和異常修正程序的地址。
在內(nèi)核模式下執(zhí)行時觸發(fā)頁錯誤異常涧窒,ARM64架構(gòu)內(nèi)核的處理流程如下:
(1)如果不允許內(nèi)核執(zhí)行用戶空間的指令,那么進(jìn)程在內(nèi)核模式下試圖執(zhí)行用戶空間的指令時锭亏,內(nèi)核崩潰纠吴。
(2)如果進(jìn)程在內(nèi)核模式下訪問用戶虛擬地址,那么先使用函數(shù)__do_page_fault處理慧瘤;如果處理失敗戴已,最后通過__do_kernel_fault處理固该。
(3)其他情況使用函數(shù)__do_kernel_fault處理。
__do_kernel_fault針對數(shù)據(jù)的訪問觸發(fā)的異常糖儡,嘗試在異常表中查找異常修成程序伐坏。如果找到異常修正程序,把保存在內(nèi)核棧中的異常鏈接寄存器(ELR_EL1)的值改為異常修正程序的虛擬地址握联。當(dāng)異常處理程序返回時桦沉,處理器把程序計數(shù)器設(shè)置成異常鏈接寄存器的值,執(zhí)行異常修正程序金闽。