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