Linux內存管理第三章 -- 頁表管理(Page Table Management)

Linux內存管理第三章 – 頁表管理(Page Table Management)

頁目錄描述(Describing the Page Directory)

每一個進程都有一個指針指向它自己的PGD(Page Global Directory),PGD是一個物理頁框。該頁框包含有一組類型爲pgd_t的結構。該類型有具體的架構代碼中指定。例如在x86下,其定義如下:

	typedef struct { unsigned long long pgd; } pgd_t;
	typedef struct { unsigned long pte_low, pte_high; } pte_t;
	typedef struct { unsigned long long pmd; } pmd_t;

每種架構加載page tables的方式有所不同。例如x86架構下,每個進程的page tables是通過複製mm_struct->pgd到cr3寄存器進行加載的。

  • PGD中的每一個active項都對應着一個物理頁框,該頁框包含一組PMD(Page Middle Directory)其類型爲pmd_t。
  • PMD對應的又是一個物理頁框,PMD頁框中時一組PTE(Page Table Entries)其類型爲pte_t,
  • PTE對應一個物理頁框用於存放最終的數據。
    fenye

一條線性地址可能被切割爲多個部分形成多級頁表和頁內偏移。爲了幫助線性地址的切割,爲每一級也表定義了一個宏:

	/* PAGE_SHIFT determines the page size */
	#define PAGE_SHIFT	12
	#define PAGE_SIZE	(1UL << PAGE_SHIFT)
	#define PAGE_MASK	(~(PAGE_SIZE-1))

dizhi
1233

頁表項描述(Describing a Page Table Entry)

如上面所描述,struct pte_t,pmd_t,pgd_t分別描述PTE,PMD,PGD。儘管它們通常是一個非負整數,但是它們仍然被定義成結構體有以下兩種原因:

  1. 爲了類型保護,因此避免它們被人不合時宜的誤用
  2. 爲了支持x86的PAE功能,因爲PAE有多處4個bit來描述大於4GB的memory

從下面的定義來看,分別有兩種定義:

#ifdef CONFIG_X86_PAE
	extern unsigned long long __supported_pte_mask;
	extern int nx_enabled;
	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;
	typedef struct { unsigned long long pgprot; } pgprot_t;
	#define pte_val(x)	((x).pte_low | ((unsigned long long)(x).pte_high << 32))
	#define HPAGE_SHIFT	21
#else
	#define nx_enabled 0
	typedef struct { unsigned long pte_low; } pte_t;
	typedef struct { unsigned long pmd; } pmd_t;
	typedef struct { unsigned long pgd; } pgd_t;
	typedef struct { unsigned long pgprot; } pgprot_t;
	#define boot_pte_t pte_t /* or would you rather have a typedef */
	#define pte_val(x)	((x).pte_low)
	#define HPAGE_SHIFT	22
#endif

爲了類型轉換,分別定義了4對宏方便轉換:
struct -> uint:pte_val(), pmd_val(), pgd_val() ,pgprot_val()
uint -> struct:__pte(), __pmd(), __pgd() , __pgprot()

宏pgprot_t 是用來存儲pte的保護位,一般用來與pte低12位進行比較和設置其值。
pte在x86沒有開PAE的case下,其低12位是用來存儲保護位標誌的:
tupain

  • Field:
    由於一個頁框的容量是4KB,所以該物理地址的低12位總是0‘
    若此頁表結構指向的一個頁目錄項,則該字段的物理地址對應的物理頁框裏面的內容是一個頁表。
    若此頁表結構指向的一個頁表項,則該字段的物理地址對應的物理頁框是一頁數據
  • Present (P):
    -若Present = 1,所指的頁在內存中
    若Present = 0,所指的頁不在內存中.此時該結構中的其他位置可以由操作系統來支配,如果運行的的線性地址對用的頁表項的Present = 0,則分頁單元會將該線性地址存入寄存器cr2,併產生一個缺頁異常:14號異常
  • Accessed (A):
    分頁單元堆相應的頁框進行尋址時就設置這個標誌.當選中頁被交換出去時,該標誌位就可以有操作系統來支配.分頁單元從不重置這個標誌
  • Dirty (D):
    此標誌只用於頁表項中,用於標記頁框有進行寫操作
  • Read/Write (R/W):
    頁或者頁表的存取權限
  • User/Supervisor (U/S):
    頁或者頁表所需的特權級
  • Page Size:
    只應用於目錄項,若設置爲1,則頁目錄項指的是4MB或者2MB的頁框.

