專題:Linux內存管理專題
關鍵詞:swapper_pd_dir、ARM PGD/PTE、Linux PGD/PTE、pgd_offset_k。
Linux下的頁表映射分為兩種,一是Linux自身的頁表映射,另一種是ARM32 MMU硬件的映射。
1. ARM32頁表映射
由於ARM32和Linux內核維護的頁表項有所不同,所以維護了兩套PTE。
PGD存放在swapper_pd_dir中,一個PGD目錄項其實包含了兩份ARM32 PGD。
所以再分配PTE的時候,共分配了1024個PTE,512個給Linux OS維護用;512個給ARM32 MMU用,對應兩個PGD的頁表數目。
由於Linux OS和ARM32的PTE緊鄰,所以兩者的轉換也方便進行。
1.1 ARM32處理器查詢頁表
32bit的Linux采用三級映射:PGD-->PMD-->PTE,64bit的Linux采用四級映射:PGD-->PUD-->PMD-->PTE,多了個PUD。
縮寫是PGD:Page Global Directory、PUD:Page Upper Directory、PMD:Page Middle Directory、PTE:Page Table Entry。
在ARM32 Linux采用兩層映射,省略了PMD,除非在定義了CONFIG_ARM_LPAE才會使用3級映射。
在ARM32架構中,可以按段(section)來映射,這是采用單層映射模式。
使用頁面映射需要兩層映射結構,頁面可以是64KB或4KB大小。
1.1.1 ARM32架構MMU4KB頁面映射過程
如果采用頁表映射的方式,段映射表就變成一級映射表(Linux中稱為PGD),其頁表項提供的不再是物理地址,而是二級頁表的基地址。
32位虛擬地址的高12位(bit[31:20])作為訪問一級頁表的索引值,找到相應的表項,每個表項指向一個二級頁表。
以虛擬地址的次8位(bit[19:12])作為訪問二級頁表的索引值,得到相應的頁表項,從這個頁表項中找到20位的物理頁面地址。
最后將這20位物理頁面地址和虛擬地址的低12位拼湊在一起,得到最終的32位物理地址。
這個過程在ARM32架構中由MMU硬件完成,軟件不需要接入。
ARM32架構MMU頁表映射過程
1.1.2 ARMv7-AR中關於Short Descriptor映射概覽圖
關於4K頁表的映射過程在ARMv7-AR用戶架構手冊有關介紹。
一個地址映射的概覽圖,32位虛擬地址從TTBR1中找到First-level table地址,然后取虛擬地址VA[31:20]作為序號找到Second-level table地址。
取虛擬地址VA[19:12]作為序號找到Page地址。
規格書中Small Page映射過程
Figure B3-11 Small page address translation是映射的細節:
1.2 Linux頁表映射相關數據結構
我們知道在map_lowmem()使用create_mapping()創建頁表映射,這個函數的參數結構是struct map_desc。
下面來研究它的相關結構,有助於理解內核是如何處理頁表映射的。
arch\arm\include\asm\mach\map.h: struct map_desc { unsigned long virtual;------虛擬地址起始地址 unsigned long pfn;----------物理地址開始頁幀號 unsigned long length;-------內存空間大小 unsigned int type;----------mem_types中的序號 };
map_desc中的type指向類型為struct mem_type的mem_types數組:
arch\arm\mm\mm.h: struct mem_type { pteval_t prot_pte;------------PTE屬性 pteval_t prot_pte_s2;---------定義CONFIG_ARM_LPAE才有效 pmdval_t prot_l1;-------------PMD屬性 pmdval_t prot_sect;-----------Section類型映射 unsigned int domain;----------定義ARM中不同的域 }; arch\arm\mm\mmu.c: static struct mem_type mem_types[] = { ... [MT_MEMORY_RWX] = { .prot_pte = L_PTE_PRESENT | L_PTE_YOUNG | L_PTE_DIRTY,----------------------注意這里都是L_PTE_*類型,需要在寫入MMU對應PTE時進行轉換。 .prot_l1 = PMD_TYPE_TABLE, .prot_sect = PMD_TYPE_SECT | PMD_SECT_AP_WRITE, .domain = DOMAIN_KERNEL, }, [MT_MEMORY_RW] = { .prot_pte = L_PTE_PRESENT | L_PTE_YOUNG | L_PTE_DIRTY | L_PTE_XN, .prot_l1 = PMD_TYPE_TABLE, .prot_sect = PMD_TYPE_SECT | PMD_SECT_AP_WRITE, .domain = DOMAIN_KERNEL, }, ... }
下面重點關注Page Table類型的一級頁表和二級頁表的細節,以及Linux內核中的定義:
ARM32中PGD定義
下面是First-level descriptor詳細說明:
/* * Hardware page table definitions. * * + Level 1 descriptor (PMD) * - common */ #define PMD_TYPE_MASK (_AT(pmdval_t, 3) << 0)---------------------------01對應PageTable #define PMD_TYPE_FAULT (_AT(pmdval_t, 0) << 0) #define PMD_TYPE_TABLE (_AT(pmdval_t, 1) << 0) #define PMD_TYPE_SECT (_AT(pmdval_t, 2) << 0) #define PMD_PXNTABLE (_AT(pmdval_t, 1) << 2) /* v7 */ #define PMD_BIT4 (_AT(pmdval_t, 1) << 4) #define PMD_DOMAIN(x) (_AT(pmdval_t, (x)) << 5) #define PMD_PROTECTION (_AT(pmdval_t, 1) << 9) /* v5 */
ARM32中PTE定義
下面是Second-level descriptor詳細說明:
/* * + Level 2 descriptor (PTE) * - common */ #define PTE_TYPE_MASK (_AT(pteval_t, 3) << 0) #define PTE_TYPE_FAULT (_AT(pteval_t, 0) << 0) #define PTE_TYPE_LARGE (_AT(pteval_t, 1) << 0) #define PTE_TYPE_SMALL (_AT(pteval_t, 2) << 0) #define PTE_TYPE_EXT (_AT(pteval_t, 3) << 0) /* v5 */ #define PTE_BUFFERABLE (_AT(pteval_t, 1) << 2) #define PTE_CACHEABLE (_AT(pteval_t, 1) << 3) /* * - extended small page/tiny page */ #define PTE_EXT_XN (_AT(pteval_t, 1) << 0) /* v6 */ #define PTE_EXT_AP_MASK (_AT(pteval_t, 3) << 4) #define PTE_EXT_AP0 (_AT(pteval_t, 1) << 4) #define PTE_EXT_AP1 (_AT(pteval_t, 2) << 4) #define PTE_EXT_AP_UNO_SRO (_AT(pteval_t, 0) << 4) #define PTE_EXT_AP_UNO_SRW (PTE_EXT_AP0) #define PTE_EXT_AP_URO_SRW (PTE_EXT_AP1) #define PTE_EXT_AP_URW_SRW (PTE_EXT_AP1|PTE_EXT_AP0) #define PTE_EXT_TEX(x) (_AT(pteval_t, (x)) << 6) /* v5 */ #define PTE_EXT_APX (_AT(pteval_t, 1) << 9) /* v6 */ #define PTE_EXT_COHERENT (_AT(pteval_t, 1) << 9) /* XScale3 */ #define PTE_EXT_SHARED (_AT(pteval_t, 1) << 10) /* v6 */ #define PTE_EXT_NG (_AT(pteval_t, 1) << 11) /* v6 */
Linux中PTE定義
由於Linux對於PTE的定義和ARM硬件不一致,下面的L_開頭的定義都是針對Linux的,L_MT開頭的是bit[5:2]表示的內存類型。
/* * "Linux" PTE definitions. * * We keep two sets of PTEs - the hardware and the linux version. * This allows greater flexibility in the way we map the Linux bits * onto the hardware tables, and allows us to have YOUNG and DIRTY * bits. * * The PTE table pointer refers to the hardware entries; the "Linux" * entries are stored 1024 bytes below. */ #define L_PTE_VALID (_AT(pteval_t, 1) << 0) /* Valid */ #define L_PTE_PRESENT (_AT(pteval_t, 1) << 0) #define L_PTE_YOUNG (_AT(pteval_t, 1) << 1) #define L_PTE_DIRTY (_AT(pteval_t, 1) << 6) #define L_PTE_RDONLY (_AT(pteval_t, 1) << 7) #define L_PTE_USER (_AT(pteval_t, 1) << 8) #define L_PTE_XN (_AT(pteval_t, 1) << 9) #define L_PTE_SHARED (_AT(pteval_t, 1) << 10) /* shared(v6), coherent(xsc3) */ #define L_PTE_NONE (_AT(pteval_t, 1) << 11) /* * These are the memory types, defined to be compatible with * pre-ARMv6 CPUs cacheable and bufferable bits: XXCB */ #define L_PTE_MT_UNCACHED (_AT(pteval_t, 0x00) << 2) /* 0000 */ #define L_PTE_MT_BUFFERABLE (_AT(pteval_t, 0x01) << 2) /* 0001 */ #define L_PTE_MT_WRITETHROUGH (_AT(pteval_t, 0x02) << 2) /* 0010 */ #define L_PTE_MT_WRITEBACK (_AT(pteval_t, 0x03) << 2) /* 0011 */ #define L_PTE_MT_MINICACHE (_AT(pteval_t, 0x06) << 2) /* 0110 (sa1100, xscale) */ #define L_PTE_MT_WRITEALLOC (_AT(pteval_t, 0x07) << 2) /* 0111 */ #define L_PTE_MT_DEV_SHARED (_AT(pteval_t, 0x04) << 2) /* 0100 */ #define L_PTE_MT_DEV_NONSHARED (_AT(pteval_t, 0x0c) << 2) /* 1100 */ #define L_PTE_MT_DEV_WC (_AT(pteval_t, 0x09) << 2) /* 1001 */ #define L_PTE_MT_DEV_CACHED (_AT(pteval_t, 0x0b) << 2) /* 1011 */ #define L_PTE_MT_VECTORS (_AT(pteval_t, 0x0f) << 2) /* 1111 */ #define L_PTE_MT_MASK (_AT(pteval_t, 0x0f) << 2)
ARM PMD描述符bit[8:5]用於描述Domain,但ARM Linux只定義使用三個:
#define DOMAIN_KERNEL 2---------用於內核空間 #define DOMAIN_TABLE 2 #define DOMAIN_USER 1-----------用於用戶空間 #define DOMAIN_IO 0-------------用於I/O地址域
1.3 設置PGD頁面目錄
create_mapping的參數是struct map_desc類型,用於描述一個虛擬地址區域線性映射到物理區域。基於這塊區域創建PGD/PTE。
static void __init create_mapping(struct map_desc *md) { unsigned long addr, length, end; phys_addr_t phys; const struct mem_type *type; pgd_t *pgd; ... type = &mem_types[md->type];------------------------------找到對應的struct mem_type ... addr = md->virtual & PAGE_MASK;---------------------------對齊到頁 phys = __pfn_to_phys(md->pfn);----------------------------頁到物理地址轉換 length = PAGE_ALIGN(md->length + (md->virtual & ~PAGE_MASK)); ...
pgd = pgd_offset_k(addr);---------------------------------根據addr找到對應虛擬地址對應的pgd地址 end = addr + length; do { unsigned long next = pgd_addr_end(addr, end); alloc_init_pud(pgd, addr, next, phys, type);----------初始化下一級頁表 phys += next - addr; addr = next; } while (pgd++, addr != end);-----------------------------遍歷區間地址,步長是PGDIR_SIZE,即2MB大小的空間。 }
這里面有三個地方需要解釋:
pgd_offset_k
將虛擬地址進行轉換得到PMD的指針。
#define PGDIR_SHIFT 21 /* to find an entry in a page-table-directory */ #define pgd_index(addr) ((addr) >> PGDIR_SHIFT) #define pgd_offset(mm, addr) ((mm)->pgd + pgd_index(addr)) #define pgd_offset_k(addr) pgd_offset(&init_mm, addr) struct mm_struct init_mm = { .mm_rb = RB_ROOT, .pgd = swapper_pg_dir, .mm_users = ATOMIC_INIT(2), .mm_count = ATOMIC_INIT(1), .mmap_sem = __RWSEM_INITIALIZER(init_mm.mmap_sem), .page_table_lock = __SPIN_LOCK_UNLOCKED(init_mm.page_table_lock), .mmlist = LIST_HEAD_INIT(init_mm.mmlist), INIT_MM_CONTEXT(init_mm) };
由虛擬內存布局圖中swapper_pg_dir可知,大小為16KB,里面有詳細的解釋。init_mm.pgd指向swapper_pg_dir。
pgd_addr_end
include\asm-generic\pgtable.h: #define pgd_addr_end(addr, end) \ ({ unsigned long __boundary = ((addr) + PGDIR_SIZE) & PGDIR_MASK; \ (__boundary - 1 < (end) - 1)? __boundary: (end); \ }) arch\arm\include\asm\pgtable-2level.h: /* * 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 #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為21,所以一個PGD頁表目錄對應2MB大小的空間,即[addr, addr+PGDIR_SIZE)。所以PGD的數目為2^11,2028個。整個PGD頁表占用空間為2048*4B=8KB。
這和ARM硬件的4096 PGD不一致。這里涉及到Linux實現技巧,在創建PTE中進行分析。
所以此處按照2MB步長,遍歷[virtual, virtual+length)空間創建PDG頁表和PTE。
alloc_init_pte
由於ARM-Linux采用兩級頁表映射,跳過PUD/PMD,直接到alloc_init_pte創建PTE。
alloc_init_pud-->alloc_init_pmd-->alloc_init_pte arch\arm\mm\mmu.c: static void __init alloc_init_pte(pmd_t *pmd, unsigned long addr,------------這里的pmd=pud=pgd。 unsigned long end, unsigned long pfn, const struct mem_type *type) { pte_t *pte = early_pte_alloc(pmd, addr, type->prot_l1);------------------使用prot_l1作為參數,創建PGD頁表目錄,返回addr對應的pte地址。 do { set_pte_ext(pte, pfn_pte(pfn, __pgprot(type->prot_pte)), 0);---------調用體系結構相關匯編,配置PTE。 pfn++; } while (pte++, addr += PAGE_SIZE, addr != end);-------------------------遍歷[addr, end)區間內存,以PAGE_SIZE為步長。 }
下面看看如何分配PGD頁表目錄:
static pte_t * __init early_pte_alloc(pmd_t *pmd, unsigned long addr, unsigned long prot) { if (pmd_none(*pmd)) {---------------------------------------------------如果PGD的內容為空,即PTE還沒有創建,擇取建立頁面。 pte_t *pte = early_alloc(PTE_HWTABLE_OFF + PTE_HWTABLE_SIZE);-------分配512+512個PTE頁表項 __pmd_populate(pmd, __pa(pte), prot);-------------------------------生成pmd頁表目錄,並刷入RAM } BUG_ON(pmd_bad(*pmd)); return pte_offset_kernel(pmd, addr);------------------------------------返回當前addr對應的PTE地址 } early_alloc-->early_alloc_aligned: static void __init *early_alloc_aligned(unsigned long sz, unsigned long align) { void *ptr = __va(memblock_alloc(sz, align));-------------------------------------基於memblock進行分配,這里分配4096B,剛好是一頁大小。 memset(ptr, 0, sz); return ptr; }
所以存放PGD需要的空間通過memblock進行申請,PTE_HWTABLE_OFF和PTE_HWTABLE_SIZE都為512,所以一個1024個PTE。
下面是early_pte_alloc分配的空間示意圖:前面512個表項是給Linux OS使用的,后512個表項是給ARM硬件MMU用的。
Linux內核PGD/PTE映射關系
static inline void __pmd_populate(pmd_t *pmdp, phys_addr_t pte, pmdval_t prot) { pmdval_t pmdval = (pte + PTE_HWTABLE_OFF) | prot;------------------生成pmdp[0]的內容 pmdp[0] = __pmd(pmdval); #ifndef CONFIG_ARM_LPAE pmdp[1] = __pmd(pmdval + 256 * sizeof(pte_t));---------------------生成緊鄰的pmdp[1]的內容 #endif flush_pmd_entry(pmdp);---------------------------------------------將pmdp兩個刷入到RAM中 }
Linux的PGD頁表目錄和ARM32不同,總數和ARM32是一樣的。
在arm_mm_memblock_reserve中,通過swapper_pg_dir可以知道其大小為16KB。
就來看看SWAPPER_PG_DIR_SIZE,一共2048個PGD,但是每個PGD包含了兩個相鄰的PGD頁面目錄項。
typedef pmdval_t pgd_t[2];---------------------------------------------8字節
#define SWAPPER_PG_DIR_SIZE (PTRS_PER_PGD * sizeof(pgd_t))-------------2048*8B=16KB
/* * Reserve the special regions of memory */ void __init arm_mm_memblock_reserve(void) { /* * Reserve the page tables. These are already in use, * and can only be in node 0. */ memblock_reserve(__pa(swapper_pg_dir), SWAPPER_PG_DIR_SIZE); ... }
1.4 設置PTE表項
要理解是如何設置PTE表項,就需要參照B3.3.1 Translation table entry formants中關於Second-level descriptors的描述。
arch\arm\include\asm\pgtable-2level.h: #define set_pte_ext(ptep,pte,ext) cpu_set_pte_ext(ptep,pte,ext) arch\arm\include\asm\glue-proc.h: #ifndef MULTI_CPU ... #define cpu_set_pte_ext __glue(CPU_NAME,_set_pte_ext) ...
#endif arch\arm\mm\proc-v7-2level.S: /* * cpu_v7_set_pte_ext(ptep, pte) * * Set a level 2 translation table entry. * * - ptep - pointer to level 2 translation table entry----------放入r0 * (hardware version is stored at +2048 bytes) * - pte - PTE value to store----------------------------------放入r1 * - ext - value for extended PTE bits------------------------放入r2 */ ENTRY(cpu_v7_set_pte_ext) #ifdef CONFIG_MMU str r1, [r0] @ linux version----------將r1的值存入r0地址的內存中 bic r3, r1, #0x000003f0--------------------------清除r1的bit[9:4],存入r3 bic r3, r3, #PTE_TYPE_MASK-----------------------PTE_TYPE_MASK為0x03,記清除低2位 orr r3, r3, r2-----------------------------------r3與r2或,存入r3 orr r3, r3, #PTE_EXT_AP0 | 2---------------------這里將bit1和bit4置位,所以是Small page。 tst r1, #1 << 4----------------------------------判斷r1的bit4是否為0 orrne r3, r3, #PTE_EXT_TEX(1)--------------------設置TEX為1 eor r1, r1, #L_PTE_DIRTY tst r1, #L_PTE_RDONLY | L_PTE_DIRTY orrne r3, r3, #PTE_EXT_APX-----------------------設置AP[2] tst r1, #L_PTE_USER orrne r3, r3, #PTE_EXT_AP1-----------------------設置AP[1] tst r1, #L_PTE_XN orrne r3, r3, #PTE_EXT_XN------------------------設置XN位 tst r1, #L_PTE_YOUNG tstne r1, #L_PTE_VALID eorne r1, r1, #L_PTE_NONE tstne r1, #L_PTE_NONE moveq r3, #0 ARM( str r3, [r0, #2048]! )---------------------並沒有寫入r0,而是寫入r0+2048Bytes的偏移。 THUMB( add r0, r0, #2048 ) THUMB( str r3, [r0] ) ALT_SMP(W(nop)) ALT_UP (mcr p15, 0, r0, c7, c10, 1) @ flush_pte #endif bx lr ENDPROC(cpu_v7_set_pte_ext)