date: 2014-09-10 19:09linux
備註:本文中引用的內核代碼的版本是2.4.0。數組
在前面的文章中,咱們介紹了linux頁式內存管理,講到了頁面目錄PGD、中間目錄PMD以及頁表PT,本文來看下內核中對應的結構體定義。函數
PGD、PMD以及PT分別是由pgd_t(頁面目錄項)、pmd_t(中檢目錄項)以及pte_t(頁表項)構成的數組,這些表項(雖然只有32位)被定義成結構體,定義在<asm/page.h>中:this
/* * These are used to make use of C type-checking.. */ #if CONFIG_X86_PAE typedef struct { unsigned long pte_low, pte_high; } pte_t; typedef struct { unsigned long long pmd; } pmd_t; typedef struct { unsigned long long pgd; } pgd_t; #define pte_val(x) ((x).pte_low | ((unsigned long long)(x).pte_high << 32)) #else typedef struct { unsigned long pte_low; } pte_t; typedef struct { unsigned long pmd; } pmd_t; typedef struct { unsigned long pgd; } pgd_t; #define pte_val(x) ((x).pte_low) #endif #define PTE_MASK PAGE_MASK typedef struct { unsigned long pgprot; } pgprot_t; #define pmd_val(x) ((x).pmd) #define pgd_val(x) ((x).pgd) #define pgprot_val(x) ((x).pgprot) #define __pte(x) ((pte_t) { (x) } ) #define __pmd(x) ((pmd_t) { (x) } ) #define __pgd(x) ((pgd_t) { (x) } ) #define __pgprot(x) ((pgprot_t) { (x) } )
可見,當採用32位地址時,pgd_t、pmd_t和pte_t就是無符號整形數。爲何要定義成結構體呢?一方面是爲了方便後續的擴展;另外一方面就像面向對象中的封裝同樣,這裏也是一種封裝,並定義了相關的「訪問器」函數。以pte_t爲例,經過pte_val宏來訪問結構體中成員,另外經過set_pte來設置結構體。宏set_pte定義在<asm/pgtable-2level.h>中:設計
/* * Certain architectures need to do special things when PTEs * within a page table are directly modified. Thus, the following * hook is made available. */ #define set_pte(pteptr, pteval) (_(pteptr) = pteval) /* * (pmds are folded into pgds so this doesnt get actually called, * but the define is needed for a generic inline function.) */ #define set_pmd(pmdptr, pmdval) (_(pmdptr) = pmdval) #define set_pgd(pgdptr, pgdval) (_(pgdptr) = pgdval)
前面咱們講過,物理頁面是以4K爲邊界對齊的,意味着每一個物理頁面的起始地址(固然是物理地址)的低12都爲0,只有高20位是有效的。內核中有一個物理頁面Page的數組mem_map,每一個物理頁面對應mem_map數組中的一個元素,而數組的下標就是物理頁面的序號。物理頁面在數組mem_map中按起始地址順序存放,所以咱們能夠根據頁面序號獲得頁面的起始地址,很簡單,將頁面序號乘以4K(即左移12位)就能夠獲得頁面的起始地址。從這個意義上來說,物理頁面起始地址的高20位能夠看作是頁面序號。3d
物理頁面起始地址只有高20位是有效的,那麼做爲指向物理頁面起始地址的頁表項pte_t,做爲指針只須要它的高20位,因此pte_t中的低12位就挪做他用,用來表示頁面的狀態信息和訪問權限。但在頁表項pte_t結構的定義中,並無以位域的方式體現出來,內核爲此單獨定義了一個用來表示頁面保護的結構pgprot_t,它的定義也在上面的代碼中,而且內核也位置定義了「訪問器」函數。指針
雖然pgprot_t結構被獨立出來了,但一個頁面對應的頁面保護信息仍然保存在頁表項pte_t的低12位中,這裏只是爲了程序設計的方便單獨爲頁面保護信息抽象出一個結構體。咱們能夠根據物理頁面的起始地址以及頁面保護結構pgprot_t拼湊出一個頁表項pte_t,內核中__mk_pte宏就是用來幹這件事的,宏定義在<asm/pgtable-2level.h>中:code
#define __mk_pte(page\_nr,pgprot) \__pte(((page_nr) << PAGE_SHIFT) | pgprot_val(pgprot))
將物理頁面的序號左移12位獲得頁面起始地址的高20位,而後位或上低12位的頁面保護結構就可獲得物理頁面對應的頁表項pte_t了;那麼反過來,從pte_t獲得對應的物理頁面的Page結構也是瓜熟蒂落的了,在同一文件中定義了pte_page宏:對象
#define pte_page(x) (mem_map+((unsigned long)(((x).pte_low >> PAGE_SHIFT))))
數組mem_map的起始地址加上對應的下標,即獲得對應元素的地址了。blog
pgprot_t結構被定義成了一個無符號整形數,但有效的只有其低12位,與pte_t中的低12位對應,其中9位是標誌位,表示頁面的當前狀態和訪問權限(具體含義參考《x86頁式內存管理》),這些標誌位在<asm/pgtable.h>中定義以下:
#define _PAGE_BIT\_PRESENT 0 #define \_PAGE_BIT_RW 1 #define _PAGE_BIT\_USER 2 #define \_PAGE_BIT_PWT 3 #define _PAGE_BIT\_PCD 4 #define \_PAGE_BIT_ACCESSED 5 #define _PAGE_BIT\_DIRTY 6 #define \_PAGE_BIT_PSE 7 /* 4 MB (or 2MB) page, Pentium+, if present.. */ #define \_PAGE_BIT_GLOBAL 8 /* Global TLB entry PPro+ */ #define _PAGE_PRESENT 0x001 #define _PAGE_RW 0x002 #define _PAGE_USER 0x004 #define _PAGE_PWT 0x008 #define _PAGE_PCD 0x010 #define _PAGE_ACCESSED 0x020 #define _PAGE_DIRTY 0x040 #define _PAGE_PSE 0x080 /* 4 MB (or 2MB) page, Pentium+, if present.. */ #define _PAGE_GLOBAL 0x100 /* Global TLB entry PPro+ */ #define _PAGE_PROTNONE 0x080 /* If not present */
利用這些標誌位,咱們就能夠判斷處對應頁面的狀態,相關的宏定義以下:
<asm/pgtable-2level.h> #define pte_none(x) (!(x).pte_low) <asm/pgtable.h> #define pte_present(x) ((x).pte_low & (_PAGE_PRESENT | _PAGE_PROTNONE)) /* * The following only work if pte_present() is true. * Undefined behaviour if not.. */ static inline int pte_read(pte_t pte) { return (pte).pte_low & _PAGE_USER; } static inline int pte_exec(pte_t pte) { return (pte).pte_low & _PAGE_USER; } static inline int pte_dirty(pte_t pte) { return (pte).pte_low & _PAGE_DIRTY; } static inline int pte_young(pte_t pte) { return (pte).pte_low & _PAGE_ACCESSED; } static inline int pte_write(pte_t pte) { return (pte).pte_low & _PAGE_RW; }
對內核來講,當頁面表項的內容爲空(即值爲0)表示還沒有爲對應的虛存頁面創建映射。回想下邏輯地址映射的過程:利用邏輯地址的高10位在目錄表中查找到對應的目錄項,此目錄項指向一個頁表,再利用邏輯地址的中間10位在頁表中查找對應的頁面表項。按道理說,該頁表項應該指向物理頁面的起始地址(物理地址),但如今頁面表項的值爲0,即說明對應的虛存頁面還沒有映射到某個物理頁面上。內核用pte_none宏來檢測這種狀況。
若是頁面表項pte_t非空,但P(Present)位爲0,則表示映射已經創建,但對應的物理頁面不在內存中(已經換出到交換設備上了)。內核用pte_present宏來判斷pte_t對應的物理頁面是否在內存中。
pte_read等宏檢查pte_t中的相關位是否置1,從而獲得頁面的相關狀態和權限。固然這些只有當P位爲1時纔有效。
前一篇文章提到,在將linux三層頁式映射模型落實到intel的兩層頁式映射之上時,內核(2.4.0版本)採用讓中間目錄PMD「名不副實」的方案,咱們將相關細節集中展現在這裏:
<asm/page.h> /* PAGE_SHIFT determines the page size */ #define PAGE_SHIFT 12 #define PAGE_SIZE (1UL << PAGE_SHIFT) #define PAGE_MASK (~(PAGE_SIZE-1)) <asm/pgtable-2level.h> /* * traditional i386 two-level paging structure: */ #define PGDIR_SHIFT 22 #define PTRS_PER_PGD 1024 /* * the i386 is two-level, so we don't really have any * PMD directory physically. */ #define PMD_SHIFT 22 #define PTRS_PER_PMD 1 #define PTRS_PER_PTE 1024 <asm/pgtable.h> #define PMD_SIZE (1UL << PMD_SHIFT) #define PMD_MASK (~(PMD_SIZE-1)) #define PGDIR_SIZE (1UL << PGDIR_SHIFT) #define PGDIR_MASK (~(PGDIR_SIZE-1))
PGDIR_SHIFT、PMD_SHIFT和PAGE_SHIFT分別表示虛擬地址(即通過段式映射後的線性地址,後文對線性地址和虛擬地址不做區分,認爲它們是同一個東西)中頁面目錄位段、中間目錄位段以及頁表位段的劃分狀況,示意以下(偷圖自《深刻理解linux虛擬內存管理》):
PGDIR_SIZE、PMD_SIZE和PAGE_SIZE分別根據PGDIR_SHIFT、PMD_SHIFT和PAGE_SHIFT來定義,分別表示一個目錄項(pgd_t)、一箇中間目錄項(pmd_t)和一個頁面表項(pte_t)所能「領銜」的地址空間的大小。好比一個頁面表項指向一個物理頁面,它所能「領銜」的地址空間的大小就是4K(1 << 12)。而一個目錄項指向1個頁表(兩層映射的場景下),一個頁表共有1024個頁面表項,表明1024個頁面,每一個頁面4K,故一個目錄項表明4M(1 << 22)的空間
PGD_MASK、PMD_MASK和PAGE_MASK分別由PGDIR_SIZE、PMD_SIZE和PAGE_SIZE來定義。分別表示虛擬地址中頁面目錄位段、中間目錄位段以及頁表位段的掩碼。將虛擬地址與這些掩碼相位與,便可獲得對應的位段。好比目錄位段爲虛擬地址的高10位,那麼目錄位段的掩碼PGD_MASK應該是高10位爲1,低22位全爲0。MASK與SIZE的對應關係描述以下:
回到正題,2.4.0內核是如何讓中間目錄項PMD名不副實的呢?
在上面的代碼中,內核將PGD_SHIFT定義爲22,PTRS_PER_PGD定義爲1024。毫無疑義,虛擬地址的高10位用做頁面目錄位段,故PGD_SHIFT定義爲22。頁面目錄表PGD中有1024個目錄項,這與頁面目錄位段共有10bit相對應,這就是PTRS_PER_PGD的含義。而PMD_SHIFT也被定義成22,對應的PTRS_PER_PMD被定義成1。顯然這是內核玩的花招,讓PMD位段在虛擬地址中佔0個bit,還「裝模做樣」的定義了中間目標表中中間目錄的個數爲1個。
須要指出,linux的三層映射只是軟件設計上的概念,表示一種抽象。而在intel上,兩層頁面映射是由MMU硬件完成的,只要咱們設置好了CR3寄存器,MMU硬件自動幫咱們完成頁面映射(查頁面目錄表找到對應的目錄項,該目錄項指向一個頁表,再從頁表中找到對應的頁面表項等等,全程不須要CPU的參入),它壓根兒就不認PMD(只認PGD和PT),更不在意內核耍了什麼花招讓PMD「名不副實」,讓PMD「名不副實」只是軟件上的訴求,只是爲了「套上」linux的「三層映射」模型。以前都是內核欺騙CPU,感受此次像是內核本身欺騙本身了。