设置初始页表

开始在虚拟内存中执行之前,我们必须设置一个MMU转译表,将物理内存映射到虚拟内存。这个表通常称为“页表”,尽管初始的映射使用的是节,而不是页。ARM架构要求页表必须放在物理内存中的偶数16KB边界上。而页表的尺寸也永远是16KB,所以这个要求很合理。

初始页表的位置由一个名为 swapper_pg_dir 的符号定义,意为“交换页目录”,是内核对于初始页表的称呼。后来,该页表被一个更详细的页表替换(或者说交换,因此得名“交换”页目录)。

“页表”这个名字有点误导,因为其中的初始映射在ARM的术语中其实叫做“节”(p),而不是页。但页表这个词还是被模糊地用来指代“在引导时负责将物理地址转译成虚拟地址的那个东西”。

符号 swapper_pg_dir 定义为 KERNEL_RAM_VADDR - PG_DIR_SIZE 。我们来仔细看看。

如你所料,KERNEL_RAM_VADDR 正是内核在虚拟内存中的位置。它就是内核在编译期间被连接到的地址。

KERNEL_RAM_VADDR 的定义为 (PAGE_OFFSET + TEXT_OFFSET)。PAGE_OFFSET 可以是前面讨论过的Kconfig符号的四个位置之一,通常是 0xC0000000。TEXT_OFFSET通常为 0x8000,所以 KERNEL_RAM_VADDR 通常是 0xC0008000,但在某些虚拟内存分割方式或极端的 TEXT_OFFSET 设置下,它也可能是其他的值。TEXT_OFFSET 来自 arch/arm/Makefile中的 textof-y,通常是 0x8000,但在某些高通平台上可能是 0x00208000,在某些博通平台上可能是 0x00108000,所以 KERNEL_RAM_VADDR 可能是 0xC0208000 等。

我们可以确定的是,在连接内核时,这个地址传递给了连接器。检查ARM架构的连接器文件(arch/arm/kernel/vmlinux.lds.S)就可以看到,连接器确实指示了将其放在 . = PAGE_OFFSET + TEXT_OFFSET。内核被编译成在 KERNEL_RAM_VADDR 指定的地址执行,即使是我们现在分析的最早期的代码,也是按照位置无关的方式编写的。

06_页表 - 图1

TEXT_OFFSET是一个很小的区域,通常为32KB,在物理空间和虚拟空间中都位于内核RAM上方。图中内核在物理RAM中的位置0x10000000仅仅是一个例子,实际上可以是任意偶数的16MB边界。

注意内核上方的一小片空隙,最常见的位置是 0xC0000000-0xC0007FFF (TEXT_OFFSET最常见的32KB大小)。虽然它属于内核空间内存的一部分,但内核不会被放入这片内存中。它包含的是初始页表,也可能是引导程序提供的ATAGs和一些临时区域,如果内核位于 0x00000000 的情况下,这片区域还可以用于中断向量。这里的初始页表当然是供内核使用 的,但后来就被放弃了。

注意,我们给物理和虚拟内存都加上了同样的 TEXT_OFFSET 区域。这一步其实在解压缩代码的时候就完成了,解压缩的过程会把内核的第一个字节放到 PHYS_OFFSET +TEXT_OFFSET 处,这样内核位置在物理内存和虚拟内存的之间的线性差异可以一直使用32位字的几个高位比特表示,例如比特24~比特31(8比特),因此只需使用立即数算术运算,在指令中直接加入偏移量即可。从此时开始,内核RAM就必须位于可被16MB(0x01000000)整除的地址上了。

通过给 PAGE_OFFSET 加上 TEXT_OFFSET 的方法,我们在虚拟内存中找到了 swapper_pg_dir 的物理位置,然后后退 PG_DIR_SIZE。通常结果是 0xC0000000 + 0x8000 - 0x4000,所以初始页表将位于虚拟内存中的 0xC0004000处,相应的物理内存中的偏移量(用本例的 PHYS_OFFSET 0x10000000来计算的话)为 0x10004000。

06_页表 - 图2

