首页 > 代码库 > 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 内存分页