【问题标题】:Page table in Linux kernel space during boot引导期间 Linux 内核空间中的页表
【发布时间】:2013-05-22 09:42:22
【问题描述】:

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

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

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

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

  3. 开启页表前后的映射地址是否相同?

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

  4. 内核空间的页表是全局的,会被系统中的所有进程共享,包括用户进程?

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

【问题讨论】:

    标签: linux-kernel virtual-memory mmu page-tables


    【解决方案1】:

    以下讨论基于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 配置为起点内存)。所以在打开 MMU 之前,需要非常仔细地编写汇编代码,以确保在执行过程中不会出错。实际上使用了一种称为位置无关代码(PIC)的技术。

    为了进一步解释上面,我提取了几个汇编代码sn-ps:

    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 中进行初始映射。在函数__create_page_tables 中,只映射了很小一部分的地址空间。
    之后,在函数paging_init中建立了最终的页表,并且在这个函数中,映射了相当大一部分的地址空间。假设您只有 512M RAM,对于大多数常见配置,这 512M-RAM 将由内核代码逐段映射(1 段为 1MB)。如果您的 RAM 非常大(例如 2GB),则只有一部分 RAM 会被直接映射。 (我将在这里停止,因为关于问题2的细节太多了)

    1. 开启页表前后映射地址相同?

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

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

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

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

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

    【讨论】:

    • 我明白你的意思。感谢您的建议。这真的很有帮助:)
    【解决方案2】:

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

    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 运行之前,我们都熟悉的映射不需要到位。

    【讨论】:

    • 2.6.36 commit message,其中 ARM bootmem 遵循 x86 标准。
    • 嗨,天真的噪音。你写“链接==物理”,这意味着链接=逻辑地址?例如:物理地址 = 虚拟地址 +/- OFFSET .
    • 是的,System.map 中的内核地址是链接 地址或内核虚拟地址。您可以为某些平台指定要加载的物理地址。 link 是指您的 0xc0000000。此外,== 表示 MMU 映射;抱歉,这很神秘。
    • 在 ARM v5 中,转换表基址寄存器仅使用 14 -> 31 位。位 [0:13] 未使用,设置为 0。我混淆了为什么只有这 16 位,但它可以指向一级转换表的基地址吗?谢谢
    • 第一级表必须16k对齐;现在你知道为什么了。基本上是为了访问第一级表,硬件执行unsigned int ttb[va >> 20]; 参见ARM graphic 以获得视觉效果。
    猜你喜欢
    • 2019-03-24
    • 1970-01-01
    • 1970-01-01
    • 2016-02-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-12-02
    • 1970-01-01
    相关资源
    最近更新 更多