前言
在一個擁有32位的地址空間,4KB的頁面(212),並且每個PTE為4個字節,那么頁表大小為4MB(4 * 232 / 212),但若為64位地址空間,4KB的頁面(212)且每個PTE為4字節,那么頁表大小為16TB(4 * 264 / 212),由於頁表常駐內存,占用內存會很大,所以必須對頁表存儲結構進行優化,這就是我們本文所要講解的內容,常見的頁表數據結構為多級頁表(兩級、三級等)、倒置頁表、哈希頁表,我們來一一進行分析。
多級頁表
首先我們講講2級頁表,然后通過2級頁表延伸到多級頁表,現假設有16KB(214)的地址空間,並且每頁大小為64(26)字節,每個PTE為4字節,那么說明頁表為1KB(4 * 214 / 26),若我們有64字節的頁,那么將1KB可划分為16個64字節的頁面,每個頁面可容納16個PTE,前面我們講解到虛擬地址划分為虛擬頁號(VPN)和虛擬頁偏移量(VPO),但虛擬頁偏移量已經固定,那么我們只能從VPN下手將其作為索引用於索引頁表目錄,那么我們該如何利用VPN來構建各個部分的索引呢?我們首先需要構建頁表目錄,根據上述假設,總共有256個PTE分布在16頁上,頁目錄在頁表的每頁上需要一個條目,因此它具有16個條目, 最終我們需要VPN中的4位索引到目錄中,這也就意味着我們需要使用VPN的前4位,如下所示:

我們從VPN提取出了頁表目錄索引(PDI),那么我們就可以計算出每個PDE(Page Directory Entry)的地址值:
PDEAddress = PageDirectoryBase + (PDIndex * sizeof(PDE))。
有了頁目錄索引后我們還需進行進一步翻譯,如果頁目錄索引為空,很顯然第2級頁表根本就不會存在,如此一來則達到了減少內存的要求,因為只有第一級頁表才會存在於主存中,虛擬內存系統會根據需要調入或調出第2級頁表,這就減少了主存的壓力,只有經常使用的2級頁表才需要緩存在主存中。如果第1級頁表即頁目錄索引有值,那么還需根據頁目錄指向的頁表頁面去獲取PTE,要找到此PTE,我們還需要使用VPN的其余索引映射到頁表的部分。

通過使用如上頁表索引來索引頁表本身,從而找到PTE地址,也就找到了PFN(物理頁幀號)
PTEAddress = (PDE.PFN << SHIFT) + (PTIndex * sizeof(PTE))
從頁面目錄獲得的頁面幀號(PFN)必須先左移到適當位置,然后再與頁表索引組合以形成PTE的地址。假設如下為二級頁表扁平化的片段

如上第1片段為頁表目錄,在其中存在索引到第2級頁表的索引,還包括有效位,第2和第3片段分別為第1級頁表目錄索引對應的頁表(其中包含保護位,可讀?可寫?等等),假設CPU產生虛擬地址(0xFE16 = 25410 = 111111102),由於我們假設虛擬地址空間為14位,所以將轉換后的2進制不足用0填充即11111110000000,同時我們將地址空間進行虛擬頁號(VPN)和虛擬頁偏移量(VPO)划分,然后對VPN划分為頁表目錄和頁表索引,我們通過紅色、綠色、藍色由左至右分別代表頁表目錄索引、頁表索引、虛擬頁偏移量即1111 1110 000000,經過如此划分后,此時前4位(11112 = 1510)為頁表目錄索引,對應上述頁表目錄最后一行,此時頁表目錄幀號為101對應第2個頁表片段,然后根據接下來的4位(11102 = 1410),最終得到索引為倒數第2行,即最終物理頁幀號為55。最后我們通過如下物理地址計算公式
PhysAddress = (PTE.PFN << SHIFT) + offset
即最終物理地址為:55 * 26 + 000000 = 352010 = 0XDC016。假設為32位地址空間,那么頁目錄索引、頁表索引、虛擬頁偏移量分別對應為10、10、12位,那么對應的2級頁表將是如下形式

