首页 > 代码库 > Linux 内存分页
Linux 内存分页
从 Linux 2.6.11 开始,内核使用了独立于硬件架构的四级页表。但支持几级页表应该是硬件支持为标准,Linux 如何做到四级页表的呢?
下面看一段页表初始的代码就知道了。
PKMAP 固定内存部分的页表初始化
首先弄清,以下分析都是建立在配置了大于 1G 内存,并且未开启 PAE 情况下的 X86 架构的一些宏的值。
一些观点列出也都默认是以上条件下。
CallStack: page_table_range_init (arch\x86\mm\init_32.c) permanent_kmaps_init (arch\x86\mm\init_32.c) pagetable_init (arch\x86\mm\init_32.c) paging_init (arch\x86\mm\init_32.c) setup_arch (arch\x86\kernel\setup.c) start_kernel (init\main.c) startup_32 (head_32.S) static void __init page_table_range_init(unsigned long start, unsigned long end, pgd_t *pgd_base) { int pgd_idx, pmd_idx; unsigned long vaddr; pgd_t *pgd; pmd_t *pmd; vaddr = start; pgd_idx = pgd_index(vaddr); pmd_idx = pmd_index(vaddr); pgd = pgd_base + pgd_idx; for ( ; (pgd_idx < PTRS_PER_PGD) && (vaddr != end); pgd++, pgd_idx++) { pmd = one_md_table_init(pgd); pmd = pmd + pmd_index(vaddr); for (; (pmd_idx < PTRS_PER_PMD) && (vaddr != end); pmd++, pmd_idx++) { one_page_table_init(pmd); vaddr += PMD_SIZE; } pmd_idx = 0; } }
+------------------------------+ | PGD | PUD | PMD | PTE | PAGE | +------------------------------+
每一级对应在虚拟地址的偏移,加上掩码可以计算出相应级的值
PGDIR_SHIFT 22
PUD_SHIFT 22
PMD_SHIFT 22
PAGE_SHIFT 12
相应级对应的虚拟地址中的位数
PGD PUD, PMD PTE PAGE
10, 0, 0, 10, 12
每一级中包含的项数(也是每一级中索引的上限 [0, x))
PTRS_PER_PGD 1024 (2^10)
PTRS_PER_PUD 1 (2^0)
PTRS_PER_PMD 1 (2^0)
PTRS_PER_PTE 1024 (2^10)
比如 PGD 有 10 位,则它可以表示 2^10 个表项, PUD 对应 0 位,则说明页上层目录中只有一个目录项。
当要初始化一段虚拟地址对应的页表时,首先根据虚拟地址得到其对应的 PGD 数组的项,pgd_inex(vaddr) 就是做这个工作的, 它是一个宏,
#define pgd_index(address) (((address) >> PGDIR_SHIFT) & (PTRS_PER_PGD - 1))
这样就得到了该虚拟地址对应 PGD 中的项的索引,这样就得到了页全局目录项,该项中记录了页上级目录的物理地址。
那么问题来了,X86 只识别两级页表,而 Linux 代码中分布管理是以四级页表实现的,如何实现这一点呢,首先要明确,代码总是建立的硬件实现的基础上,所以说 Linux 的四级页表其实是虚拟的四级页表,也就是在代码实现上,好像是四级分布,其实,是用了四级分页的代码,来填充两级页表,看 pmd_index 的实现便知道了。
#define pmd_index(address) (((address) >> PMD_SHIFT) & (PTRS_PER_PMD - 1))
其实页目录的计算都是既定的这些公式,只是规定的位偏移和长度不同而已,取出 PMD 在虚拟地址中对应的值,然后根据该项的取值范围做掩码,就可以得到在该级中的项的偏移,由于 PTRS_PER_PMD 为 1,即该项的索引应该小于 1,即只能为 0,但知道了项的索引值也不足以明白如何构建的二级页表,那接着看下去。
当知道各级的项对应的索引后,就可以初始化整个页表了,首先外层循环填充页全局目录,然后填充每一个页全局目录项对应的 PUD, PMD, 由函数 one_md_table_init 来实现,传入页全局目录项的虚拟地址,返回页中间目录的虚拟地址,因为只相当于兼容了三级页表,所以 PUD 的初始化相当于省略掉了,它的实现只有两条语句,
pud = pud_offset(pgd, 0);
pmd_table = pmd_offset(pud, 0);
pud_offset 得到 pud 的偏移, #define pud_offset(pgd, start) (pgd), 它直接返回了 pgd 的地址,
pmd_offset 也同样返回了 pud 的虚拟地址。也就是 pgd 项的地址,然后函数返回。
然后开始初始化页中间目录,循环设置每一项,其实只有一项,把这些项设置为页表的值, one_page_table_init 来设置页表,首先申请一个页表,然后赋值给相应的 pmd 项,这样就依次把 page_table_range_init 传入的这段虚拟地址的页表给建立起来了。
总结一下,由于该架构只支持二级页表,所以在计算 PUD 和 PMD 时,都是返回的传入的上级目录项虚拟地址,也就是 PGD 的目录项虚拟地址。
按四级来分,示意图如下:
PGD PUD PMD PT +-----+ +-----+ +-----+ +-----+ | a |--->| a |-------------->| a |------------>| t0 | +-----+ +-----+ +-----+ +-----+ +-----+ +-----+ | b |------------>| b |-------------->| b | | t1 | +-----+ +-----+ +-----+ +-----+ +-----+ +-----+ | c |--->| c |-------------->| c | | t2 | +-----+ +-----+ +-----+ +-----+ | ... | | ... |
PUD 和 PMD 每个目录只有一项,并且值与 PGD 相应的表项相同。
由于通过 PGD 项得到 PUD 目录地址和通过 PUD 得到 PMD 基址时,返回的其实就是上一级目录项的地址,参见 pud_offset pmd_offset,即 PUD 其实就是 PGD 中的一项,PMD 就是 PUD 相对应的目录项,即 PGD 中的一项,那么最终形成的页表结构应该是:
PGD PT +-------------+ +-----+ +------+ |a(PUD)(PMD) |-------------->| t0 |-------->| Page | +-------------+ +-----+ +------+ | b | | t1 | +-------------+ +-----+ | c | | t2 | +-------------+ +-----+ | ... | | ... |
即全局页目录页,也是 PUD, 也是 PMD。
Linux 用宏配置,来实现了代码上的四级页表,但真实的两级页表。那么当真实是多级页表时,只需要配置相应级的偏移量(XXX_SHIFT)及对对应各级的目录项范围 (PTRS_PER_XXX) 就可以重用代码了。
Linux 内存分页