在我们的例子中,swapper_pg_dir符号位于16KB处(0x4000字节),在物理内存中位于.text段之前。在使用传统的ARM MMU时,PHYS_OFFSET为0x10000000,TEXT_OFFSET为0x8000,所以swapper_pg_dir符号位于 0x10004000。

如果使用LAPE,那么页表 PG_DIR_SIZE 为 0x5000,这样就会得到 0xC0003000 的虚拟地址和 0x10003000 的物理地址。汇编宏 pgtbl 会为我们计算该地址:我们用 r8 中计算的物理地址和 TEXT_OFFSET,减去 PG_DIR_SIZE,就得到了初始转译表的物理地址。

初始转译表的构建方式很相似:首先用零填充页表,然后构建初始页表。这些操作位于符号 _createpage_tables 处。

ARM32页表格式:

ARM32上的页表布局由两到三层组成。在ARM文档中,两层页表称为“短格式”,而三层页表称为“长格式”。长格式是LPAE(大型物理地址扩展)的一个特性,顾名思义,它用来处理大型物理内存,最多可处理40位物理地址。

这些转译表能够以1MB的节(在LPAE中为2MB,不过暂时先不考虑)或16kB的页为单位对内存进行转译。初始页表使用节,所以初始页表中的内存转译仅限于1MB的节上。

这样可以简化初始映射的操作:节可以直接编码到页表的第一层。这样,一般情况下就不需要处理两层结构的复杂性,可以直接把机器当做只有一层1MB节表来处理。

但是如果在LPAE上运行,那就要处理额外的一层,所以这里要处理最初的两层而不是一层。这就是为何此处会有LPAE代码的原因。它的任务就是在2MB节转译表中插入一个64位的指针。

06_页表 - 图3

在传统MMU中,我们仅将MMU指向第一层页表(其中只有1MB的节),而对于LPAE,我们需要一个中间层来访问2MB的节。这些项称为节描述符。

Linux页表的术语

从这里开始,代码中开始包含一些三个字母的缩写。很难说在讨论初始页表时这些缩写是否有意义,但开发人员习惯使用这些缩写。

● PGD:page global directory,全局页目录。这个词指最上面的转译表,整个MMU遍历该表来解析节和页的起始点。在我们的例子中,它指的是物理内存中0x10004000的位置。在ARM32的世界中,我们还会将它写入特殊的 CP15 转译表寄存器(有时候叫做 TTBR0),来告诉MMU在何处寻找转译。如果使用LPAE,它的值就是 0x10003000,留出 0x1000 的空间和一个指向下一层(称为PMD)的64位指针。

● P4D:page 4th level directory(第四层页目录),PUD:page upper directory(上方页目录)是Linux VMM(虚拟内存管理器)中的概念,在ARM中没有使用,因为它用于处理四层或五层转译表,而我们仅使用了两层或三层。

● PMD:page middle directory(中层页目录),是第三层转译的名字,仅在LPAE中使用。这就是为何要给LPAE初始页表保留额外的 0x1000 字节的原因。对于传统的ARM MMU(非LPAE)而言,PMD和PGD是一样的。这就是为什么代码对于传统MMU和LPAE都引用了 PMD_ORDER。因为在Linux VMM的术语中,它被认为是“PTE:s正上方的表格的格式”,由于我们没有使用PTEs,而是使用了节映射,所以“PMD节表”就是映射的最终产物。

● PET:s,页表项,它将RAM从物理内存映射到虚拟内存。初始引导转译表不会使用它,而是使用节。

再次注意:对于传统ARM来说,MMU、PGD和PMD是同一个东西。对于LPAE而言,它们是两个不同的东西。有时候这被称为将PMD“折叠”到PGD中。由于我们也可以说“折叠”P4D和PUD,因此这些术语越发令人迷惑了。

如果你用过虚拟内存,你肯定会反复遇到上述术语。对于我们构建初始“页”的目的而言,完全不需要关心这些术语。我们要构建的只是一个由1MB大小的节组成的列表,负责将虚拟内存映射到物理内存。此时我们甚至都不会处理页,所以“页”这个术语都令人迷惑。