如何使用頁表項(Using Page Table Entries)

爲了遍歷頁目錄,下面三個宏被定義用來將一個線性地址快速分理出其內部組成部分。

  • pgd_offset():輸入線性地址和mm_struct,返回線性地址中對應的PGD項。
#define pgd_offset(mm, address) ((mm)->pgd+pgd_index(address))
#define pgd_index(address) (((address) >> PGDIR_SHIFT) & (PTRS_PER_PGD-1))

pmd_offset():輸入一個PGD項(找到頁框地址)和一個線性地址(找到pmd的偏移),返回一個對應的PMD

#define pmd_offset(dir, address) ((pmd_t *) pgd_page(*(dir)) + pmd_index(address))
#define pgd_page(pgd) ((unsigned long) __va(pgd_val(pgd) & PAGE_MASK))

pte_offset_kernel():輸入一個PMD(找到頁框地址)和一個線性地址(找到頁內偏移)

#define pte_offset_kernel(pmd, address) ((pte_t *) pmd_page_kernel(*(pmd)) +  pte_index(address))
#define pmd_page_kernel(pmd) ((unsigned long) __va(pmd_val(pmd) & PAGE_MASK))
#define pte_index(address) (((address) >> PAGE_SHIFT) & (PTRS_PER_PTE - 1))

第二輪的宏函數是用來檢查頁表項是否存在或者是否有人在使用:

  • pte_none(), pmd_none() and pgd_none():如果對應的項不存在,返回1
  • pte_present(), pmd_present() and pgd_present():如果對應項的PRESENT爲被置位,返回1
  • pte_clear(), pmd_clear() and pgd_clear():將會清除對應的項
  • pmd_bad() and pgd_bad() :用來檢查頁表項是否符合要求

上述的幾組宏的使用例程:

        pgd_t *pgd;
        pmd_t *pmd;
        pte_t *ptep, pte;
        pgd = pgd_offset(mm, address);
        if (pgd_none(*pgd) || pgd_bad(*pgd))
                 goto out;
        pmd = pmd_offset(pgd, address);
        if (pmd_none(*pmd) || pmd_bad(*pmd))
                 goto out;
         ptep = pte_offset(pmd, address);
         if (!ptep)
                goto out;
         pte = *ptep;

第三輪的宏是用來檢查頁表項的權限和設置頁表項的權限。這些權限決定了一個用戶空間的進程在一個page上能幹什麼不能幹什麼。舉例:內核頁表項永遠不能被用戶進程讀取。

  • pte_read():用來測試pte的讀權限,pte_mkread() 設置讀權限,pte_rdprotect()取消讀權限
  • pte_write():用來測試pte的寫權限,pte_mkwrite() 設置讀權限,pte_wrprotect()取消讀權限
  • pte_dirty():用來測試是否有被寫過,pte_mkdirty()設置dirty位,pte_mkclean()清除dirty位
  • pte_young():用來測試是否是新頁,pte_mkyoung()設置新頁,pte_old()設置位舊頁(檢查access位)

轉換和設置頁表項(Translating and Setting Page Table Entries)

  • mk_pte():輸入一個struct page和保護位組合形成pte_t。
	#define page_to_pfn(page)	((unsigned long)((page) - mem_map)) //mem_map中的偏移就是PFN
	#define pfn_pte(pfn, prot)	__pte(((pfn) << PAGE_SHIFT) | pgprot_val(prot)) //將PFN與權限bit爲合併形成pte_t
	#define mk_pte(page, pgprot)	pfn_pte(page_to_pfn(page), (pgprot))
  • set_pte():輸入一個PDM頁框內的地址,然後將pte_t賦值在這個地址中。
	#define set_pte(pteptr, pteval) (*(pteptr) = pteval)
  • pte_page():將pte_t轉換爲struct page

分配釋放頁表(Allocating and Freeing Page Tables)

  • 分配函數: pgd_alloc(), pmd_alloc() and pte_alloc()
  • 釋放函數:pgd_free(), pmd_free() and pte_free()

內核頁表(Kernel Page Tables)

