Linux kernel 內存 - 頁表映射(SHIFT,SIZE,MASK)和轉換(32位,64位)


0. Intro

如下是在32位下的情況,32位下,只有三級頁表:PGD,PMD,PTE

在64位情況下,會有四級頁表:PGD,PUD,PMD,PTE

但是原理基本上是一樣的,本文主要是想記錄一下頁表轉換中的幾個 基本概念宏:SHITF,SIZE,MASK以及之間的轉換。

1. Linux虛擬內存三級頁表 (本文以32位為主線)

Linux虛擬內存三級管理由以下三級組成:

  • PGD: Page Global Directory (頁目錄)
  • PMD: Page Middle Directory (頁目錄)
  • PTE:  Page Table Entry  (頁表項)

每一級有以下三個關鍵描述宏:

  • SHIFT
  • SIZE
  • MASK

如頁的對應描述為:

/* PAGE_SHIFT determines the page size  asm/page.h */
#define PAGE_SHIFT		12
#define PAGE_SIZE		(_AC(1,UL) << PAGE_SHIFT)
#define PAGE_MASK		(~(PAGE_SIZE-1))

數據結構定義如下:

/* asm/page.h */
typedef unsigned long pteval_t;
 
typedef pteval_t pte_t;
typedef unsigned long pmd_t;
typedef unsigned long pgd_t[2];
typedef unsigned long pgprot_t;
 
#define pte_val(x)      (x)
#define pmd_val(x)      (x)
#define pgd_val(x)	((x)[0])
#define pgprot_val(x)   (x)
 
#define __pte(x)        (x)
#define __pmd(x)        (x)
#define __pgprot(x)     (x)

2 Page Directory (PGD and PMD)

每個進程有它自己的PGD( Page Global Directory),它是一個物理頁,並包含一個pgd_t數組。其定義見<asm/page.h>。 進程的pgd_t數據見 task_struct -> mm_struct -> pgd_t * pgd;

ARM架構的PGD和PMD的定義如下<arch/arm/include/asm/pgtable.h>:

#define PTRS_PER_PTE  512 // PTE中可包含的指針<u32>數 (21-12=9bit) #define PTRS_PER_PMD  1 #define PTRS_PER_PGD  2048 // PGD中可包含的指針<u32>數 (32-21=11bit)

#define PTE_HWTABLE_PTRS (PTRS_PER_PTE) #define PTE_HWTABLE_OFF  (PTE_HWTABLE_PTRS * sizeof(pte_t)) #define PTE_HWTABLE_SIZE (PTRS_PER_PTE * sizeof(u32))

/*  * PMD_SHIFT determines the size of the area a second-level page table can map  * PGDIR_SHIFT determines what a third-level page table entry can map  */ #define PMD_SHIFT  21 #define PGDIR_SHIFT  21

虛擬地址SHIFT宏圖:

虛擬地址MASK和SIZE宏圖:

3. Page Table Entry

PTEs, PMDs和PGDs分別由pte_t, pmd_t 和pgd_t來描述。為了存儲保護位,pgprot_t被定義,它擁有相關的flags並經常被存儲在page table entry低位(lower bits),其具體的存儲方式依賴於CPU架構。

每個pte_t指向一個物理頁的地址,並且所有的地址都是頁對齊的。因此在32位地址中有PAGE_SHIFT(12)位是空閑的,它可以為PTE的狀態位。

PTE的保護和狀態位如下圖所示:

4. 如何通過3級頁表訪問物理內存

為了通過PGD、PMD和PTE訪問物理內存,其相關宏在asm/pgtable.h中定義。

  • pgd_offset

根據當前虛擬地址和當前進程的mm_struct獲取pgd項的宏定義如下:

/* to find an entry in a page-table-directory */
#define pgd_index(addr)		((addr) >> PGDIR_SHIFT)  //獲得在pgd表中的索引
#define pgd_offset(mm, addr)	((mm)->pgd + pgd_index(addr)) //獲得pmd表的起始地址
 
/* to find an entry in a kernel page-table-directory */
#define pgd_offset_k(addr)	pgd_offset(&init_mm, addr)
  • pmd_offset

根據通過pgd_offset獲取的pgd 項和虛擬地址,獲取相關的pmd項(即pte表的起始地址)

/* Find an entry in the second-level page table.. */
#define pmd_offset(dir, addr)	((pmd_t *)(dir))   //即為pgd項的值
        
  • pte_offset

根據通過pmd_offset獲取的pmd項和虛擬地址,獲取相關的pte項(即物理頁的起始地址)

#ifndef CONFIG_HIGHPTE
#define __pte_map(pmd)		pmd_page_vaddr(*(pmd))
#define __pte_unmap(pte)	do { } while (0)
#else
#define __pte_map(pmd)		(pte_t *)kmap_atomic(pmd_page(*(pmd)))
#define __pte_unmap(pte)	kunmap_atomic(pte)
#endif
 
#define pte_index(addr)		(((addr) >> PAGE_SHIFT) & (PTRS_PER_PTE - 1))
 
#define pte_offset_kernel(pmd,addr)	(pmd_page_vaddr(*(pmd)) + pte_index(addr))
 
