現代操作系統都使用分頁機制來管理內存,這使得每個程序都擁有自己的地址空間。每當程序使用虛擬地址進行讀寫時,都必須轉換為實際的物理地址,才能真正在內存條上定位數據。如下圖所示:

內存地址的轉換是通過一種叫做頁表(Page Table)的機制來完成的,這是本節要講解的重點,即:
- 頁表是什么?為什么要采用頁表機制,而不采用其他機制?
- 虛擬地址如何通過頁表轉換為物理地址?
直接使用數組轉換
最容易想到的映射方案是使用數組:每個數組元素保存一個物理地址,而把虛擬地址作為數組下標,這樣就能夠很容易地完成映射,並且效率不低。如下圖所示:

但是這樣的數組有 2^32 個元素,每個元素大小為4個字節,總共占用16GB的內存,顯現是不現實的!
使用一級頁表
既然內存是分頁的,只要我們能夠定位到數據所在的頁,以及它在頁內的偏移(也就是距離頁開頭的字節數),就能夠轉換為物理地址。例如,一個 int 類型的值保存在第 12 頁,頁內偏移為 240,那么對應的物理地址就是 2^12 * 12 + 240 = 49392。
2^12 為一個頁的大小,也就是4K。
虛擬地址空間大小為 4GB,總共包含 2^32 / 2^12 = 2^20 = 1K * 1K = 1M = 1048576 個頁面,我們可以定義一個這樣的數組:它包含 2^20 = 1M 個元素,每個元素的值為頁面編號(也就是位於第幾個頁面),長度為4字節,整個數組共占用4MB的內存空間。這樣的數組就稱為頁表(Page Table),它記錄了地址空間中所有頁的編號。
虛擬地址長度為32位,我們不妨進行一下切割,將高20位作為頁表數組的下標,低12位作為頁內偏移。如下圖所示:

為什么要這樣切割呢?因為頁表數組共有 2^20 = 1M 個元素,使用虛擬地址的高20位作為下標,正好能夠訪問數組中的所有元素;並且,一個頁面的大小為 2^12 = 4KB,使用虛擬地址的低12位恰好能夠表示所有偏移。
注意,表示頁面編號只需要 20 位,而頁表數組的每個元素的長度卻為 4 字節,即 32 位,多出 32 - 20 = 12 位。這 12 位也有很大的用處,可以用來表示當前頁的相關屬性,例如是否有讀寫權限、是否已經分配物理內存、是否被換出到硬盤等。
例如一個虛擬地址 0XA010BA01,它的高20位是 0XA010B,所以需要訪問頁表數組的第 0XA010B 個元素,才能找到數據所在的物理頁面。假設頁表數組第 0XA010B 個元素的值為 0X0F70AAA0,它的高20位為 0X0F70A,那么就可以確定數據位於第 0X0F70A 個物理頁面。再來看虛擬地址,它的低12位是 0XA01,所以頁內偏移也是 0XA01。有了頁面索引和頁內偏移,就可以算出物理地址了。經過計算,最終的物理地址為 0X0F70A * 2^12 + 0XA01 = 0X0F70A000 + 0XA01 = 0X0F70AA01。
這種思路所形成的映射關系如下圖所示:

可以發現,有的頁被映射到物理內存,有的被映射到硬盤,不同的映射方式可以由頁表數組元素的低12位來控制。
使用這種方案,不管程序占用多大的內存,都要為頁表數組分配4M的內存空間(頁表數組也必須放在物理內存中),因為虛擬地址空間中的高1G或2G是被系統占用的,必須保證較大的數組下標有效。
現在硬件很便宜了,內存容量大了,很多電腦都配備4G或8G的內存,頁表數組占用4M內存或許不覺得多,但在32位系統剛剛發布的時候,內存還是很緊缺的資源,很多電腦才配備100M甚至幾十兆的內存,4M內存就顯得有點大了,所以還得對上面的方案進行改進,壓縮頁表數組所占用的內存。
使用兩級頁表
上面的頁表共有 2^20 = 2^10 * 2^10 個元素,為了壓縮頁表的存儲空間,可以將上面的頁表分拆成 2^10 = 1K = 1024 個小的頁表,這樣每個頁表只包含 2^10 = 1K = 1024 個元素,占用 2^10 * 4 = 4KB 的內存,也即一個頁面的大小。這 1024 個小的頁表,可以存儲在不同的物理頁,它們之間可以是不連續的。
那么問題來了,既然這些小的頁表分散存儲,位於不同的物理頁,該如何定位它們呢?也就是如何記錄它們的編號(也即在物理內存中位於第幾個頁面)。
1024 個頁表有 1024 個索引,所以不能用一個指針指向它們,必須將這些索引再保存到一個額外的數組中。這個額外的數組有1024個元素,每個元素記錄一個頁表所在物理頁的編號,長度為4個字節,總共占用4KB的內存。我們將這個額外的數組稱為頁目錄(Page Directory),因為它的每一個元素對應一個頁表。
如此,只要使用一個指針來記住頁目錄的地址即可,等到進行地址轉換時,可以根據這個指針找到頁目錄,再根據頁目錄找到頁表,最后找到物理地址,前后共經過3次間接轉換。
那么,如何根據虛擬地址找到頁目錄和頁表中相應的元素呢?我們不妨將虛擬地址分割為三分部,高10位作為頁目錄中元素的下標,中間10位作為頁表中元素的下標,最后12位作為頁內偏移,如下圖所示:

前面我們說過,知道了物理頁的索引和頁內偏移就可以轉換為物理地址了,在這種方案中,頁內偏移可以從虛擬地址的低12位得到,但是物理頁索引卻保存在 1024 個分散的小頁表中,所以就必須先根據頁目錄找到對應的頁表,再根據頁表找到物理頁索引。
例如一個虛擬地址 0011000101 1010001100 111100001010,它的高10位為 0011000101,對應頁目錄中的第 0011000101 個元素,假設該元素的高20位為 0XF012A,也即對應的頁表在物理內存中的編號為 0XF012A,這樣就找到了頁表。虛擬地址中間10位為 1010001100,它對應頁表中的第 1010001100 個元素,假設該元素的高20位為 0X00D20,也即物理頁的索引為 0X00D20。通過計算,最終的物理地址為 0X00D20 * 2^12 + 111100001010 = 0X00D20F0A。
這種思路所形成的映射關系如下圖所示:

圖中的點狀虛線說明了最終的映射關系。圖中沒有考慮映射到硬盤的情況。
采用這樣的兩級頁表的一個明顯優點是,如果程序占用的內存較少,分散的小頁表的個數就會遠遠少於1024個,只會占用很少的一部分存儲空間(遠遠小於4M)。
在極少數的情況下,程序占用的內存非常大,布滿了4G的虛擬地址空間,這樣小頁表的數量可能接近甚至等於1024,再加上頁目錄占用的存儲空間,總共是 4MB+4KB,比上面使用一級頁表的方案僅僅多出4KB的內存。這是可以容忍的,因為很少出現如此極端的情況。
也就是說,使用兩級頁表后,頁表占用的內存空間不固定,它和程序本身占用的內存空間成正比,從整體上來看,會比使用一級頁表占用的內存少得多。
使用多級頁表
對於64位環境,虛擬地址空間達到 256TB,使用二級頁表占用的存儲空間依然不小,所以會更加細化,從而使用三級頁表甚至多級頁表,這樣就會有多個頁目錄,虛擬地址也會被分割成多個部分,思路和上面是一樣的,不再贅述。