當系統啓動時,分頁功能還沒有啓用因爲頁表不會自己初始化自己。因爲每種架構中的具體實現各不相同本文只討論x86的case。page table的初始化被分爲兩個階段:

  • bootstrap階段創建前8MB的頁表來開啓分頁單元。
  • Finalising階段初始化剩餘的頁表。

Bootstrapping階段

在文件arch/i386/kernel/head.S中的startup_32()彙編函數負責開啓分頁單元。一般內核的所有常規代碼編譯後的內核鏡像vmlinuz的起始地址被設置位PAGE_OFFSET + 1MB。而內核實際加載地址是物理內存1MB的位置開始的。從0~1MB這段物理地址通常被某些設備用來個BIOS通信,所以被內核棄之不用。Bootstrapping階段的代碼從虛擬地址轉換爲物理地址的方法是vaddr - _PAGE_OFFSET,就是直接映射。Bootstrapping階段要用此方法映射從1MB開始的前8MB物理地址(1MB ~ 9MB)到虛擬地址,直到分頁單元被啓用。
內核頁表初始化從內核編譯時靜態定義的swapper_pg_dir數組開始,swapper_pg_dir的地址爲0x00101000,再建立兩頁的頁表項:pg0,pg1。將swapper_pg_dir中的第0項和第768項設置爲pg0的物理地址,第1項和第769項設定爲pg1頁框的物理地址,swapper_pg_dir中的其他項都填0.這也就是說當分頁功能開啓的時候,內核無論是用物理地址還是虛擬地址都可以將這兩頁表映射到正確的page中。其餘的頁表由paging_init()函數來進行初始化。
一旦臨時內核頁表映射完成,就會通過設置cr0寄存器的一個bit位來開啓分頁單元。

Finalising階段

在此階段會執行paging_init()函數來執行,其調用流程如下:
初四花

  • pagetable_init() 會初始化內核線性映射區間的虛擬地址到物理地址轉換所需的頁表。此時只創建pmd和pte,並未將每個頁框創建相應的struct page,其具體初始化的虛擬地址段如下圖所示:
    12233
    讓後再調用page_table_range_init()初始化固定映射區域的內核頁表:
    fix
    一旦pagetable_init()返回,內核空間的所有頁表都初始化完成,因此swapper_pg_dir會被加載到cr3寄存器,以此這些頁表就可被分頁單元使用了。
  • kmap_init():將rang_init對應的PTEs加上保護位PAGE_KERNEL
  • zone_sizes_init():初始化各個zone,其中最重要的是爲每個頁框分配struct page,形成mem_map數組。

Mapping addresses to a struct page

Mapping Physical to Virtual Kernel Addresses

從Linux的線性映射我們可以知道物理地址0對應虛擬地址PAGE_OFFSET(3GB),因此任何線性映射區的虛擬地址轉換成物理地址的方法就非常簡單了,直接將虛擬地址減去PAGE_OFFSET即可。下面來看看內核的實現:

  • 虛擬地址 --> 物理地址
     #define __pa(x)   ((unsigned long)(x)-PAGE_OFFSET) //將虛擬地址轉換爲物理地址
     static inline unsigned long virt_to_phys(volatile void * address)
    {
             return __pa(address);
    }
    
  • 物理地址 --> 爲虛擬地址
    #define __va(x)			((void *)((unsigned long)(x)+PAGE_OFFSET)) //將物理地址轉換爲虛擬地址
    static inline void * phys_to_virt(unsigned long address)
    {
    	return __va(address);
    }
    

Mapping struct pages to Physical Addresses

從上面的章節我們可以知道kernel image的起始地址是物理地址1MB的地方,然後改物理地址轉換成虛擬地址之後就是PAGE_OFFSET + 0x0010000,其後續的一個8MB的地址空間是留給內核的靜態代碼使用的區域。所以這是不是就預示着第一個可用的虛擬地址就是0xC0800000呢?其實不是如此,Linux儘量將前16MB的虛擬地址留給ZONE_DMA,因此第一個可供內核動態分配可用的虛擬地址是0xC1000000。這也就是全局變量mem_map的地址。ZONE_DMA只有在很必要的情況下使用。
物理地址轉換爲struct page是通過將物理地址視爲mem_map數組的index。將物理地址向右平移PAGE_SHIFT個bit將得到PFN,PFN也是mem_map的index。即 struct *page = mem_map[paddr >> PAGE_SHIFT]

  • vaddr -> struct page:
    #define virt_to_page(kaddr) (mem_map + (__pa(kaddr) >> PAGE_SHIFT))
    
  • struct page -> paddr:
    顯而易見,通過page的地址就可以知道該page在mem_map中的index,其index就是PFN,然後將其後12位補0,就是物理地址。
    至此來總結下Linux的地址模型:
    moxing

