《深入理解Linux核心》-2.4. 硬體分頁
分頁單元用來把線性地址轉換成實體地址。它的一個主要的任務就是根據線性地址的訪問許可權檢查請求的訪問型別。如果訪問的記憶體不合法,它會產生一個頁面錯誤異常(參考第4章和第8章)。
出於效能考慮,線性地址被分成固定大小的組,叫做頁面;一個頁面中連續的線性地址被對映到連續的實體地址上。如此,核心可以對一個頁面進行許可權控制,而不需要針對其中的每個線性地址。一般情況下,我們說頁面時,就是指一個線性地址集合和這些地址包含的資料。
分頁單元把所有RAM分成固定長度的頁幀(物理頁)。每個頁幀包含一個頁面,也就是頁幀的大小和頁面大小一樣。頁幀是主記憶體的組成部分,因此也是一塊儲存區域。區分頁幀和頁面很重要,頁面是一塊資料,它被儲存在任意一個頁幀上或者磁碟上。
把線性地址對映到實體地址的資料結構叫做頁表;它儲存在主存中,並被核心初始化(啟用分頁單元的情況下)。
從80386開始,所有80x86處理器支援分頁;通過設定cr0暫存器的PG標誌位可以啟用它。當PG=0時,線性地址等於實體地址。
2.4.1. 常規分頁
從80386開始,頁面的大小為4KB。
32位線性地址被分成三部分:
目錄
最高10位有效位
頁表
中間10位
偏移
最低12位有效位
線性地址的轉換分為兩步,每一步基於一種轉換表。第一個轉換表叫做頁目錄,第二個叫做頁表。
採用這種兩級結構的目的是減少每個CPU的頁表使用的RAM記憶體。如果只有一級頁表,那麼每個CPU的頁表將需要2^20個節點(比如,線性地址空間大小是4KB,每個頁表節點4位元組,則頁表總共佔用4MB記憶體),即使程序並沒有使用那麼多的記憶體。兩級結構的策略是隻為程序實際使用的記憶體區域建立頁表,以此減少記憶體佔用。
每個活動的程序都有一個頁目錄。這些頁目錄不是一開始就全部分配,而是在程序實際需要的時候才被分配。
當前頁目錄的實體地址儲存在c3暫存器中。線性地址中的目錄部分指向頁目錄節點,這個節點用來定位頁表。找到頁表後,再使用線性地址中的表字段來定位頁表節點,這個節點包含相應頁幀的實體地址。偏移欄位則決定了資料在頁幀中的實際位置(看圖2-7)。因為偏移長度是12位,所以每個頁面包含4096位元組資料。
圖2-7. 80x86處理器的分頁
由於目錄和表字段的長度都是10位元,因此它們可以容納1024個節點,進而,一個頁目錄可以定位的地址數目為1024 * 1024 * 4096 = 2^32個記憶體元,這正是32位地址所能表示的。
也目錄和頁表具有相同的結構。每個節點包含以下欄位:
Present 標誌
當為1時,頁面在主存中;為0時,表示頁面不在主存中,該節點剩下的位元位被作業系統用作其他目的。當一個Present為0的頁表或者頁目錄需要做地址轉換時,分頁單元把線性地址儲存在暫存器cr2中,併產生一個異常14:頁面錯誤異常。(參考17章關於Linux如何使用這個欄位)
包含頁幀實體地址高20有效位的欄位
因為頁幀大小為4KB,它的實體地址必須是4096的倍數,因此它的低12有效位總是0。如果這個欄位在頁目錄中,則對應的頁幀包含一個頁表;如果是頁表中的欄位,對應的頁幀包含一個頁面的資料。
Accessed 標誌
每當分頁單元定址到對應的頁幀時,它被設定為1。這個標誌位在作業系統換出頁面的時候會用到。分頁單元從不清除它,它必須由作業系統來清除。
Dirty 標誌
僅頁表節點會用到。頁幀上有寫操作時,它被設定為1。跟Accessed標誌一樣,Dirty會被作業系統在換出頁面的時候用到。分頁單元從不清除它,它必須由作業系統來清除。
Read/Write 標誌
包含頁面或頁表的訪問許可權(讀寫或者只讀)(參考本節之後的“硬體保護結構”一段)。
User/Supervisor 標誌
包含需要訪問頁或者頁表的特權級別(參考本節之後的“硬體保護結構”一段)。
PCD和PWT 標誌
控制硬體快取處理頁面或者頁表的方式(參考本節之後的“硬體快取”一段)。
Page size 標誌
僅對頁目錄有效。當其為1時,該節點指向一個2MB或者4MB大小的頁幀(參考後面段落)。
Global 標誌
僅對頁表有效。Pentium Pro引入它來防止頻繁使用的頁面被刷出TLB快取(參考本章之後的“轉換查詢快取(TLB)”一段)。
2.4.2. 擴充套件分頁
從奔騰處理器開始,80x86微處理器引入擴充套件分頁,它允許頁幀的大小為4MB,而不是4KB(看圖2-8)。擴充套件分頁用來把大的連續線性地址轉換成對應的實體地址;這時,核心不需要中間頁表,從而節省了記憶體和TLB節點(參考“塊表(TLB)”一段)。
圖2-8. 擴充套件分頁
擴充套件分頁的啟用是通過設定頁目錄節點的Page Size標誌位來的,此時,分頁單元把32位線性地址化分成兩部分:
目錄
高10位有效位
偏移
剩下的22位
擴充套件分頁的頁目錄節點和普通分頁的一樣,除了以下兩點:
- Page Size標誌必須被設定
- 20位實體地址只有高10位是有效的。這是因為每個實體地址按4MB對齊,所以22個最低有效位總是0
擴充套件分頁和常規分頁共存,通過設定cr4暫存器的PSE標誌可以啟用擴充套件分頁。
2.4.3. 硬體保護體系
分頁單元採用一種和分段單元不同的保護體系。80x86處理器中,分段需要四個特權等級,而分頁單元只需要兩個,它們由之前提到的User/Supervisor標誌位控制。當它為0時,頁面僅可被核心態程序訪問,為1時,頁面總是可以被訪問。
除此之外,分段系統使用三種訪問許可權(讀、寫、執行),而分頁系統只使用兩種訪問許可權(讀和寫)。Read/Write標誌位0時,頁面只讀,否則可以讀寫。
2.4.4. 常規分頁的一個例子
一個簡單的例子有利於我們更好的理解常規分頁是怎麼工作的,假設核心給程序分配0x20000000和0x2003ffff之間的地址空間,包含64個頁面。我們不關心頁面對應頁幀的實體地址;實際上,它們甚至不一定在主存中,這裡,我們只關心頁表節點的其他欄位。
我們首先看線性地址的10個最高有效位,它的值為0x80(128),因此,它指向頁目錄的第129個節點,這個節點裡麵包含頁表的實體地址(參考圖2-9)。如果程序沒有分配其他線性地址,頁目錄剩下的節點全部為0。
圖2-9. 分頁示例
我們假定的地址的中間10位(線性地址中的表字段)的範圍從0到0x03f,所以只有前64個頁表節點是有效的,剩下的以0填充。
假設我們現在需要讀取線性地址0x20021406處的內容,分頁單元的處理邏輯如下:
1、目錄欄位0x80用來選擇頁目錄的第0x80號節點,它指向相應的頁表;
2、表字段0x21用來選擇頁表的第0x21號節點,它指向包含目標頁的頁幀;
3、最後,偏移欄位0x406用來選擇頁幀中0x406處的位元組。
如果頁表0x21處節點的Present標誌被清除,說明對應頁面不在主存中,此時頁面單元發出一個頁面錯誤異常。當程序嘗試訪問0x20000000和0x2003ffff範圍之外的地址時,也會導致同樣的異常,因為頁表中的其他節點都是0,尤其是它們的Present標誌位為0。
2.4.5. 實體地址擴充套件(PAE)分頁機制
一個處理器可以支援的最大RAM受連線到地址匯流排的針腳數限制。老的Intel處理器,從80386到奔騰都使用32位實體地址。理論上這些系統可以使用最多4GB RAM;實際上,由於使用者態線性地址空間的要求,核心不能直接定位超過1GB的RAM,下一節“Linux分頁”我們會看到為什麼。
然而,那些同時執行成千上萬個程序的大伺服器需要比4GB更多的記憶體,這迫使Intel擴充套件32位80x86架構能支援的記憶體數。
Intel通過把處理器地址針腳從32增加到36來滿足這些需求。從高能奔騰(Pentium Pro)開始,所有Intel處理器能夠最大定址2^36=64GB記憶體。這時候,需要一種新的分頁機制來吧32位的線性地址轉換成36位實體地址。
對於高能奔騰處理器,Intel引入一種叫做實體地址擴充套件(PAE)的機制。另一種機制是,頁面大小擴充套件(PSE-36),在奔騰三處理器上被採用,但是Linux沒有使用它,我們也不打算在本書介紹。
PAE通過設定cr4暫存器中的PAE標誌來啟用。頁目錄的Page Size標誌支援更大的頁大小(當PAE啟用時,是2MB)。
Intel為了支援PAE,改變了分頁機制:
- 64GB記憶體被分成2^24個頁幀,頁幀大小保持不變,頁表中儲存的實體地址從20位擴充套件到24位。因為PAE頁表必須包含12個標誌位(偏移)和24個實體地址位,所以頁表節點必須從32位擴充套件到64位。這個相應的導致了一個4KB的PAE頁表(一個頁幀中的頁表)只能包含512個節點而不是1024個節點。
- 增加了新的一級頁表,叫做頁目錄指標表(PDPT),每個節點64位。(譯者注:頁目錄本來是4KB,一個頁幀就能儲存,不需要查詢頁目錄,現在增加到四個頁目錄,需要通過PDPT的方式來定位頁目錄)
- cr3暫存器包含27位的頁目錄指標表基地址。因為PDPT儲存在RAM的前4GB記憶體中,並且按32位元組對齊,27位足夠表示PDPT的基地址。
當對映一個4KB頁面的線性地址時,32位線性地址按如下方式翻譯:
cr3:指向PDPT
31, 30位:指向PDPT中四個可能的節點
29-21位:指向頁目錄中512箇中的一個節點
20-12位: 指向頁表中512箇中的一個節點
11-0位:4KB頁面偏移
對映2MB頁面時,32位線性地址翻譯方式如下:
cr3:指向PDPT
31, 30位:指向PDPT中四個可能的節點
29-21位:指向頁目錄中512箇中的一個節點
20-0位:2MB頁面偏移
總而言之,一旦cr3被設定,它可以定址4GB記憶體。當我們想要尋得更多地址時,我們必須更改cr3或者PDPT的值。然而,PAE的主要問題是線性地址仍然是32位,核心程式設計師必須重用相同的線性地址來對映不同的記憶體。我們會在之後的章節概述Linux在PAE啟用時是怎樣初始化頁表的,“記憶體大於4GB時的最終核心頁表”。顯然,PAE沒有增大單個程序可用的記憶體空間,因為它只處理實體地址。此外,只有核心可以修改程序的頁表,所以使用者態程序不能使用超過4GB的記憶體。另一方面,PAE卻允許核心使用最大64GB的記憶體,從而有效增加了系統可以執行的程序數。
2.4.6. 64位架構上的分頁
通過前文的學習,我們知道32位處理器使用兩級頁表,但在64位架構上卻不適合。我們從理論上來解釋為什麼:
假設頁表大小為4KB,所有線性地址的偏移欄位必須為12位,剩下52位給頁表和頁目錄。如果我們只使用64位中的48位(這個限制剛好給我們很舒服的256TB記憶體)來定址,剩下的48-12=36位用作頁表和頁目錄欄位。如果各分一半,18位,那麼每個程序的頁表和頁目錄都擁有2^18個節點,超過256000個節點(總共4MB)。
為了節省記憶體,所有64位分頁系統使用更多的分頁層級。層級的數量取決於處理器型別。表2-4總結了一些Linux支援的64位平臺分頁層次數。
表2-4. 一些64位系統分頁層級
Linux系統提供一種通用的分頁模型,可以支援大部分硬體系統,我們在下一節“Linux分頁”會講到。
2.4.7. 硬體快取
現代微處理器的時鐘頻率可以達到幾個GB,而RAM(DRAM)晶片的訪問時間在幾百個時鐘週期左右。這意味著當執行需要訪問RAM的指令時,CPU會等待。
因此引入硬體快取來減少CPU和RAM的速度不匹配問題。這種方法基於一種“區域性原則”,對程式和資料均有效。“區域性原則”認為,由於程式的迴圈結構和相關的資料往往被打包儲存到線性陣列,跟最近使用的記憶體臨近的地址有極大可能很快被使用。因此,引入一個更小更快的記憶體來存放最近使用的程式碼和資料是有意義的。80x86為此增加了一個新的硬體單元叫做行(line)。它由一些批連續的位元組組成,這些資料在較慢的DRAM和較快的用來實現快取的SRAM之間傳輸。
這個快取被分成幾個行子集。極端情況下,一個子集一行,快取可以做到直接對映,也就是記憶體中的一行總是儲存在快取中相同位置。另一個極端情況,快取是全相聯的,即記憶體中的任何行可以儲存到快取中的任意位置。但是,大部分的快取使用N路相聯,記憶體中的任意行可以被儲存在快取中特定N行的任意位置。
如圖2-10,快取單元被插在分頁單元和主存之間。它包含一個硬體快取儲存和一個快取控制器。快取儲存儲存實際的資料,快取控制器儲存一些節點,每個節點對應快取儲存中的一行。每個節點包含一個tag和一些描述快取狀態的標誌位。tag允許快取控制器識別對映到快取中的記憶體位置。實體地址被分成三部分:高位表示tag,中間對應快取控制器子索引,低位表示偏移。
圖2-10. 處理器硬體快取
當訪問一個RAM記憶體單元時,CPU提取實體地址的子索引欄位,並拿高位tag和這個子集中的所有行的tag作比較,匹配到則快取命中,否則快取未命中。
快取命中時,快取控制器對不同的訪問型別有不同的流程。對於讀操作,控制器直接從快取中取出資料並傳輸給CPU暫存器,不用訪問RAM,因而節省了時間。對於寫操作,控制器有兩種策略:直寫和回寫。直寫就是同時寫RAM和快取,這個過程中禁止其他對快取的寫操作。寫回策略效率更高,只有快取被更新,控制器會在適當的時機把快取中的內容更新到RAM中,比如CPU接到一個重新整理快取指令或者發生了一個硬體重新整理訊號(通常是由快取未命中導致)。
當快取未命中時,快取被直接寫到記憶體,必要的情況下,會把RAM中的資料載入到快取中。
多處理器系統中每個處理器都有一個獨立的硬體快取,因而需要額外的硬體裝置保證快取間的資料同步。如圖2-11,每個CPU有自己的區域性硬體快取,但更新操作變得更加耗時:當一個CPU修改自己快取中的資料是,必須要檢查相應的資料是否存在其他CPU的快取中,如果是,則要通知對應的CPU來更新自己快取中的值。這一般被叫做快取窺探。幸運的是,所有這些操作都由硬體來完成,跟核心無關。
圖2-11. 雙處理器中的快取
快取技術更新很快。比如,第一代奔騰處理器只有一個快取晶片叫做一級快取(L1-cache)。更新一些的CPU添加了更大但稍慢的快取,叫做二級快取、三級快取等等。不同層級的快取之間的一致性由硬體保證,Linux忽略這些細節,並且假定只有一個快取。
cr0暫存器的CD標誌位用來啟用和禁用快取,NW標誌指定快取使用直寫或者寫回策略。
另一個奔騰處理器有趣的功能是它允許作業系統為每個頁幀關聯一個不同的快取管理策略。這通常由PCD和PWT標誌位來完成,但是Linux系統清除了這兩個標誌位,因此,所有頁面都啟用快取,並且使用回寫策略。
2.4.8. 快表技術(TLB)
80x86使用TLB來加速線性地址轉換。當線性地址第一次被訪問時,它對應的實體地址是通過記憶體中的頁表來獲取的,然後這個地址被儲存在TLB中,因而之後訪問相同的地址就不需要再訪問記憶體了,可以從TLB中快速獲得。
多處理器系統中,每個CPU一個快表。跟硬體快取不同的是,快表之間不需要同步,因為不同的程序的線性地址空間時獨立的。
當一個CPU的cr3暫存器被修改時,硬體自動清除本地快表的所有節點,因為一個新的頁表被使用,而快表還指向老的資料。