首页 > 代码库 > linux内核探索之内存管理(三):页表
linux内核探索之内存管理(三):页表
主要参考《深入Linux内核架构》、《深入理解Linux内核》及内核linux-3.18.3
页表用于建立用户进程的虚拟地址空间和系统物理内存(内存、页帧)之间的映射。IA-32系统默认使用两级分页系统,但是内核中总是使用四级页表,第三和第四级页表由特定于体系结构的代码模拟。
页表管理分为两个部分,第一部分依赖于体系结构,第二部分体系结构无关。但是所有的数据结构和操作数据结构的几乎所有函数都定义在特定于体系结构的文件中。这些数据结构和函数通常在include/asm-arch/page.h和include/asm-arch/pgtable.h中找到,简称为pagh.h和pgtable.h。但IA-32和AMD64平台的文件为:arch/asm-x86/page_32.h、arch/asm-x86/page_64.h。
数据结构
1. 内存地址的分解
根据四级页表的需要,虚拟内存地址分为5部分(4个表项用于选择页,1个索引表示页内位置)。各体系结构不仅地址子长不同,而且地址字拆分方式也不同,内核定义了宏,用于将地址分解为各分量。
更直观描述:
每一个进程有自己的页全局目录和自己的页表集。当发生进程切换时,linux就把CR3控制寄存器的内容保存在当前一个执行进程的描述符中。然后把下一个要执行的描述符的值装入CR3寄存器中,当心进程重新开始在CPU上执行时,分页单元可以指向正确的页表。
BIT_PER_LONG:表示用于unsigned long 变量的比特位的数目
PAGE_SHIFT:表示每个指针末端的几个比特位,用于指定所选页帧内部的位置
PMD_SHIFT:指定页内偏移量和最后一级页表所需比特位的总数。该值减去PAGE_SHIFT可得最后一级索引所需比特位的数目。该值表明了一个中间层页表项管理的部分地址空间的大小,为2^PMD_SHIFT字节。
PUD_SHIFT:由PMD_SHIFT加上中间层页表索引所需的比特位长度。
PGDIR_SHIFT则由PUD_SHIFT加上上层页表索引所需的比特位长度。对全局目录中的一项所能寻址的部分地址空间长度计算以2为底的对数,即PGDIR_SHIFT。
在各级页没目录/页表中所能存储的指针数目,可以通过宏定义确定。PTRS_PER_PGD指定了全局页目录中项的数目。PTRS_PER_PMD对应于中间页目录,PTRS_PER_PUD对应于上层页目录中项的数目,PTRS_PER_PTE则是页表中项的数目。两级页表的体系结构中会将PTRS_PER_PMD和PTRS_PER_PUD定义为1,这使得内核的剩余部分感觉该体系结构也提供了四级页转换结构,尽管实际上只有两级页表。中间层页目录和上层页目录实际上被消去。
相关头文件:
Include/asm-generic/pgtable-nopud.h用来提供模拟上层页目录所需的所有声明;
Include/asm-generic/pgtable-nopmd.h用于在只有二级地址转换的系统上模拟中间层页表。
n比特位长的地址可寻址的地址区域长度为2^n字节。内核定义了额外的宏变量保存计算得到的值,以避免多次重复计算。相关的宏定义如下:
include/asm-generic/page.h: #define PAGE_SHIFT 12 #ifdef __ASSEMBLY__ #define PAGE_SIZE (1 << PAGE_SHIFT) #else #define PAGE_SIZE (1UL << PAGE_SHIFT) #endif #define PAGE_MASK (~(PAGE_SIZE-1)) include/asm-generic/pgtable-nopud.h: #define PUD_SHIFT PGDIR_SHIFT #define PTRS_PER_PUD 1 #define PUD_SIZE (1UL << PUD_SHIFT) include/asm-generic/pgtable-nopmd.h: #define PMD_SHIFT PUD_SHIFT #define PTRS_PER_PMD 1 #define PMD_SIZE (1UL << PMD_SHIFT) #define PMD_MASK (~(PMD_SIZE-1)) ./include/asm/pgtable_32_types.h: #define PGDIR_SIZE (1UL << PGDIR_SHIFT) ./include/asm/pgtable-2level_types.h: #define PGDIR_SHIFT 22 #define PTRS_PER_PGD 1024 ./include/asm/pgtable-3level_types.h: #define PGDIR_SHIFT 30 #define PTRS_PER_PGD 4 ./include/asm/pgtable_64_types.h: #define PGDIR_SHIFT 39 #define PTRS_PER_PGD 512
PTR_PER_XXX指定了给定目录项能够代表多少指针。由于x64对每个页表索引使用9个比特位,所以每个页表可容纳2^9=512个指针。
内核也需要一种方法从给定地址中提取各个分量。内核使用如下定义的位掩码来完成该工作。
#define PAGE_MASK (~(PAGE_SIZE-1)) #define PUD_MASK (~(PUD_SIZE-1)) #define PMD_MASK (~(PMD_SIZE-1)) #define PUD_MASK PGDIR_MASK
2. 页表的格式
上述定义已经确立了页表项的数目,但是没有定义其结构。内核提供了4个数据结构(pagh.h)来表示页表项的结构:
Pgd_t 用于全局页目录项
Pud_t 用于上层页目录项
Pmd_t用于中间页目录项
Pte_t 用于直接页表项
include/asm-generic/page.h: typedef struct { unsigned long pte; } pte_t; typedef struct { unsigned long pmd[16]; } pmd_t; typedef struct { unsigned long pgd; } pgd_t; typedef struct { unsigned long pgprot; } pgprot_t; typedef struct page *pgtable_t;
用于分析页表项的函数:
函数 | 描述 |
include/asm-generic/page.h: #define pte_val(x) ((x).pte) #define pmd_val(x) ((&x)->pmd[0]) #define pgd_val(x) ((x).pgd) #define pgprot_val(x) ((x).pgprot) | 将pte_t等类型的变量转换为unsigned long整数 |
include/asm-generic/page.h: #define __pte(x) ((pte_t) { (x) } ) #define __pmd(x) ((pmd_t) { (x) } ) #define __pgd(x) ((pgd_t) { (x) } ) #define __pgprot(x) ((pgprot_t) { (x) } ) | Pgd_val等函数的逆,将unsigned long型的整数转换为pgd_t等类型的变量 |
arch/x86/include/asm/pgtable.h: #define pgd_index(address) (((address) >> PGDIR_SHIFT) & (PTRS_PER_PGD - 1)) static inline unsigned long pud_index(unsigned long address) static inline unsigned long pte_index(unsigned long address) static inline unsigned long pmd_index(unsigned long address) | 从内存指针和页表项获得下一级页表的地址 |
static inline int pgd_present(pgd_t pgd) static inline int pte_present(pte_t a) static inline int pmd_present(pmd_t pmd) static inline int pud_present(pud_t pud) | 检查对应项的_PRESENT位是否设置。如果该项对应的页表或页在内存中,则会置位 |
static inline int pgd_none(pgd_t pgd) static inline int pte_none(pte_t pte) static inline int pmd_none(pmd_t pmd) static inline int pud_none(pud_t pud) | 对_present函数的值逻辑去翻。如果返回true,则检查的页不在内存中 |
#define pgd_clear(pgd) native_pgd_clear(pgd) #define pud_clear(pud) native_pud_clear(pud) #define pte_clear(mm, addr, ptep) native_pte_clear(mm, addr, ptep) #define pmd_clear(pmd) native_pmd_clear(pmd) | 删除传递的页表项,通常是将其设置为零 |
static inline int pmd_bad(pmd_t pmd) static inline int pud_bad(pud_t pud) static inline int pgd_bad(pgd_t pgd) static inline int pmd_bad(pmd_t pmd) | 检查中间层页表、上层页表、全局页表的项是否无效。如果函数从外部接收输入参数,则无法假定参数是有效的,为保证安全性,可以调用这些函数进行检查 |
#define pud_page(pud) pfn_to_page(pud_val(pud) >> PAGE_SHIFT) #define pgd_page(pgd) pfn_to_page(pgd_val(pgd) >> PAGE_SHIFT) #define pte_page(pte) pfn_to_page(pte_pfn(pte)) | 返回保存页数据的page结构或中间页目录的项 |
static inline pmd_t *pmd_offset(pud_t *pud, unsigned long address) static inline pud_t *pud_offset(pgd_t *pgd, unsigned long address) #define pgd_offset(mm, address) ((mm)->pgd + pgd_index((address))) #define pgd_offset_k(address) pgd_offset(&init_mm, (address)) | 接收内存描述地址,和线性地址address,该宏产生地址addr在相应表项的线性地址或目录项的地址 |
未完待续:
特定于PTE(page table的信息)
linux内核探索之内存管理(三):页表