簡而言之,對於32位地址空間,會將VPN中的前10位(位22..31)用於索引頁表目錄,緊接下來的10位(12 ..21)用於索引所選的頁表。換言之,對於2級頁表結構其本質是:VPN的前m位為頁表目錄索引,而接下來的n位為頁表索引,同時需要注意的是2級頁表其地址是從上往下增加。根據上述將32位地址空間中的頁表以2級結構划分,此時第1級頁表大小為(1024 * 4) = 4KB,而第2級頁表為(1024 * 1024 * 4) = 4MB,所以頁表大小將為4KB + 4MB,這么算來比直接使用單級頁表結構為4MB情況更糟糕了不是嗎,其實情況並不是這樣,如上算出的4KB + 4MB為最極限的情況,上述已經講解過只有經常需要用到的2級頁表才緩存在主存中,所以實際情況下頁表大小會小於4MB。
早期操作系統采用的是2級頁表結構,但是現如今大多數操作系統采用多級頁表結構,就像樹一樣,不過是深度或層次更深了而已。假設我們有一個30位虛擬地址空間和一個較小的頁面(512字節),因此,我們的虛擬地址具有21位的虛擬頁號和9位的偏移量,使頁表的每個部分都適合單個頁面是構建多級頁表的目標,但到目前為止,我們僅考慮了頁表本身,如果頁表目錄很大,那該怎么辦?為了確定一個多級頁表中需要多少級才能使頁表的所有部分用一個頁面容納,我們首先確定一個頁面中可以容納多少個PTE。我們假設給定的頁面大小為512字節,並假設PTE大小為4字節,我們知道在單個頁面上可容納128個PTE。當我們索引到頁表的頁面時,可以得出結論,我們需要使用VPN的最低有效7位(log2128)作為索引

通過確定單頁面需要容納128個PTE,那么將占據地址空間7位,那么還剩下14位地址空間,如果將剩下的214作為頁表目錄, 那么將橫跨128頁而不再是1頁,那么對於構建多級頁表的目標將無法實現,為了解決這個問題,我們需要將14位進行再次划分,將頁表目錄進行設置為多頁,頁表目錄位於上方從而指向另一頁表目錄,因此我們可以進行如下划分

現在,在索引上層頁表目錄時,我們使用虛擬地址的最高位(圖中PD Index:0),該索引可用於從頂級頁表目錄中獲取頁表目錄的條目,如果有效,則對來自自頂層PDE和VPN的下一部分(PD Index:1)的物理幀號組合來查詢頁表目錄的第二層,最后,如果有效,則為PTE地址通過將頁表索引與第二級PDE中的地址結合使用,可以形成一個地址。 當然這個過程需要做很多工作,所有這些都是為了在多級表中查找物理頁幀號。最終多級頁表結構如下這般

上述我們講過若為64位地址空間,4KB的頁面(212)且每個PTE為4字節,在單級頁表情況下,那么頁表大小為16TB(4 * 264 / 212)= 16TB,若我們划分為3級,如下:

那么對於上述外部頁即頁目錄索引將需要占內存4 * 232 = 16GB,所以我們仍需繼續划分層級,但是每個層級都有一個額外的間接方式,因此會產生額外的開銷。比如64位地址空間在4KB頁面上將使用大地址空間,所以多級頁表成為具有小頁的大地址空間的內存消耗。
哈希頁表
處理大於32位地址空間常用的方法是使用哈希頁表(使用稀疏的地址空間),采用虛擬頁碼作為哈希值,對於每一個PTE使用鏈表結構存儲從而解決沖突或碰撞,每個元素由三個字段組成:虛擬頁碼、映射的頁幀、指向鏈表內下一個元素的指針。通過哈希算法將虛擬頁碼映射到哈希頁表,然后將虛擬頁碼與鏈表第一個元素的第一個字段進行比較,若匹配則將第二個字段用來形成物理地址,否則遍歷鏈表查找對應匹配項。哈希頁表如下圖所示

