18

我对 Linux 内核的页表管理感到困惑?

在 Linux 内核空间中,在页表打开之前。内核将运行在具有 1-1 映射机制的虚拟内存中。打开页表后,内核会查询页表以将虚拟地址转换为物理内存地址。问题是:

  1. 这时候打开页表后内核空间还是1GB(从0xC0000000-0xFFFFFFFF)?

  2. 并且在内核进程的页表中,只映射了 0xC0000000 - 0xFFFFFFFF 范围内的页表条目(PTE)?超出此范围的 PTE 将不会被映射,因为内核代码永远不会跳转到那里?

  3. 打开页表前后的映射地址是一样的吗?

    例如。开启页表前,虚拟地址0xC00000FF映射到物理地址0x000000FF,开启页表后,上述映射不变。虚拟地址 0xC00000FF 仍然映射到物理地址 0x000000FF。不同的是,打开页表后,CPU 已经查阅页表,将虚拟地址转换为物理地址,而之前不需要这样做。

  4. 内核空间中的页表是全局的,将在系统中的所有进程(包括用户进程)之间共享?

  5. 这种机制在 x86 32bit 和 ARM 中是否相同?

4

2 回答 2

19

下面的讨论基于32位ARM Linux,内核源代码版本为3.9,
如果你通过设置初始页表(稍后将被函数覆盖paging_init)和转向的过程来解决你所有的问题在 MMU 上。

当内核首次由引导加载程序启动时,汇编函数stext(在 arch\arm\kernel\head.s 中)是第一个运行的函数。请注意,此时 MMU 尚未打开。

