linux中的頁表實現

頁表是軟體實現的,但是頁表的查找是MMU完成的,所以硬體定義了頁表的實現規則,軟體可以做的只有選擇頁表的級數

,是否使用huge page以及填充對應的許可權標誌位。前面的文章主要介紹了頁表的實現規則,本文將討論linux系統中頁表的具體實現。

相關數據結構

還是那個三級頁表,但增加了很多內容。

頁表PGD的首地址是放在前面介紹到的mm_struct中的,pgd_offset(), pmd_offset(), pmd_offset()分別用於從虛擬(線性)地址中提取查找PGD, PMD和PTE表所需的index。

linux中用PAGE_SHIFT表示一個PTE所對應的內存範圍(也就是一個page的大小)所需佔用的bit數(也就是12), PMD_SHIFT和PGDIR_SHIFT分別表示一個PMD和一個PGD所對應的內存範圍所需佔用的bit數。

PAGE_SIZE, PMD_SIZE, PGDIR_SIZE則分別表示一個PTE, PMD, PGD所對應的內存範圍的大小。

#define PAGE_SIZE (1UL << PAGE_SHIFT)

PAGE_MASK, PMD_MASK, PGDIR_MASK則是用於提取地址位的掩碼。

#define PAGE_MASK (~(PAGE_SIZE-1))

對於頁表描述符中的標誌位, linux中基本都有對應的函數來處理。比如PTE中的R/W位,linux用_PAGE_WRITE宏表示,pte_write()用於檢測該位,判斷page是否可寫;pte_mkwrite()可將該位置1,設置page為可寫;pte_wrprotect()可將該位清0,設置page為不可寫。

和硬體的配合

不同的處理器支持的頁表級數是不一樣的,比如IA-32只支持2級,x64支持4級甚至5級,而linux作為一個通用的操作系統,需要兼容不同的硬體平台,如果針對每種硬體平台進行不同的頁表實現,那就太麻煩了。試想,如果linux採用2級頁表,對IA-32到是沒問題,對x64就無能為力了,那如果linux統一採用4級或5級頁表,對x64的配合倒是剛剛好,那對IA-32呢?其實不難,只要把表示PUD中PMD entry個數的PTRS_PER_PMD和表示PMD中PTE entry個數的PTRS_PER_PMD都設為1就可以了,相當於把中間兩級的PUD和PMD摺疊了起來,PGD就等同於直接指向PTE了。

進程頁表的建立

內核載入一個進程後,需要為該進程創建屬於它的頁表。pgd_alloc(), pmd_alloc()和pte_alloc()分別用於創建PGD, PMD和PTE。因為頁表本身也是放在內存中的, 其本質上是由若干物理pages組成的,分配物理內存本身就是耗時的,加之在分配過程中需要關中斷,而頁表的創建和銷毀又是很頻繁的操作,而且現在的頁表多達四級甚至五級,所以整個過程的系統開銷是不可小覷的。那怎麼加速這一過程呢?加速最常用的就是cache啦,具體的做法是把要提供給頁表創建的物理pages放到一組叫quicklist的鏈表中,以後銷毀頁表釋放的頁就往這個quicklist里送,創建頁表需要的頁就直接從這個quicklist里找,其實就是一個專用的freelist(原理和slab差不多),當然要比去整個物理內存的free pages list分配要快啦。

如果建立了新的映射關係,則需要調用mk_pte()創建一個新的頁表項,因為頁表描述符由物理頁面號和許可權控制位組成,所以新建頁表項的工作主要就是填充這兩部分內容,組裝完成就可以調用set_pte()插入到對應的頁表中去了。

內核頁表的建立

內核剛被載入的時候,paging還沒有打開,還工作在實模式(關於實模式請參考這篇文章)。x86要求在打開paging之前(置位CR0寄存器的PG位),軟體需要至少設置一個page directory和一個page table,從linux實現的角度,就是至少要有一個PGD和一個PTE。試想,打開paging後,操作系統給出的地址對於CPU來說就是虛擬地址了,這時如果還沒有任何的頁表,怎麼去獲得物理地址,進而操作物理內存呢?為此,linux採用的做法是:假設內核初始化時需要用到的物理內存不超過8MB(0x00000000到0x007fffff),若一個page為4KB,則這8MB內存共含有2048個pages,需要2048個PTE來映射。一個PTE佔4個位元組,因此一共需要8KB的page(也就是2個page)來存儲,這2個用作PTE頁表的page用pg0和pg1表示,分別放在物理地址為0x00102000和0x00103000的位置。

而PGD則用一個叫swapper_pg_dir的變數表示,放在物理地址為0x00101000的位置,大小為1個page。因為這個臨時頁表的目的就是要讓這8MB物理內存在實模式和打開paging的保護模式下都可以被訪問,因此需要建立2個實模式下PGD entries的和2個用於保護模式的PGD entries。實模式下本來是沒有什麼虛擬地址的概念的,但也可以理解為是一種虛擬地址等於物理地址的特殊映射。頁表是使用虛擬地址作為index的,在32位兩級頁表中,高10位是PGD表的index,對於0x00000000到0x007fffff的地址,index就是0和1。對於內核空間來說,0x00000000到0x007fffff的物理地址在保護模式下對應的虛擬地址為0xc0000000到0xc07fffff,高10位是1100000000b和1100000001b,換算成10進位就是768和769。

這裡每個PGD entry和PTE的最後4個bit都是0111b(7),結合頁表描述符的介紹可以知道,表示的是這8MB的內存是writable的,user和kernel都可以訪問的,present的。

實模式下的工作完成之後,就可以將swapper_pg_dir的物理地址放入CR3,

movl $swapper_pg_dir-0xc0000000,%eax
movl %eax,%cr3 /* set the page table pointer.. */

置PG位為1,進入paging保護模式了

movl %cr0,%eax
orl $0x80000000,%eax
movl %eax,%cr0 /* set paging (PG) bit */

這時接力棒就叫到了paging_init()的手中,由它來完成正式的內核頁表(被稱為master kernel page table)的創建了。master kernel page table中PGD的首地址還是存在swapper_pg_dir變數中的。

參考:

??? ??? API - ARM32

parrotshen.blogspot.com

kernel.org/doc/html/lat


推薦閱讀:

理解 Linux 中的 /etc/services 文件
Linux常用命令--系統狀態篇
如何評價龍芯三號開源電腦主機?
Exim 的配置
Linuxer精華文章匯總

TAG:Linux | Linux內核 | 內存管理 |