雖然通過哈希頁表查找很快,同時采用如上划重點標記的鏈表數據結構解決沖突問題,雖說消除了條目在內存中連續的需求,但是仍然以更高的內存開銷進行存儲即消耗更多內存,特別是如果頁表是完整的,並且具有有效/無效位以使未使用的條目無效,那么哈希頁表不再那么適用,此時我們采用其他方案,如下倒置頁表。
倒置頁表
通過前面內容學習我們知道對於每個進程都有一個關聯的頁表,該進程中的每一個虛擬頁都在頁表中對應一項,不管是否有效,進程通過虛擬地址引用頁,操作系統通過計算虛擬地址在頁表中的位置即PTE,但這種方式有明顯的缺點,如上我們也敘述過,每個頁表可能包含數以百萬計的條目,如此一來,頁表將占用大量的物理內存以跟蹤其他物理內存是如何使用的,為解決這個問題,我們可以使用倒置頁表(inverted page table),對於每個真正的內存頁,倒置頁表才有一個條目,每個條目包含保存在真正內存位置上的頁的虛擬地址,以及擁有該頁進程的信息,因此,整個系統中所有進程將只有一個頁表,並且每個物理內存的頁只有一個相應的條目,換言之,與知道每個進程的虛擬頁在哪里相反,現在我們知道擁有哪個物理頁的進程與它對應的虛擬頁。IBM是最早采用倒置頁表的公司,從IBM System 38、RS/6000、到現代的IBM Power CPU。對於IBM RT,系統的虛擬地址包含三部分:進程Id、頁碼、頁偏移量,每個倒置頁表條目包含兩部分:進程Id、頁碼,這里的進程Id作為地址空間的標識符,當發生內存引用時,由進程Id和頁碼組成的虛擬地址被提交到內存子系統,然后搜索倒置頁表來尋址匹配,如果找到匹配條目,則生成物理地址,如果未找到匹配條目則為非法地址訪問。 倒置頁表結構如下:

雖然倒置頁表減少了存儲每個頁表所需的內存空間,但是它增加了由於引用頁而查找頁表所需要的時間,由於倒置頁表是按照物理地址排序,而查找則是根據虛擬地址,因此查找匹配可能需要搜索整個表,這種搜索需要耗費很長時間,為解決這個問題,可以使用一個哈希表結構,從而將搜索限制在一個或最多數個頁表條目,當然,每訪問哈希表就增加了一次內存引用,因此每次虛擬地址的引用至少需要兩個內存讀,一個用於哈希表條目,另一個用於頁表條目即PTE,同時結合前面所學,在搜索哈希表之前,肯定先搜索TLB,這樣可大大提高性能。對於倒置頁表還會帶來一個問題,那就是實現共享內存,共享內存需要將多個虛擬地址映射到同一物理地址,很顯然,這種標准的方式無法應用於倒置頁表,因為每一個物理頁只有一個虛擬頁條目,一個物理頁不可能有兩個或多個共享的虛擬地址,所以為解決這個問題,只能允許頁表包含一個虛擬地址到共享物理地址的映射,這也就意味着,對於未映射的虛擬地址的引用勢必會導致頁錯誤。
總結
本節我們非常詳細的討論了多級頁表結構、對於哈希頁表和倒置頁表數據結構通過看圖理解起來非常簡單,從本節內容我們可總結出:對應頁表結構可以擁有良好的時間復雜度或空間復雜度,但不能同時兼得。到此關於虛擬內存重要內容基本上都已囊括,若有遺漏,后續我會繼續進行補充。接下來我們將進入內存管理分頁和分段的學習,講完之后,會陸續進入到程序的執行、進程、死鎖、並發等,相信大家會比較感興趣,感謝您的閱讀,我們下節再見。