#define pte_offset_map(pmd,addr)	(__pte_map(pmd) + pte_index(addr))
#define pte_unmap(pte)			__pte_unmap(pte)
 
#define pte_pfn(pte)		(pte_val(pte) >> PAGE_SHIFT)
#define pfn_pte(pfn,prot)	__pte(__pfn_to_phys(pfn) | pgprot_val(prot))
 
#define pte_page(pte)		pfn_to_page(pte_pfn(pte))
#define mk_pte(page,prot)	pfn_pte(page_to_pfn(page), prot)
 
#define set_pte_ext(ptep,pte,ext) cpu_set_pte_ext(ptep,pte,ext)
#define pte_clear(mm,addr,ptep)	set_pte_ext(ptep, __pte(0), 0)

其示意圖如下圖所示:

64位

上面主要是介紹了32位的 頁表轉換 邏輯;

現在我們來看看64位的頁表轉換邏輯, 和32位的區別;

區別

  1. 32位的是32個bit,64位的是64個bit的虛擬地址;但是這個64位的虛擬地址中不是每一個bit都使用了,現在只使用了48個bit,其中,PGD,PUD,PMD,PTE分別是9個bit,PAGE大小占用12個bit,12個bit剛好是一個page的大小,也就是4k。
  2. 需要注意的是,PGD,PUD,PMD,PTE分別都是9個bit,在虛擬地址中,虛擬地址轉換到物理地址,是由MMU完成的,MMU根據虛擬地址,分別抽出PGD,PUD,PMD,PTE的值,就可以計算出物理機制。
  3. PGD,PUD,PMD,PTE 分別都是一個4k的page,其實,PGD,PUD,PMD,PTE 是四張table,table的大小都是4k,其中table的entry分別是:pgd_t, pud_t, pmd_t, pte_t, 都是unsigned long 類型(8個字節),4k(2的12次方)/8 字節 (2的3次方)= 512 個entry(2的9次方)
  4. PTE的table大小也是4k,entry大小也是8字節,所以,PTE表中可以存放512個entry(也就是512個物理機地址),8個字節是64位,其中PTE只需要48位就可以了,剩下的12位作為flag,記錄,這個pte entry的屬性(accessed,present,dirty ...)

arch/x86/include/asm/page_types.h

#define PAGE_SHIFT      12
#define PAGE_SIZE       (_AC(1,UL) << PAGE_SHIFT)
#define PAGE_MASK       (~(PAGE_SIZE-1))

#define PMD_PAGE_SIZE       (_AC(1, UL) << PMD_SHIFT)
#define PMD_PAGE_MASK       (~(PMD_PAGE_SIZE-1))

#define PUD_PAGE_SIZE       (_AC(1, UL) << PUD_SHIFT)
#define PUD_PAGE_MASK       (~(PUD_PAGE_SIZE-1))

SHIFT

arch/x86/include/asm/pgtable_64_types.h 中,定義了 64位 x86下的,pte_t的類型其實是pteval_t, 而 pteval_t 其實是 unsigned long 類型。其他的也一樣都是unsigned long , unsigned long 在x86_64 下是8個字節。

// PAGE_SHIFT是12位,PMD_SHITT就是21位,剛好,PTE占用了9位
arch/x86/include/asm/pgtable_64_types.h <<PMD_SHIFT>>
#define PMD_SHIFT 21
#define PUD_SHIFT 30

typedef unsigned long   pteval_t;
typedef unsigned long   pmdval_t;
typedef unsigned long   pudval_t;
typedef unsigned long   pgdval_t;
typedef unsigned long   pgprotval_t;

typedef struct { pteval_t pte; } pte_t;

 32位編譯器:

      char :1個字節
      char*(即指針變量): 4個字節(32位的尋址空間是2^32, 即32個bit,也就是4個字節。同理64位編譯器)
      short int : 2個字節
      int:  4個字節
      unsigned int : 4個字節
      float:  4個字節
      double:   8個字節
      long:   4個字節
      long long:  8個字節
      unsigned long:  4個字節

  64位編譯器:

      char :1個字節
      char*(即指針變量): 8個字節
      short int : 2個字節
      int:  4個字節
      unsigned int : 4個字節
      float:  4個字節
      double:   8個字節
      long:   8個字節
      long long:  8個字節
      unsigned long:  8個字節

四級分頁模型

x86-64架構采用四級分頁模型,它是Linux四級分頁機制的一個很好的實現。我們將x86-64架構的分頁模型作為分析的入口點,它很好的“迎合”了Linux的四級分頁機制。稍候我們再分析這種機制如何同樣做到適合三級和二級分頁模型。

PGDIR_SHIFT及相關宏

表示線性地址中offset字段、Table字段、Middle Dir字段和Upper Dir字段的位數。PGDIR_SIZE用於計算頁全局目錄中一個表項能映射區域的大小。PGDIR_MASK用於屏蔽線性地址中Middle Dir字段、Table字段和offset字段所在位。