Translation Lookaside Buffer (TLB)

在早期,當處理器需要將虛擬地址映射成物理地址時,它必須掃描所有頁目錄來搜索需要的PTE。爲了避免這種可以想象的消耗,各種不同的硬件架構都給出了一小塊內部緩存空間,即提供TLB來緩存虛擬地址到物理地址的轉換表。
儘管不是所有架構都有提供TLB,但是Linux內核還是假設所有架構夠提供,不能提供的在編譯的時候關掉響應的config即可。Linux提供了一些刷新TLB的hook函數:

void flush_tlb_all(void)
刷新系統中每一個處理器的整個TLB,這時最昂貴的TLB刷新操作。當該操作完成之後,所有對page tables的修改都變得全局可見。例如當調用vfree()之後就需要刷新整個TLB
void flush_tlb_mm(struct mm_struct *mm)
刷新userspace中和mm_struct相關的整個TLB。在某些架構下,如MIPS,該操作需要對應所有的處理器,但通常是局部的處理器。該操作僅僅是在當整個address space都有影響的case下才執行如:fork的時候複製了整個地址空間或者是刪除了所有地址映射
void flush_tlb_range(struct mm_struct *mm, unsigned long start, unsigned long end)
刷新mm上下文中指定範圍內的用戶空間的頁表。當一個新的region被移動或者被改變時如調用mremap()時調用
void flush_tlb_page(struct vm_area_struct *vma, unsigned long addr)
該API是用來刷新TLB中的單個page,當一個page產生page fault之後或者是page out之後調用
void flush_tlb_pgtables(struct mm_struct *mm, unsigned long start, unsigned long end)
當page tables被釋放或者被分裂時調用。有些平臺只緩存最低level的page table,PTE,當pages被刪除或者是被重新映射後,就需要刷新TLB
void update_mmu_cache(struct vm_area_struct *vma, unsigned long addr, pte_t pte)
該API僅僅在page fault完成之後被調用

Level 1 CPU Cache Management

CPU caches,例如,TLB caches,主要是利用程序優先使用局部的數據。爲了避免在使用數據時都要從主內存中取數據,CPU從而開始緩存一小部分數據在CPU cache中。通常來講,有兩種層級的cache,Level 1 和Level 2 CPU cache,L2 cache比L1 cache要慢很多。Linux通常只關心L1 cache。
CPU cache通常被組織成lines(行)。每個Line通常非常小,一般是32個字節,每個Line都需要與自己的邊界對齊。換句話說,就是一個32字節的cache line將要求32字節對齊。在Linux中line大小有變量L1_CACHE_BYTES來描述,由各種架構自己定義具體的值。
地址如何被映射到cache line因架構而異。但是主要有三種方式:

  • direct mapping:每個內存塊只能映射到一個可能的cache line
  • associative mapping:任意的內存塊能映射到任何一個cache line
  • set associative mapping:一種組合方法,任何內存狂只能映射到某個緩存行子集中的任意行。

不管是什麼映射方案,它們有一個共同點:地址相近並且和cache size對齊的地址儘量使用不同的緩存行。因此Linux使用最簡單的策略來最大利用cache:

  • 結構中經常被訪問的字段通常在結構開始的位置,從而增加僅用一個緩存行來處理普通字段的機會。
  • 一個結構中不相關的項應該至少有緩存大小的字節數的隔離從而避免在CPU之間的錯誤共享。
  • 通用緩存中的對象,如mm_struct 緩存,應該要與L1 CPU cache對齊從而避免錯誤共享。

如果CPU要引用的一個地址不再CPU cache中,CPU就需要從主內存中取數據。然後CPU cache匹配失敗的代價是非常昂貴的,因爲從L1 cache中訪問一個地址只需要10ns,而從主內存中訪問一個地址要100 ~ 200ns。因此最基本的原則是儘可能多的命中緩存儘可能少的命失緩存。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章