Linux采用了通用的四级分页机制,所谓通用就是指Linux使用这种分页机制管理所有架构的分页模型,即便某些架构并不支持四级分页。对于常见的x86架构,如果系统是32位,二级分页模型就可满足系统需求;如果32位系统采用PAE(物理地址扩展)模式,Linux使用三级分页模型;如果是64位系统,Linux使用四级分页模型,也就是说x86架构的分页模型可能是二级、三级或四级。
正如上面所说,Linux通用的四级分页机制均可以满足上述三种情况。本文将分析x86架构下与分页机制相关的一些宏和函数,来展现这种分页机制的通用型。
四级分页模型
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。
三级分页模型
Linux四级分页机制在x86-64架构下有完美的体现,因为每个页表实际上都是有效的。而在接下来要分析的使用PAE的x86-32架构中,页上级目录实际上是不存在的,但是Linux采用了一种有效的方法使三级分页模型仍然可以用四级分页模型来管理。
PGDIR_SHIFT及相关宏
在x86-32的PAE模式下,PGDIR_SHIFT的值为30,它包含offset的12位,Table字段的9位,Middle Dir字段的9位。通过PTRS_PER_PGD可知,页全局目录占线性地址最高的2位。
#define PGDIR_SHIFT 30 #define PTRS_PER_PGD 4
pgd_offset()
在此模式下,pgd_offset()的实现与四级分页机制相同。
PUD_SHIFT及相关宏
在三级分页模型中,页上级目录是不存在的,但是Linux仍然保留了它应有的位置。具体的做法是将PUD_SHIFT的值设为PGDIR_SHIFT,那么与此值有关的宏也就与页全局目录对应的宏相等。在这种模式下,页上级目录在线性地址占据的位数为0,因此页上级目录的表项仅有一项。
typedef struct { pgd_t pgd; } pud_t; #define PUD_SHIFT PGDIR_SHIFT #define PTRS_PER_PUD 1 #define PUD_SIZE (1UL << PUD_SHIFT) #define PUD_MASK (~(PUD_SIZE-1))
pud_offset()
pud_offset()本应该返回线性地址addrress在页上级目录对应的表项地址,由于三级分页模型中并不存在该页目录,因此pud_offset()直接返回address在页全局目录中表项的地址。这也正是Linux四级分页机制通用型的体现。
static inline pud_t * pud_offset(pgd_t * pgd, unsigned long address) { return (pud_t *)pgd; }
PMD_SHIFT及相关宏
在x86-32的PAE模式下,PMD_SHIFT的值与上述四级分页模型相同。因此与此值对应的PTRS_PER_PMD、PMD_SIZE和PMD_MASK都与四级分页模型中的值相等。
arch/x86/include/asm/pgtable-3level_types.h #define PMD_SHIFT 21 #define PTRS_PER_PMD 512
pmd_offset()
该函数与四级分页模型中的对应函数相同。
通过上面的分析可知,采用PAE的x86-32架构本质上采用了三级分页模型,但是Linux依然将它按照四级分页模型对待。因此线性地址依旧被划分为5部分,每部分占据2,0,9,9,12位。与此对应,每级页表中依次包含的表项个数为4,1,512,512。
二级分页模型
x86-32采用二级分页模型,在此分页模型中只包含页全局目录和页表,并不存在页中间目录和页上级目录。
PGDIR_SHIFT
在三级分页模型中,PGDIR_SHIFT占据22位,即0位页上级目录、0位页中间目录、10位页表和12位偏移。页全局目录同样占线性地址的10位,因此PTRS_PER_PGD为1024。
arch/x86/include/asm/pgtable-2level_types.h #define PGDIR_SHIFT 22 #define PTRS_PER_PGD 1024
pgd_offset()
实现方法和上述两种分页机制相同。
PMD_SHIFT和PUD_SHIFT及相关宏
在二级分页模型中,PMD_SHIFT和PUD_SHIFT最后都指向PGDIR_SHIFT,因此与该值相关的宏都与页全局目录对应的值相同。
include/asm-generic/pgtable-nopmd.h #define PUD_SHIFT PGDIR_SHIFT #define PTRS_PER_PUD 1 #define PUD_SIZE (1UL << PUD_SHIFT) #define PUD_MASK (~(PUD_SIZE-1)) typedef struct { pud_t pud; } pmd_t; #define PMD_SHIFT PUD_SHIFT #define PTRS_PER_PMD 1 #define PMD_SIZE (1UL << PMD_SHIFT) #define PMD_MASK (~(PMD_SIZE-1))
pmd_offset()和pud_offset()
这两个宏最终都是返回address在页全局目录中对应的表项。
static inline pmd_t * pmd_offset(pud_t * pud, unsigned long address) { return (pmd_t *)pud; } static inline pud_t * pud_offset(pgd_t * pgd, unsigned long address) { return (pud_t *)pgd; }
通过上面的分析可知,对于采用二级分页模型的x86-32架构,Linux仍然使用四级分页机制进行管理,只不过将页上级目录和页中间目录都等价于页全局目录。因此,线性地址被划分为五部分,每部分依次占据10,0,0,10,12。每级页表中包含的表项个数为1024,1,1,1024。
问个问题,那个
#define pgd_offset(mm, address) ((mm)->pgd + pgd_index((address)))
pgd不是指页全局目录的物理地址么,怎么返回是线性地址
[回复一下]
路人甲 回复:
2月 15th, 2019 at 18:35
@luowanqian, 我也觉得是物理地址, 用的时候再通过pgd_page_vaddr或其它函数转成线性地址
[回复一下]