在四級分頁模型中,PGDIR_SHIFT占據39位,即9位頁上級目錄、9位頁中間目錄、9位頁表和12位偏移。頁全局目錄同樣占線性地址的9位,因此PTRS_PER_PGD為512。

arch/x86/include/asm/pgtable_64_types.h
#define PGDIR_SHIFT 39
#define PTRS_PER_PGD 512
#define PGDIR_SIZE (_AC(1, UL) << PGDIR_SHIFT)
#define PGDIR_MASK (~(PGDIR_SIZE - 1))

pgd_offset()

該函數返回線性地址address在頁全局目錄中對應表項的線性地址。mm為指向一個內存描述符的指針,address為要轉換的線性地址。該宏最終返回addrress在頁全局目錄中相應表項的線性地址。

arch/x86/include/asm/pgtable.h

#define pgd_index(address) (((address) >> PGDIR_SHIFT) & (PTRS_PER_PGD - 1))

#define pgd_offset(mm, address) ((mm)->pgd + pgd_index((address)))

PUD_SHIFT及相關宏

表示線性地址中offset字段、Table字段和Middle Dir字段的位數。PUD_SIZE用於計算頁上級目錄一個表項映射的區域大小,PUD_MASK用於屏蔽線性地址中Middle Dir字段、Table字段和offset字段所在位。

在64位系統四級分頁模型下,PUD_SHIFT的大小為30,包括12位的offset字段、9位Table字段和9位Middle Dir字段。由於頁上級目錄在線性地址中占9位,因此頁上級目錄的表項數為512。

arch/x86/include/asm/pgtable_64_types.h

#define PUD_SHIFT 30
#define PTRS_PER_PUD 512
#define PUD_SIZE        (_AC(1, UL) << PUD_SHIFT)
#define PUD_MASK        (~(PUD_SIZE - 1))

pud_offset()

pgd_val(pgd)獲得pgd所指的頁全局目錄項,它與PTE_PFN_MASK相與得到該項所對應的物理頁框號。__va()用於將物理地址轉化為虛擬地址。也就是說,pgd_page_vaddr最終返回頁全局目錄項pgd所對應的線性地址。因為pud_index()返回線性地址在頁上級目錄中所在表項的索引,因此pud_offset()最終返回addrress對應的頁上級目錄項的線性地址。

arch/x86/include/asm/page.h

#define __va(x)                 ((void *)((unsigned long)(x)+PAGE_OFFSET))

arch/x86/include/asm/pgtable_types.h
#define PTE_PFN_MASK            ((pteval_t)PHYSICAL_PAGE_MASK)
arch/x86/include/asm/pgtable.h
static inline unsigned long pgd_page_vaddr(pgd_t pgd)
{
        return (unsigned long)__va((unsigned long)pgd_val(pgd) & PTE_PFN_MASK);
}
static inline pud_t *pud_offset(pgd_t *pgd, unsigned long address)

{
        return (pud_t *)pgd_page_vaddr(*pgd) + pud_index(address);
}

PMD_SHIFT及相關宏

表示線性地址中offset字段和Table字段的位數,2的PMD_SHIFT次冪表示一個頁中間目錄項可以映射的內存區域大小。PMD_SIZE用於計算這個區域的大小,PMD_MASK用來屏蔽offset字段和Table字段的所有位。PTRS_PER_PMD表示頁中間目錄中表項的個數。

在64位系統中,Linux采用四級分頁模型。線性地址包含頁全局目錄、頁上級目錄、頁中間目錄、頁表和偏移量五部分。在這兩種模型中PMD_SHIFT占21位,即包括Table字段的9位和offset字段的12位。PTRS_PER_PMD的值為512,即2的9次冪,表示頁中間目錄包含的表項個數。

#define PMD_SHIFT 21
#define PTRS_PER_PMD 512
#define PMD_SIZE (_AC(1, UL) << PMD_SHIFT)
#define PMD_MASK (~(PMD_SIZE - 1))

pmd_offset()

該函數返回address在頁中間目錄中對應表項的線性地址。

static inline pmd_t *pmd_offset(pud_t *pud, unsigned long address)
{
        return (pmd_t *)pud_page_vaddr(*pud) + pmd_index(address);
}
static inline unsigned long pud_page_vaddr(pud_t pud)
{
        return (unsigned long)__va((unsigned long)pud_val(pud) & PTE_PFN_MASK);
}

PAGE_SHIFT及相關宏

表示線性地址offset字段的位數。該宏的值被定義為12位,即頁的大小為4KB。與它對應的宏有PAGE_SIZE,它返回一個頁的大小;PAGE_MASK用來屏蔽offset字段,其值為oxfffff000。PTRS_PER_PTE表明頁表在線性地址中占據9位。

arch/x86/include/asm/page_types.h

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

通過上面的分析可知,在x86-64架構下64位的線性地址被划分為五部分,每部分占據的位數分別為9,9,9,9,12,實際上只用了64位中的48位。對於四級頁表而言,級別從高到底每級頁表中表項的個數為512,512,512,512。

Refs

https://wenku.baidu.com/view/c565e26da98271fe910ef970.html

https://blog.csdn.net/shuningzhang/article/details/38090695


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM