1. 程式人生 > >《深入理解Linux核心》-2.5. Linux分頁

《深入理解Linux核心》-2.5. Linux分頁

Linux採用一種可以相容32位和64位系統的分頁模型。正如前一章所說,32位系統上使用兩級分頁是足夠的,但是64位系統上需要更多層級的分頁。Linux直到2.6.10,使用三級分頁,2.6.11之後使用四級分頁。圖2-12闡述了這四種頁表,它們分別是:

  • 全域性頁目錄
  • 頂層頁目錄
  • 中層頁目錄
  • 頁表

全域性頁目錄包含若干頂層頁目錄的地址,頂層頁目錄又包含若干中層頁目錄的地址,中層頁目錄又包含一些頁表的地址。每個頁表節點指向一個頁幀。因此,線性地址可以分成五個部分。圖2-12沒有展示它們的位數,因為它們的長度取決於計算機架構。

對於沒有啟用PAE的32位系統,兩級分頁就足夠了。Linux實際上會忽略頂層頁目錄和中層頁目錄。但是為了同時相容32位和64位系統,頂層頁目錄和中層頁目錄的位置依然儲存著,Linux定義它們的節點數為1,並使其對映到全域性頁目錄的某個節點。

圖2-12. Linux分頁模型

這裡寫圖片描述

對於開啟了擴充套件分頁的32位系統,使用三級分頁:Linux的全域性頁目錄對應80x86的頁目錄指標表,頂層頁目錄被移除,中層頁目錄對應80x86的頁目錄,頁表對應80x86的頁表。

64位系統使用三級或四級分頁。

Linux程序處理嚴重依賴於分頁。實際上,線性地址到實體地址的自動轉換是的以下設計目標成為可行:

  • 位每個程序分配不同的實體地址空間,保證對定址錯誤形成有效的保護。
  • 區別頁面和頁幀。當一個頁面被儲存到頁幀中,然後儲存到磁碟上,當其被再次取出時,可以被加載出到不同的頁幀中。這個是虛擬記憶體管理的基本要素(參考第17章)。

本章剩下部分,我們將會具體的講述80x86處理器的分頁原理。

正如第9章所提,每個程序有自己的全域性頁目錄和頁表,但程序切換時,Linux把cr3暫存器儲存在程序描述符中,然後從下個程序的描述符中載入新的值到cr3中,這樣,但新程序恢復執行時,分頁單元能指向正確的頁表。

線性地址對映到實體地址已經變成了一個機械性的任務,雖然它仍然有些複雜。本章接下來的幾段列出了一些非常枯燥的函式和巨集,核心用它們來定址和管理頁表。你也許現在就想跳過這些函式,但瞭解它們的作用和原理非常有必要,因為我們在整本書的討論中經常會遇見它們。

2.5.1. 線性地址欄位

以下巨集大大簡化了對頁表的處理:

  • PAGE_SHIFT

    定義了偏移欄位的位數。對於80x86處理器,它的值為12。2的12次方等於4KB,正好是一個頁面的大小。PAGE_SIZE

    巨集使用它來返回頁面大小,PAGE_MASK巨集產生值0xfffff000,用來計算出偏移欄位。

  • PMD_SHIFT

    偏移和表字段的總位數;它等於一箇中層頁目錄節點可以對映的區域大小的對數。PMD_MASK巨集用來取出偏移和表字段的值。

  • PUD_SHIFT

    偏移+表+中層頁目錄的總位數;也就是一個頂層目錄節點可以對映的區域大小的對數。PUD_SIZE巨集用來計算一個全域性頁目錄節點可以對映的空間大小。PUD_MASK巨集用來取出偏移、表、中層頁目錄的所有位。

    80x86處理器上,PUD_SHIFT總是等於PMD_SHIFTPUD_SIZE,等於4MB或者2MB。

  • PGDIR_SHIFT

    偏移+表+中層目錄+頂層目錄的總位數;也就是一個全域性頁目錄節點可以對映的空間大小的對數。PGDIR_SIZE等於一個頂層目錄節點可以對映的空間大小。PGDIR_MASK巨集用來取出偏移、表、中層頁目錄、頂層頁目錄的所有位。

  • PTRS_PER_PTE, PTRS_PER_PMD, PTRS_PER_PUD, and PTRS_PER_PGD

    分別計算頁表、中層頁目錄、頂層頁目錄、全域性頁目錄的節點數。80x86上當關閉PAE時,它們分別等於1024,1,1,1024,啟用PAE時則為512、512、1、4。

2.5.2. 頁表處理

pte_t, pmd_t, pud_t, pgd_t分別描述了頁表、中層頁目錄、頂層頁目錄、全域性頁目錄的欄位。
在PAE啟用時,它們是64位的資料型別,否則是32位。

五個型別轉換巨集:__pte, __pmd, __pud, __pgd__pgprot,它們把一個無符號整數轉換成對應的型別。另外五個巨集:ptr_val, pmd_val, pud_val, pgd_val, 和pgprot_val,它們執行相反的轉換,把對應的資料型別轉換成無符號整數。

核心還提供了幾個巨集和函式來讀取和修改頁表節點:

  • pte_none, pmd_none, pud_none, 和pdg_none,當對應的節點為0時值為1,否則為0。
  • pte_clear, pmd_clear, pud_clear, 和pgd_clear,清除對應的頁表節點,以此來禁止程序使用該節點對應的線性地址。ptep_get_and_clear()清除頁表節點並返回之前的值。
  • set_pte, set_pmd, set_pud, 和set_pgd設定頁表節點的值;set_pte_atomic等價於set_pte,但是當啟用PAE時,它還保證64位值被原子寫入。
  • pte_same(a, b),當兩個頁表節點a,b指向相同頁面且具有相同的訪問許可權時返回1,否則返回0。
  • pmd_large(e),如果中層頁目錄節點e指向大頁面(2MB或4MB),則返回1,否則返回0。

pmd_bad巨集用來檢測中層頁表節點,返回1表示指向一個非法的頁表,一般有以下幾種情況:

  • 對應的頁面不在記憶體中(Present標誌被清除)
  • 頁面只讀(Read/Write標誌被清除)
  • Accessed或者Dirty標誌被清除(Linux為每個存在的頁表強制地設定為1)。

pud_badpgd_bad巨集總是返回0。沒有定義pte_bad巨集,因為對於頁表節點來說,無論它指向的頁面不在記憶體中、不可寫或者不可訪問,都是合法的。

pte_present巨集在頁表節點的Present標誌或者Page Size標誌等於1時返回1,否則返回0。前一章有提到,Page Size標誌對於頁表節點沒有意義,但是核心會在當頁面在記憶體中但是不具有讀寫和執行許可權時設定Present為0,Page Size為1,如此,任何對於這類頁面的訪問都會產生一個頁面錯誤異常(Present為0),而且核心可以通過Page Size的值判斷該異常不是由於缺頁引起的。

pmd_present巨集在Present標誌為1時返回1,也就是對應的頁面或者頁表已被載入至記憶體中。pud_presentpgd_present巨集總是返回1。

表2-5中列出的函式用來讀取頁表節點中的標誌位。除了pte_file(),這些函式能正常工作的前提是pte_present返回1。

表2-5. 頁表讀取函式

這裡寫圖片描述

表2-6中列出的函式用來設定頁表節點中的標誌位。

表2-6. 頁面標誌設定函式

這裡寫圖片描述

表2-7列出的巨集用來把頁面地址和一組保護標誌組合到頁表節點中,或者相反的從頁表節點中提取處頁面地址。注意,其中一些巨集通過“頁描述符”的線性地址來引用一個頁面(參考第8章“頁描述符”),而不是直接通過頁面的線性地址。

表2-7. 頁表節點操作巨集

這裡寫圖片描述

表中最後幾個函式用來簡化頁表節點的建立和刪除。

當使用兩級頁表時,中間頁目錄的建立和刪除有點麻煩。正如之前所說,中間頁目錄包含單個指向子頁表的節點,而中間頁目錄又是全域性頁目錄的節點中的節點。對於頁表來說,建立節點更加複雜,因為這個節點指向的頁表可能並不存在,這時候就需要分配一個新的頁幀,填充0,並新增節點。

當開啟PAE時,核心使用三級頁表。當核心建立一個新的全域性頁目錄時,它還建立四個對應的中間頁目錄,它們僅在全域性頁目錄釋放的時候才被釋放。

對於兩級或者三級頁表,頂層頁目錄節點總是對應全域性頁目錄的單個節點。

80x86架構通常有以下函式:

表2-8. 頁分配函式

這裡寫圖片描述

2.5.3. 實體記憶體佈局

核心在初始化階段會建立一個實體地址對映表,它定義了那些實體地址空間可以被核心使用,那些不能(這些記憶體用於硬體裝置I/O共享記憶體或者因為相應的頁幀包含BIOS資料)。

核心預留以下頁幀:

  • 落在不可用的實體地址區間中的地址
  • 包含核心程式碼和初始化資料結構的頁面

預留頁幀中的頁面不能被動態分配或者換出到磁碟。

一般情況下,核心被安裝在RAM中以0x00100000為起始地址的區域,即從第二個兆位元組開始。總共需要的也幀數取決於核心配置,典型的配置使小於3MB。

為什麼核心不從RAM第一個兆位元組載入?因為PC架構必須要考慮幾種特殊情況,比如:

  • 第0個頁幀被BIOS用來儲存硬體配置資訊,這些資訊在啟動自測階段(Power-On Self-Test)被檢測;很多膝上型電腦甚至在系統初始化之後還想這些頁幀寫入資料。
  • 0x000a0000到0x000fffff之間的實體地址預留用作BIOS例程,和對映記憶體到ISA顯示卡。
  • 第一兆B記憶體的一些額外頁幀被用在特殊型號的電腦。比如IBM ThinkPad把0xa頁幀對映到0xa9。

系統啟動的早期階段(參考附錄A),核心通過查詢BIOS來知道實體記憶體的大小。最新的計算機中,核心還呼叫一個BIOS例程來建立一串實體地址和它們對應的記憶體型別。

然後,核心通過執行machine_specific_memory_setup()函式來建立實體地址對映(參考表2-9)。當然,核心建立這張表的依據是上面說的那個BIOS列表,如果BIOS列表不可用,核心用預設的規則來建立這張表;0x9f(LOWMEMSIZE())到0x100(HIGH_MEMORY)之間的頁幀都被預留。

表2-9. BIOS實體地址對映表舉例

這裡寫圖片描述

表2-9展示了一個典型的128MB記憶體計算機的記憶體配置。0x07ff0000到0x07ff2fff之間的地址用來儲存系統硬體資訊,這些硬體資訊在POST階段被BIOS寫入;在系統初始化階段,核心把這些資訊拷貝到相應的核心資料結構,之後標記這些頁幀為可用狀態。而0x07ff3000到0x07ffffff之間的地址則被對映到ROM晶片上。0xffff0000開始的實體地址被預留用作BIOS的ROM晶片的地址對映(參考附錄A)。BIOS有時候不會為某些實體地址提供資訊(如表中,0x000a0000到0x000effff之間的地址)。為了安全起見,Linux認為這些地址是不可用的。

BIOS公佈的地址對核心並不是全部可見:比如,當開啟PAE時,核心也只能定址4GB空間,即使實際上有更多的實體記憶體可以使用。setup_memory()函式在machine_specific_memory_setup()之後被呼叫:它通過分析實體地址表來初始化一些變數用來描述核心實體記憶體佈局。這些變數見表2-10:

表2-10. 核心描述實體記憶體佈局的變數

這裡寫圖片描述

為了是核心被載入到連續的頁幀,Linux跳過RAM的第一個1MB空間。顯然,那些未被核心預留的頁幀將被用作儲存動態分配的頁面。

圖2-13展示了Linux的前3MB記憶體分佈。在此,我們假設核心需要的記憶體小於3MB。

_text對應實體地址0x00100000,表示核心程式碼的第一個位元組。核心程式碼的最後一個位元組用與之相似的符號_etext表示核心資料分成兩部分:已初始化和為初始化的。初始化的資料從_etext開始到_edata結束。接著是未初始化資料,以_end結束。

圖片中出現的這些符號並沒有在Linux原始碼中定義,它們在核心編譯階段生成。

圖2-13. Linux2.6的前768個頁幀(3MB)

這裡寫圖片描述

2.5.4. 程序頁表

程序的線性地址空間分成兩部分:

  • 從0x00000000到0xbfffffff的線性地址可在核心態和使用者態訪問
  • 從0xc0000000到0xffffffff的線性地址只能在核心態訪問

程序允許在使用者態時,它提交的地址小於0xc0000000;當它執行在核心態時,它執行核心程式碼並且提交的地址大於等於0xc0000000。但是在某些情況下,核心必須訪問使用者態地址來獲取或者儲存資料。

PAE_OFFSET巨集產生值0xc0000000;這是程序的核心空間的起始地址。本書中,我們經常直接使用0xc0000000來代替PAE_OFFSET

全域性頁表的第一個節點對映到小於0xc0000000的線性地址(禁用PAE時對應前768個節點,啟用PAE時對應前三個節點),它的內容由程序自己決定。而剩下的節點對所有程序來說都是相同的,並且等於主核心全域性頁目錄的相應節點(參看下面一段)。

2.5.5. 核心頁表

核心維護一套頁表供自己使用,它們的根叫做主全域性頁目錄。系統初始化之後,這些頁表從不被任何程序或者核心執行緒使用;但是,它的最高節點被系統中的任意普通程序用來引用全域性頁目錄。

我們會在第8章(非連續記憶體空間的線性地址)介紹核心是如何在改變主核心全域性頁目錄的時候同步到程序的全域性頁目錄的。

我們這裡只講述核心是如何初始化它自己的頁表的。這分為兩個階段。事實上,在核心映象剛好載入到記憶體後,CPU仍然執行在實時狀態,此時分頁還未啟用。

第一階段,核心建立一個有限的地址空間,它包含核心程式碼和資料段、初始頁表、和一些128KB的動態資料結構。這個最小的地址空間剛好夠安裝核心和它的核心資料結構。

第二階段,核心利用所有已存的RAM並建立頁表。