Linux 內(nèi)核的頁面錯誤:原因與解決方案
當程序訪問虛擬內(nèi)存中的一個頁面時,如果該頁面當前不在物理內(nèi)存中,就會觸發(fā)一個稱為"page fault"(頁異常)的異常。操作系統(tǒng)需要處理這個異常,并將所需頁面從磁盤加載到內(nèi)存中。實現(xiàn)虛存管理的一個關(guān)鍵是page fault異常處理,其過程中主要涉及到函數(shù) — do_pgfault的具體實現(xiàn)。
比如,在程序的執(zhí)行過程中由于某種原因(頁框不存在/寫只讀頁等)而使 CPU 無法最終訪問到相應(yīng)的物理內(nèi)存單元,即無法完成從虛擬地址到物理地址映射時,CPU 會產(chǎn)生一次頁訪問異常,從而需要進行相應(yīng)的頁訪問異常的中斷服務(wù)例程。這個頁訪問異常處理的時機被操作系統(tǒng)充分利用來完成虛存管理,即實現(xiàn)“按需調(diào)頁”/“頁換入換出”處理的執(zhí)行時機。當相關(guān)處理完成后,頁訪問異常服務(wù)例程會返回到產(chǎn)生異常的指令處重新執(zhí)行,使得應(yīng)用軟件可以繼續(xù)正常運行下去。
一、內(nèi)存管理前奏:虛擬內(nèi)存與MMU
在深入探討頁異常之前,先來了解一下 Linux 內(nèi)存管理的基礎(chǔ)架構(gòu)。在 Linux 系統(tǒng)中,進程并不直接訪問物理內(nèi)存,而是通過內(nèi)存管理單元(Memory Management Unit,MMU)來管理虛擬地址與物理地址的映射關(guān)系。
想象一下,你正在玩一款角色扮演游戲,每個角色都有自己獨立的背包(虛擬地址空間),背包里的物品位置(虛擬地址)與實際倉庫(物理內(nèi)存)中的存儲位置是通過游戲管理員(MMU)來協(xié)調(diào)的。這樣,每個角色都覺得自己的背包很大,有足夠的空間放置物品,而實際上倉庫的空間是有限的。這就是虛擬內(nèi)存的作用,它讓進程以為自己擁有很大的連續(xù)內(nèi)存空間,而不必關(guān)心物理內(nèi)存的實際布局和大小限制。
什么是虛擬內(nèi)存?
簡單地說是指程序員或CPU“看到”的內(nèi)存。但有幾點需要注意:
- 虛擬內(nèi)存單元不一定有實際的物理內(nèi)存單元對應(yīng),即實際的物理內(nèi)存單元可能不存在;
- 如果虛擬內(nèi)存單元對應(yīng)有實際的物理內(nèi)存單元,那二者的地址一般是不相等的;
- 通過操作系統(tǒng)實現(xiàn)的某種內(nèi)存映射可建立虛擬內(nèi)存與物理內(nèi)存的對應(yīng)關(guān)系,使得程序員或CPU訪問的虛擬內(nèi)存地址會自動轉(zhuǎn)換為一個物理內(nèi)存地址。
那么這個“虛擬”的作用或意義在哪里體現(xiàn)呢?在操作系統(tǒng)中,虛擬內(nèi)存其實包含多個虛擬層次,在不同的層次體現(xiàn)了不同的作用。首先,在有了分頁機制后,程序員或CPU“看到”的地址已經(jīng)不是實際的物理地址了,這已經(jīng)有一層虛擬化,我們可簡稱為內(nèi)存地址虛擬化。有了內(nèi)存地址虛擬化,我們就可以通過設(shè)置頁表項來限定軟件運行時的訪問空間,確保軟件運行不越界,完成內(nèi)存訪問保護的功能。
虛擬內(nèi)存地址空間的引入,不僅解決了物理內(nèi)存不足的問題,還提供了內(nèi)存保護和進程隔離的功能。每個進程都有自己獨立的虛擬地址空間,彼此之間互不干擾,就像不同的游戲角色在各自的背包里操作物品,不會影響到其他角色的背包。這樣,一個進程的內(nèi)存訪問錯誤不會導(dǎo)致整個系統(tǒng)崩潰,大大提高了系統(tǒng)的穩(wěn)定性和安全性。
通過 MMU 的映射,虛擬地址被轉(zhuǎn)換為物理地址,這個過程就像是游戲管理員根據(jù)角色背包里的物品位置信息,到實際倉庫中找到對應(yīng)的物品。MMU 通過維護頁表(Page Table)來記錄虛擬地址與物理地址的映射關(guān)系,頁表就像是一本詳細的地址轉(zhuǎn)換字典,MMU 根據(jù)虛擬地址在頁表中查找對應(yīng)的物理地址。
在 32 位的 Linux 系統(tǒng)中,虛擬地址空間通常為 4GB,其中一部分用于用戶空間,另一部分用于內(nèi)核空間。用戶空間的進程只能訪問自己的虛擬地址空間,無法直接訪問內(nèi)核空間,這種隔離機制有效地保護了內(nèi)核的安全,防止用戶進程的非法操作對內(nèi)核造成破壞 。例如,普通用戶在自己的權(quán)限范圍內(nèi)進行文件操作,無法直接訪問系統(tǒng)核心文件,保障了系統(tǒng)的穩(wěn)定性。
虛擬內(nèi)存與 MMU 的這種映射機制,為 Linux 系統(tǒng)的內(nèi)存管理奠定了堅實的基礎(chǔ),同時也為頁異常的發(fā)生埋下了伏筆,當進程訪問的虛擬地址在頁表中找不到對應(yīng)的物理地址映射時,頁異常就會登場 。
二、數(shù)據(jù)結(jié)構(gòu)與函數(shù)
首先是初始化過程。參考ucore總控函數(shù)init的代碼,可以看到在調(diào)用完成虛擬內(nèi)存初始化的vmm_init函數(shù)之前,需要首先調(diào)用pmm_init函數(shù)完成物理內(nèi)存的管理,這也是我們lab2已經(jīng)完成的內(nèi)容。接著是執(zhí)行中斷和異常相關(guān)的初始化工作,即調(diào)用pic_init函數(shù)和idt_init函數(shù)等,這些工作與lab1的中斷異常初始化工作的內(nèi)容是相同的。
在調(diào)用完idt_init函數(shù)之后,將進一步調(diào)用三個lab3中才有的新函數(shù)vmm_init、ide_init和swap_init。這三個函數(shù)涉及了本次實驗中的兩個練習。第一個函數(shù)vmm_init是檢查我們的練習1是否正確實現(xiàn)了。為了表述不在物理內(nèi)存中的“合法”虛擬頁,需要有數(shù)據(jù)結(jié)構(gòu)來描述這樣的頁,為此ucore建立了mm_struct和vma_struct數(shù)據(jù)結(jié)構(gòu)(接下來的小節(jié)中有進一步詳細描述),假定我們已經(jīng)描述好了這樣的“合法”虛擬頁,當ucore訪問這些“合法”虛擬頁時,會由于沒有虛實地址映射而產(chǎn)生頁訪問異常。如果我們正確實現(xiàn)了練習1,則do_pgfault函數(shù)會申請一個空閑物理頁,并建立好虛實映射關(guān)系,從而使得這樣的“合法”虛擬頁有實際的物理頁幀對應(yīng)。這樣練習1就算完成了。
ide_init和swap_init是為練習2準備的。由于頁面置換算法的實現(xiàn)存在對硬盤數(shù)據(jù)塊的讀寫,所以ide_init就是完成對用于頁換入換出的硬盤(簡稱swap硬盤)的初始化工作。完成ide_init函數(shù)后,ucore就可以對這個swap硬盤進行讀寫操作了。swap_init函數(shù)首先建立swap_manager,swap_manager是完成頁面替換過程的主要功能模塊,其中包含了頁面置換算法的實現(xiàn)(具體內(nèi)容可參考5小節(jié))。
然后會進一步調(diào)用執(zhí)行check_swap函數(shù)在內(nèi)核中分配一些頁,模擬對這些頁的訪問,這會產(chǎn)生頁訪問異常。如果我們正確實現(xiàn)了練習2,就可通過do_pgfault來調(diào)用swap_map_swappable函數(shù)來查詢這些頁的訪問情況并間接調(diào)用實現(xiàn)頁面置換算法的相關(guān)函數(shù),把“不常用”的頁換出到磁盤上。
ucore在實現(xiàn)上述技術(shù)時,需要解決三個關(guān)鍵問題:
- 當程序運行中訪問內(nèi)存產(chǎn)生pagefault異常時,如何判定這個引起異常的虛擬地址內(nèi)存訪問是越界、寫只讀頁的“非法地址”訪問還是由于數(shù)據(jù)被臨時換出到磁盤上或還沒有分配內(nèi)存的“合法地址”訪問?
- 何時進行請求調(diào)頁/頁換入換出處理?
- 如何在現(xiàn)有ucore的基礎(chǔ)上實現(xiàn)頁替換算法?
接下來將進一步分析完成lab3主要注意的關(guān)鍵問題和涉及的關(guān)鍵數(shù)據(jù)結(jié)構(gòu)。
對于第一個問題的出現(xiàn),在于實驗二中有關(guān)內(nèi)存的數(shù)據(jù)結(jié)構(gòu)和相關(guān)操作都是直接針對實際存在的資源—物理內(nèi)存空間的管理,沒有從一般應(yīng)用程序?qū)?nèi)存的“需求”考慮,即需要有相關(guān)的數(shù)據(jù)結(jié)構(gòu)和操作來體現(xiàn)一般應(yīng)用程序?qū)μ摂M內(nèi)存的“需求”。一般應(yīng)用程序的對虛擬內(nèi)存的“需求”與物理內(nèi)存空間的“供給”沒有直接的對應(yīng)關(guān)系,ucore是通過page fault異常處理來間接完成這二者之間的銜接。
page_fault函數(shù)不知道哪些是“合法”的虛擬頁,原因是ucore還缺少一定的數(shù)據(jù)結(jié)構(gòu)來描述這種不在物理內(nèi)存中的“合法”虛擬頁。為此ucore通過建立mm_struct和vma_struct數(shù)據(jù)結(jié)構(gòu),描述了ucore模擬應(yīng)用程序運行所需的合法內(nèi)存空間。當訪問內(nèi)存產(chǎn)生page fault異常時,可獲得訪問的內(nèi)存的方式(讀或?qū)懀┮约熬唧w的虛擬內(nèi)存地址,這樣ucore就可以查詢此地址,看是否屬于vma_struct數(shù)據(jù)結(jié)構(gòu)中描述的合法地址范圍中,如果在,則可根據(jù)具體情況進行請求調(diào)頁/頁換入換出處理(這就是練習2涉及的部分);如果不在,則報錯。mm_struct和vma_struct數(shù)據(jù)結(jié)構(gòu)結(jié)合頁表表示虛擬地址空間和物理地址空間的示意圖如下所示:
在ucore中描述應(yīng)用程序?qū)μ摂M內(nèi)存“需求”的數(shù)據(jù)結(jié)構(gòu)是vma_struct(定義在vmm.h中),以及針對vma_struct的函數(shù)操作。這里把一個vma_struct結(jié)構(gòu)的變量簡稱為vma變量。vma_struct的定義如下:
struct vma_struct {
// the set of vma using the same PDT
struct mm_struct *vm_mm;
uintptr_t vm_start; // start addr of vma
uintptr_t vm_end; // end addr of vma
uint32_t vm_flags; // flags of vma
//linear list link which sorted by start addr of vma
list_entry_t list_link;
};
vm_start和vm_end描述了一個連續(xù)地址的虛擬內(nèi)存空間的起始位置和結(jié)束位置,這兩個值都應(yīng)該是PGSIZE 對齊的,而且描述的是一個合理的地址空間范圍(即嚴格確保 vm_start < vm_end的關(guān)系);list_link是一個雙向鏈表,按照從小到大的順序把一系列用vma_struct表示的虛擬內(nèi)存空間鏈接起來,并且還要求這些鏈起來的vma_struct應(yīng)該是不相交的,即vma之間的地址空間無交集;vm_flags表示了這個虛擬內(nèi)存空間的屬性,目前的屬性包括:
#define VM_READ 0x00000001 //只讀
#define VM_WRITE 0x00000002 //可讀寫
#define VM_EXEC 0x00000004 //可執(zhí)行
vm_mm是一個指針,指向一個比vma_struct更高的抽象層次的數(shù)據(jù)結(jié)構(gòu)mm_struct,這里把一個mm_struct結(jié)構(gòu)的變量簡稱為mm變量。這個數(shù)據(jù)結(jié)構(gòu)表示了包含所有虛擬內(nèi)存空間的共同屬性,具體定義如下:
struct mm_struct {
// linear list link which sorted by start addr of vma
list_entry_t mmap_list;
// current accessed vma, used for speed purpose
struct vma_struct *mmap_cache;
pde_t *pgdir; // the PDT of these vma
int map_count; // the count of these vma
void *sm_priv; // the private data for swap manager
};
mmap_list是雙向鏈表頭,鏈接了所有屬于同一頁目錄表的虛擬內(nèi)存空間,mmap_cache是指向當前正在使用的虛擬內(nèi)存空間,由于操作系統(tǒng)執(zhí)行的“局部性”原理,當前正在用到的虛擬內(nèi)存空間在接下來的操作中可能還會用到,這時就不需要查鏈表,而是直接使用此指針就可找到下一次要用到的虛擬內(nèi)存空間。由于mmap_cache 的引入,可使得 mm_struct 數(shù)據(jù)結(jié)構(gòu)的查詢加速 30% 以上。
pgdir所指向的就是 mm_struct數(shù)據(jù)結(jié)構(gòu)所維護的頁表。通過訪問pgdir可以查找某虛擬地址對應(yīng)的頁表項是否存在以及頁表項的屬性等。map_count記錄mmap_list 里面鏈接的 vma_struct的個數(shù)。sm_priv指向用來鏈接記錄頁訪問情況的鏈表頭,這建立了mm_struct和后續(xù)要講到的swap_manager之間的聯(lián)系。
涉及vma_struct的操作函數(shù)也比較簡單,主要包括三個:
- vma_create—創(chuàng)建vma
- insert_vma_struct—插入一個vma
- find_vma—查詢vma。
vma_create函數(shù)根據(jù)輸入?yún)?shù)vm_start、vm_end、vm_flags來創(chuàng)建并初始化描述一個虛擬內(nèi)存空間的vma_struct結(jié)構(gòu)變量。insert_vma_struct函數(shù)完成把一個vma變量按照其空間位置[vma->vm_start,vma->vm_end]從小到大的順序插入到所屬的mm變量中的mmap_list雙向鏈表中。find_vma根據(jù)輸入?yún)?shù)addr和mm變量,查找在mm變量中的mmap_list雙向鏈表中某個vma包含此addr,即vma->vm_start<=addr end。這三個函數(shù)與后續(xù)講到的page fault異常處理有緊密聯(lián)系。
涉及mm_struct的操作函數(shù)比較簡單,只有mm_create和mm_destroy兩個函數(shù),從字面意思就可以看出是是完成mm_struct結(jié)構(gòu)的變量創(chuàng)建和刪除。在mm_create中用kmalloc分配了一塊空間,所以在mm_destroy中也要對應(yīng)進行釋放。在ucore運行過程中,會產(chǎn)生描述虛擬內(nèi)存空間的vma_struct結(jié)構(gòu),所以在mm_destroy中也要進對這些mmap_list中的vma進行釋放。
三、Page Fault異常處理
當進程訪問它的虛擬地址空間中的 PAGE 時,如果這個 PAGE 目前還不在物理內(nèi)存中,此時 CPU 就像一個找不到文件的辦事員,無法繼續(xù)工作。Linux 會立即產(chǎn)生一個 hard page fault 中斷,這就像是辦事員向上級報告文件缺失的情況 。
在這個過程中,系統(tǒng)需要從慢速設(shè)備(如磁盤)將對應(yīng)的數(shù)據(jù) PAGE 讀入物理內(nèi)存,就好比從倉庫(磁盤)中找到文件并取出來。然后,建立物理內(nèi)存地址與虛擬地址空間 PAGE 的映射關(guān)系,這一步就像是給文件貼上標簽,標明它在虛擬地址空間中的位置。只有完成這些步驟后,進程才能訪問這部分虛擬地址空間的內(nèi)存,辦事員才能繼續(xù)處理文件。
產(chǎn)生頁訪問異常的原因主要有:
- 目標頁幀不存在(頁表項全為0,即該線性地址與物理地址尚未建立映射或者已經(jīng)撤銷);
- 相應(yīng)的物理頁幀不在內(nèi)存中(頁表項非空,但Present標志位=0,比如在swap分區(qū)或磁盤文件上),這在本次實驗中會出現(xiàn),我們將在下面介紹換頁機制實現(xiàn)時進一步講解如何處理;
- 不滿足訪問權(quán)限(此時頁表項P標志=1,但低權(quán)限的程序試圖訪問高權(quán)限的地址空間,或者有程序試圖寫只讀頁面)
當出現(xiàn)上面情況之一,那么就會產(chǎn)生頁面page fault(#PF)異常。CPU會把產(chǎn)生異常的線性地址存儲在CR2中,并且把表示頁訪問異常類型的值(簡稱頁訪問異常錯誤碼,errorCode)保存在中斷棧中。
頁訪問異常錯誤碼有32位。位0為1表示對應(yīng)物理頁不存在;位1為1表示寫異常(比如寫了只讀頁;位2為1表示訪問權(quán)限異常(比如用戶態(tài)程序訪問內(nèi)核空間的數(shù)據(jù))CR2是頁故障線性地址寄存器,保存最后一次出現(xiàn)頁故障的全32位線性地址。CR2用于發(fā)生頁異常時報告出錯信息。當發(fā)生頁異常時,處理器把引起頁異常的線性地址保存在CR2中。操作系統(tǒng)中對應(yīng)的中斷服務(wù)例程可以檢查CR2的內(nèi)容,從而查出線性地址空間中的哪個頁引起本次異常。
產(chǎn)生頁訪問異常后,CPU硬件和軟件都會做一些事情來應(yīng)對此事。首先頁訪問異常也是一種異常,所以針對一般異常的硬件處理操作是必須要做的,即CPU在當前內(nèi)核棧保存當前被打斷的程序現(xiàn)場,即依次壓入當前被打斷程序使用的EFLAGS,CS,EIP,errorCode;由于頁訪問異常的中斷號是0xE,CPU把異常中斷號0xE對應(yīng)的中斷服務(wù)例程的地址(vectors.S中的標號vector14處)加載到CS和EIP寄存器中,開始執(zhí)行中斷服務(wù)例程。這時ucore開始處理異常中斷,首先需要保存硬件沒有保存的寄存器。
在vectors.S中的標號vector14處先把中斷號壓入內(nèi)核棧,然后再在trapentry.S中的標號__alltraps處把DS、ES和其他通用寄存器都壓棧。自此,被打斷的程序執(zhí)行現(xiàn)場(context)被保存在內(nèi)核棧中。接下來,在trap.c的trap函數(shù)開始了中斷服務(wù)例程的處理流程,大致調(diào)用關(guān)系為:
trap—> trap_dispatch—>pgfault_handler—>do_pgfault
在操作系統(tǒng)中,do_pgfault(頁錯誤處理函數(shù))是由內(nèi)核調(diào)用的函數(shù)。當程序訪問一個尚未映射到物理內(nèi)存的頁面時,會觸發(fā)頁錯誤異常。此時,操作系統(tǒng)會捕獲這個異常,并將控制權(quán)轉(zhuǎn)移到do_pgfault函數(shù)中進行處理。
具體的調(diào)用關(guān)系可能因不同的操作系統(tǒng)和架構(gòu)而有所差異,以下是一般情況下的調(diào)用關(guān)系:
- 當發(fā)生頁錯誤時,CPU會產(chǎn)生一個異常,并將控制權(quán)交給操作系統(tǒng)。
- 操作系統(tǒng)根據(jù)異常類型確定是否為頁錯誤,并檢查導(dǎo)致頁錯誤的原因。
- 如果是頁面訪問權(quán)限問題或者缺頁(頁面尚未加載到物理內(nèi)存)等原因引起的頁錯誤,則執(zhí)行相應(yīng)的處理邏輯。
- 在處理邏輯中,操作系統(tǒng)可能需要分配物理內(nèi)存來滿足缺頁請求,并將相應(yīng)的頁面加載到物理內(nèi)存中。
- 執(zhí)行完必要的處理后,操作系統(tǒng)將重新設(shè)置相關(guān)寄存器和標志位,然后恢復(fù)被中斷的進程繼續(xù)執(zhí)行。
總之,do_pgfault函數(shù)作為內(nèi)核提供的一個重要回調(diào)函數(shù),在頁錯誤發(fā)生時負責處理該異常并采取必要措施
產(chǎn)生頁訪問異常后,CPU把引起頁訪問異常的線性地址裝到寄存器CR2中,并給出了出錯碼errorCode,說明了頁訪問異常的類型。ucore OS會把這個值保存在struct trapframe 中tf_err成員變量中。而中斷服務(wù)例程會調(diào)用頁訪問異常處理函數(shù)do_pgfault進行具體處理。這里的頁訪問異常處理是實現(xiàn)按需分頁、頁換入換出機制的關(guān)鍵之處。
ucore中do_pgfault函數(shù)是完成頁訪問異常處理的主要函數(shù),它根據(jù)從CPU的控制寄存器CR2中獲取的頁訪問異常的物理地址以及根據(jù)errorCode的錯誤類型來查找此地址是否在某個VMA的地址范圍內(nèi)以及是否滿足正確的讀寫權(quán)限,如果在此范圍內(nèi)并且權(quán)限也正確,這認為這是一次合法訪問,但沒有建立虛實對應(yīng)關(guān)系。所以需要分配一個空閑的內(nèi)存頁,并修改頁表完成虛地址到物理地址的映射,刷新TLB,然后調(diào)用iret中斷,返回到產(chǎn)生頁訪問異常的指令處重新執(zhí)行此指令。如果該虛地址不在某VMA范圍內(nèi),則認為是一次非法訪問。
1. 缺頁錯誤的分類處理
我們在前作內(nèi)存尋址中介紹了 CPU 發(fā)展過程中內(nèi)存尋址方式的變化?,F(xiàn)代 CPU 都支持分段和分頁的內(nèi)存尋址模式。出于尋址能力的考慮,現(xiàn)代操作系統(tǒng),也順應(yīng)著都支持段頁式的內(nèi)存管理模式。當然,雖然支持段頁式,但是 Linux 中只啟用了段基址為 0 的段。也就是說,在 Linux 當中,實際起作用的只有分頁模式。
具體來說,分頁模式在邏輯上將虛擬內(nèi)存和物理內(nèi)存同時等分成固定大小的塊。這些塊在虛擬內(nèi)存上稱之為「頁」,而在物理內(nèi)存上稱之為「頁幀」,并交由 CPU 中的 MMU 模塊來負責頁幀和頁之間的映射管理。
引入分頁模式的好處,可以大致概括為兩個方面:
- 允許虛存空間遠大于實際物理內(nèi)存大小的情況。這是因為,分頁之后,操作系統(tǒng)讀入磁盤的文件時,無需以文件為單位全部讀入,而可以以內(nèi)存頁為單位,分片讀入。同時,考慮到 CPU 不可能一次性需要使用整個內(nèi)存中的數(shù)據(jù),因此可以交由特定的算法,進行內(nèi)存調(diào)度:將長時間不用的頁幀內(nèi)的數(shù)據(jù)暫存到磁盤上。
- 減少了內(nèi)存碎片的產(chǎn)生。這是因為,引入分頁之后,內(nèi)存的分配管理都是以頁大?。ㄍǔJ?4KiB,擴展分頁模式下是 4MiB)為單位的;虛擬內(nèi)存中的頁總是對應(yīng)物理內(nèi)存中實際的頁幀。這樣一來,在虛擬內(nèi)存空間中,頁內(nèi)連續(xù)的內(nèi)存在物理內(nèi)存上也一定是連續(xù)的,不會產(chǎn)生碎片。
當進程在進行一些計算時,CPU 會請求內(nèi)存中存儲的數(shù)據(jù)。在這個請求過程中,CPU 發(fā)出的地址是邏輯地址(虛擬地址),然后交由 CPU 當中的 MMU 單元進行內(nèi)存尋址,找到實際物理內(nèi)存上的內(nèi)容。若是目標虛存空間中的內(nèi)存頁(因為某種原因),在物理內(nèi)存中沒有對應(yīng)的頁幀,那么 CPU 就無法獲取數(shù)據(jù)。這種情況下,CPU 是無法進行計算的,于是它就會報告一個缺頁錯誤(Page Fault)。
因為 CPU 無法繼續(xù)進行進程請求的計算,并報告了缺頁錯誤,用戶進程必然就中斷了。這樣的中斷稱之為缺頁中斷。在報告 Page Fault 之后,進程會從用戶態(tài)切換到系統(tǒng)態(tài),交由操作系統(tǒng)內(nèi)核的 Page Fault Handler 處理缺頁錯誤。
基本來說,缺頁錯誤可以分為兩類:硬缺頁錯誤(Hard Page Fault)和軟缺頁錯誤(Soft Page Fault)。這里,前者又稱為主要缺頁錯誤(Major Page Fault);后者又稱為次要缺頁錯誤(Minor Page Fault)。當缺頁中斷發(fā)生后,Page Fault Handler 會判斷缺頁的類型,進而處理缺頁錯誤,最終將控制權(quán)交給用戶態(tài)代碼。
若是此時物理內(nèi)存里,已經(jīng)有一個頁幀正是此時 CPU 請求的內(nèi)存頁,那么這是一個軟缺頁錯誤;于是,Page Fault Hander 會指示 MMU 建立相應(yīng)的頁幀到頁的映射關(guān)系。這一操作的實質(zhì)是進程間共享內(nèi)存——比如動態(tài)庫(共享對象),比如 mmap 的文件。
若是此時物理內(nèi)存中,沒有相應(yīng)的頁幀,那么這就是一個硬缺頁錯誤;于是 Page Fault Hander 會指示 CPU,從已經(jīng)打開的磁盤文件中讀取相應(yīng)的內(nèi)容到物理內(nèi)存,而后交由 MMU 建立這份頁幀到頁的映射關(guān)系。
不難發(fā)現(xiàn),軟缺頁錯誤只是在內(nèi)核態(tài)里輕輕地走了一遭,而硬缺頁錯誤則涉及到磁盤 I/O。因此,處理起來,硬缺頁錯誤要比軟缺頁錯誤耗時長得多。這就是為什么我們要求高性能程序必須在對外提供服務(wù)時,盡可能少地發(fā)生硬缺頁錯誤。
除了硬缺頁錯誤和軟缺頁錯誤之外,還有一類缺頁錯誤是因為訪問非法內(nèi)存引起的。前兩類缺頁錯誤中,進程嘗試訪問的虛存地址尚為合法有效的地址,只是對應(yīng)的物理內(nèi)存頁幀沒有在物理內(nèi)存當中。后者則不然,進程嘗試訪問的虛存地址是非法無效的地址。比如嘗試對 nullptr 解引用,就會訪問地址為 0x0 的虛存地址,這是非法地址。
此時 CPU 報出無效缺頁錯誤(Invalid Page Fault)。操作系統(tǒng)對無效缺頁錯誤的處理各不相同:Windows 會使用異常機制向進程報告;*nix 則會通過向進程發(fā)送 SIGSEGV 信號(11),引發(fā)內(nèi)存轉(zhuǎn)儲。
缺頁中斷會交給PageFaultHandler處理,其根據(jù)缺頁中斷的不同類型會進行不同的處理:
- Hard Page Fault:也被稱為Major Page Fault,翻譯為硬缺頁錯誤/主要缺頁錯誤,這時物理內(nèi)存中沒有對應(yīng)的頁幀,需要CPU打開磁盤設(shè)備讀取到物理內(nèi)存中,再讓MMU建立VA和PA的映射。
- Soft Page Fault:也被稱為Minor Page Fault,翻譯為軟缺頁錯誤/次要缺頁錯誤,這時物理內(nèi)存中是存在對應(yīng)頁幀的,只不過可能是其他進程調(diào)入的,發(fā)出缺頁異常的進程不知道而已,此時MMU只需要建立映射即可,無需從磁盤讀取寫入內(nèi)存,一般出現(xiàn)在多進程共享內(nèi)存區(qū)域。
- Invalid Page Fault:翻譯為無效缺頁錯誤,比如進程訪問的內(nèi)存地址越界訪問,又比如對空指針解引用內(nèi)核就會報segment fault錯誤中斷進程直接掛掉。
2. 缺頁錯誤出現(xiàn)的原因
(1) 頁表相關(guān)問題
當進程訪問虛擬地址時,首先會查詢頁表以獲取對應(yīng)的物理地址。如果頁表中找不到對應(yīng)虛擬地址的頁表項(Page Table Entry,PTE),就會觸發(fā)頁異常 。這可能是因為該虛擬地址是無效的,就像你在一個公司的員工名單(頁表)中查找一個根本不存在的員工的工號(虛擬地址),自然是找不到的。
另一種情況是,虛擬地址是有效的,但對應(yīng)的物理頁面尚未被載入主存,頁表項還沒有建立,就好比員工雖然存在,但還沒有分配工位(物理內(nèi)存位置),在名單上也沒有記錄其工位信息。比如在程序動態(tài)分配內(nèi)存時,malloc 函數(shù)只是在虛擬地址空間中預(yù)留了一段地址范圍,當進程首次訪問這段地址時,由于對應(yīng)的物理內(nèi)存尚未分配和映射,就會導(dǎo)致頁表中沒有相應(yīng)的 PTE,從而觸發(fā)頁異常 。
(2)訪問權(quán)限沖突
即使頁表中存在對應(yīng)虛擬地址的 PTE,但如果該 PTE 的訪問權(quán)限設(shè)置拒絕當前進程的訪問操作,也會引發(fā)頁異常。例如,一個進程試圖對一個只讀的頁面進行寫入操作,而該頁面的 PTE 中設(shè)置了只讀權(quán)限,這就好比你拿著一張只能進入圖書館閱讀區(qū)的通行證,卻試圖進入書庫(禁止區(qū)域)取書,自然會被拒絕并觸發(fā)異常。這種情況通常用于保護系統(tǒng)關(guān)鍵數(shù)據(jù)和代碼,防止進程的非法訪問和修改,保障系統(tǒng)的穩(wěn)定性和安全性。
四、處理流程全解析
1. 捕獲與跳轉(zhuǎn)
當頁異常發(fā)生時,首先由 CPU 捕獲這個異常信號。就像在一個公司里,員工(進程)在執(zhí)行任務(wù)(訪問內(nèi)存)時遇到問題(頁異常),會立即向上級(CPU)報告 。CPU 捕獲到這個異常后,會跳轉(zhuǎn)到專門處理頁異常的函數(shù)page_fault_handler,這個函數(shù)就像是公司里專門處理問題的部門,負責解決內(nèi)存訪問異常的問題 。在page_fault_handler中,會進一步分析異常的原因和類型,為后續(xù)的處理做準備。
2. 處理邏輯分支
(1) 無效地址處理
如果經(jīng)檢查發(fā)現(xiàn)訪問的地址是無效地址,屬于越界訪問,系統(tǒng)會返回segment fault錯誤 。這就好比員工試圖進入一個被禁止進入的辦公室(無效地址區(qū)域),會被保安(系統(tǒng))阻止并報告給上級。如果是用戶地址發(fā)生segment fault,系統(tǒng)會直接殺死該進程,以防止進程對系統(tǒng)造成進一步的破壞,就像公司會開除違反規(guī)定的員工 。而如果是內(nèi)核地址發(fā)生segment fault,情況則更為嚴重,可能會導(dǎo)致內(nèi)核崩潰,就像公司的核心管理層出現(xiàn)嚴重問題,可能會導(dǎo)致整個公司運營癱瘓 。
(2) 有效地址處理
首次訪問:當頁是第一次被訪問時,會執(zhí)行demand_page_faults(請求調(diào)頁)操作 。這時候,系統(tǒng)會檢查頁表中是否存在該虛擬地址對應(yīng)的頁表項(PTE),即通過pmd_none和pte_none等函數(shù)來判斷 。如果不存在,就需要分配新的頁幀,并對其進行初始化,從磁盤中讀取相應(yīng)的數(shù)據(jù)到內(nèi)存中 。這就像是公司要為新入職的員工(新訪問的頁)分配一個工位(頁幀),并從倉庫(磁盤)中取出相關(guān)的辦公用品(數(shù)據(jù))放到工位上,以便員工能夠正常開展工作(進程能夠正常訪問內(nèi)存) 。
頁在 swap 分區(qū):如果頁被交換到了 swap 分區(qū),系統(tǒng)會檢查頁表項中的present標志位 。這個標志位就像是一個標簽,用來標識頁面是否在主存中 。如果present標志位為 0,表示該頁不在主存中,此時需要分配新的頁幀,并從磁盤的 swap 分區(qū)重新讀入內(nèi)存 。這就好比員工的辦公用品被暫時存放到了倉庫的臨時存儲區(qū)(swap 分區(qū)),當員工需要使用時,需要從臨時存儲區(qū)把辦公用品取回到工位(內(nèi)存)上 。
COW 情況:當vm_area_struct允許寫操作,但對應(yīng)的頁表項(PTE)禁止寫操作時,就會觸發(fā)寫時復(fù)制(Copy-On-Write,COW)機制 。這是一種優(yōu)化策略,在多個進程共享同一個頁面時,只有當某個進程試圖對頁面進行寫操作時,才會真正復(fù)制出一個新的頁面供該進程使用,而不是在一開始就為每個進程都復(fù)制一份 。就好比多個員工共同使用一份文件(共享頁面),當其中一個員工想要修改文件內(nèi)容(寫操作)時,系統(tǒng)會為他復(fù)制一份文件副本(新頁面),讓他在副本上進行修改,而不影響其他員工使用的原始文件 。這樣可以節(jié)省內(nèi)存資源,提高系統(tǒng)的效率 。
五、實例與應(yīng)用場景
1. 程序運行中的體現(xiàn)
以一個簡單的 C 程序為例,當程序執(zhí)行到malloc函數(shù)分配內(nèi)存時,實際上只是在虛擬地址空間中預(yù)留了一段地址范圍,并沒有立即分配物理內(nèi)存。只有當程序首次訪問這段地址時,才會觸發(fā)頁異常 。假設(shè)我們有如下 C 代碼:
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr = (int *)malloc(1024 * sizeof(int)); // 分配內(nèi)存,但未實際占用物理內(nèi)存
if (ptr == NULL) {
perror("malloc failed");
return 1;
}
// 首次訪問分配的內(nèi)存,會觸發(fā)頁異常
ptr[0] = 100;
printf("Value at ptr[0]: %d\n", ptr[0]);
free(ptr);
return 0;
}
在這段代碼中,malloc函數(shù)分配了一段虛擬內(nèi)存地址,當執(zhí)行ptr[0] = 100;時,由于這是首次訪問該地址,對應(yīng)的物理內(nèi)存尚未分配和映射,系統(tǒng)會觸發(fā)頁異常。操作系統(tǒng)會捕獲這個異常,為該虛擬地址分配物理內(nèi)存頁幀,并建立虛擬地址與物理地址的映射關(guān)系,然后程序才能成功地將值 100 寫入ptr[0] 。
2. 系統(tǒng)性能影響
頁異常對系統(tǒng)性能有著顯著的影響,尤其是硬缺頁。由于硬缺頁需要從磁盤讀取數(shù)據(jù)到內(nèi)存,而磁盤 I/O 的速度遠遠慢于內(nèi)存訪問速度,頻繁的硬缺頁會導(dǎo)致系統(tǒng)性能大幅下降 。當一個進程頻繁地訪問大量數(shù)據(jù),而這些數(shù)據(jù)又不在物理內(nèi)存中時,就會不斷地觸發(fā)硬缺頁。比如一個數(shù)據(jù)庫管理系統(tǒng)在處理大量數(shù)據(jù)查詢時,如果內(nèi)存不足,無法緩存所有需要的數(shù)據(jù),就會頻繁地從磁盤讀取數(shù)據(jù),導(dǎo)致大量的硬缺頁發(fā)生。這不僅會增加磁盤 I/O 的負擔,還會使進程的執(zhí)行速度明顯變慢,進而影響整個系統(tǒng)的響應(yīng)速度和吞吐量。
相比之下,軟缺頁的影響相對較小,因為它不需要從磁盤讀取數(shù)據(jù),只是建立映射關(guān)系,這個過程相對快速。但如果軟缺頁過于頻繁,也會消耗一定的系統(tǒng)資源,影響系統(tǒng)的整體性能 。例如,在一個多進程共享內(nèi)存的場景中,如果進程之間頻繁地訪問共享內(nèi)存,可能會導(dǎo)致大量的軟缺頁,因為每個進程在首次訪問共享內(nèi)存時都需要建立映射關(guān)系。雖然單個軟缺頁的處理時間較短,但大量的軟缺頁累積起來,也會對系統(tǒng)性能產(chǎn)生一定的影響。