除其他外,此函数完成的两个导入工作stext是:

  • 创建初始页面表(稍后将被函数覆盖paging_init
  • 打开 MMU
  • 跳转到内核初始化代码的C部分并继续

在深入研究您的问题之前,了解以下内容是有益的:

  • 在MMU开启之前,CPU下发的每一个地址都是物理地址
  • MMU开启后,CPU下发的每一个地址都是虚拟地址
  • 在打开 MMU 之前应该设置一个正确的页表,否则你的代码将简单地“被吹走”
  • 按照惯例,Linux 内核使用较高的 1GB 虚拟地址部分,而用户空间使用较低的 3GB 部分

现在是棘手的部分:
第一个技巧:使用与位置无关的代码。汇编函数stext链接到地址“ PAGE_OFFSET + TEXT_OFFSET”(0xCxxxxxxx),这是一个虚拟地址,但是由于MMU还没有开启,所以汇编函数stext实际运行的地址是“ PHYS_OFFSET + TEXT_OFFSET”(实际值取决于你的实际硬件),这是一个物理地址。

所以,事情是这样的:函数程序stext“认为”它运行在像 0xCxxxxxxx 这样的地址,但实际上它运行在地址 (0x00000000 + some_offeset) 中(假设你的硬件将 0x00000000 配置为 RAM 的起点)。所以在打开 MMU 之前,需要非常仔细地编写汇编代码,以确保在执行过程中不会出错。事实上,使用了一种称为位置无关代码 (PIC) 的技术。

为了进一步解释上述内容,我提取了几个汇编代码片段:

ldr r13, =__mmap_switched    @ address to jump to after MMU has been enabled

b   __enable_mmu             @ jump to function "__enable_mmu" to turn on MMU

请注意,上面的“ldr”指令是一条伪指令,意思是“获取函数__mmap_switched的(虚拟)地址并将其放入r13”

函数 __enable_mmu 依次调用函数 __turn_mmu_on:(请注意,我从函数 __turn_mmu_on 中删除了几条指令,这些指令是函数的基本指令,但不符合我们的兴趣)

ENTRY(__turn_mmu_on)
    mcr p15, 0, r0, c1, c0, 0       @ write control reg to enable MMU====> This is where MMU is turned on, after this instruction, every address issued by CPU is "virtual address" which will be translated by MMU
    mov r3, r13   @ r13 stores the (virtual) address to jump to after MMU has been enabled, which is (0xC0000000 + some_offset)
    mov pc, r3    @ a long jump
ENDPROC(__turn_mmu_on)

第二个技巧:在打开 MMU 之前设置初始页表时的相同映射。更具体地说,内核代码运行的同一地址范围被映射两次。

  • 正如预期的那样,第一个映射将地址范围 0x00000000(同样,此地址取决于硬件配置)到(0x00000000 + 偏移量)映射到 0xCxxxxxxx 到(0xCxxxxxxx + 偏移量)
  • 有趣的是,第二个映射将地址范围 0x00000000 到(0x00000000 + 偏移量)映射到自身(即:0x00000000 -->(0x00000000 + 偏移量))

为什么这样做?请记住,在 MMU 开启之前,CPU 发出的每个地址都是物理地址(从 0x00000000 开始),而在 MMU 开启之后,CPU 发出的每个地址都是虚拟地址(从 0xC0000000 开始)。
因为ARM是流水线结构,所以在MMU开启的那一刻,ARM的管道中仍有指令在使用MMU开启前CPU产生的(物理)地址!为了避免这些指令被炸毁,必须设置一个相同的映射来满足它们。

现在回到你的问题:

  1. 这时候打开页表后内核空间还是1GB(从0xC0000000-0xFFFFFFFF)?

A:我猜你的意思是打开MMU。答案是肯定的,内核空间是1GB(实际上它也占用了0xC0000000以下的几兆字节,但这不是我们感兴趣的)

  1. 并且在内核进程的页表中,只映射了 0xC0000000 - 0xFFFFFFFF 范围内的页表条目(PTE)?超出此范围的 PTE 将不会被映射,因为内核代码永远不会跳转到那里?

答:虽然这个问题的答案相当复杂,因为它涉及到很多关于特定内核配置的细节。
要完全回答这个问题,您需要阅读内核源代码中设置初始页表的部分(汇编函数__create_page_tables)和设置最终页表的函数(C 函数 paging_init)。
简单来说,ARM中有两级页表,第一级页表是PGD,占用16KB。内核在初始化过程中首先将该 PGD 清零,并在汇编函数中进行初始映射__create_page_tables。在 function__create_page_tables中,只有很小一部分的地址空间被映射。
之后,在函数中建立最终的页表paging_init,并且在这个函数中,映射了相当大一部分的地址空间。假设您只有 512M RAM,对于大多数常见配置,这 512M-RAM 将由内核代码逐段映射(1 段为 1MB)。如果您的 RAM 非常大(例如 2GB),则只有一部分 RAM 会被直接映射。(我将在这里停止,因为关于问题 2 的细节太多了)

  1. 打开页表前后的映射地址是一样的吗?

A:我想我在“第二招:在打开MMU之前设置初始页表时的相同映射”的解释中已经回答了这个问题。

4. 内核空间中的页表是全局的,将在系统中的所有进程(包括用户进程)之间共享?

答:是的,不是的。是的,因为所有进程共享内核页表的相同副本(内容)(较高的 1GB 部分)。不,因为每个进程都使用自己的 16KB 内存来存储内核页表(尽管更高 1GB 部分的页表内容对于每个进程都是相同的)。

5. 这种机制在 x86 32bit 和 ARM 中是否相同?

不同的架构使用不同的机制

于 2014-12-03T07:50:36.963 回答
11

Linux启用MMU时,只需要映射内核空间的虚拟地址即可。这发生在启动早期。此时,没有用户空间MMU 可以将多个虚拟地址映射到同一个物理地址没有任何限制。因此,在启用 MMU 时,最简单的方法是virt==phys对内核代码空间和映射link==phys0xC0000000映射进行映射。

  1. 打开页表前后的映射地址是一样的吗?

如果物理代码地址是Oxff并且最终链接地址是0xc00000FF,那么我们在打开 MMU 时会出现重复映射。0xff0xc00000ff都映射到同一个物理页面。一个简单的jmp(跳转)或b(分支)将从一个地址空间移动到另一个地址空间。此时,virt==phys可以在我们在最终目标地址处执行时删除映射。

我认为以上应该回答点13。基本上,引导页表不是最终页表。

4. 内核空间中的页表是全局的,将在系统中的所有进程(包括用户进程)之间共享?

是的,这是VIVT缓存的一大胜利,还有许多其他原因。

5. 这个机制在 x86 32bit 和 ARM 中是一样的吗?

当然,底层机制是不同的。即使对于这些系列中的不同处理器,它们也是不同的;486 vs P4 vs Amd-K6;ARM926 vs Cortex-A5 vs Cortex-A8 等。但是,语义非常相似。

请参阅:Bootmem@lwn.net - 有关早期 Linux 内存阶段的文章。

根据版本,不同的内存池页表映射在引导期间处于活动状态。我们都熟悉的映射在运行之前不需要到位init

于 2013-05-22T19:01:47.893 回答