我对Linux内核中的页表管理感到困惑吗?
在Linux内核空间中,在打开页面表之前。内核将使用1-1映射机制在虚拟内存中运行。打开页表后,内核会查询页表以将虚拟地址转换为物理内存地址。 问题是:
此时,打开页面表后,内核空间仍为1GB(从0xC0000000 - 0xFFFFFFFF开始)?
在内核进程的页表中,只映射了0xC0000000 - 0xFFFFFFFF范围内的页表项(PTE)? PTE超出此范围将不会映射,因为内核代码永远不会跳转到那里?
打开页面表之前和之后的映射地址是否相同?
例如。在打开页表之前,虚拟地址0xC00000FF映射到物理地址0x000000FF,然后在打开页表后,上面的映射不会改变。虚拟地址0xC00000FF仍然映射到物理地址0x000000FF。不同的是,只有在打开页表后,CPU才会查询页表,将虚拟地址转换为以前无需执行的物理地址。
内核空间中的页表是全局的,将在系统中的所有进程(包括用户进程)之间共享吗?
此机制在x86 32位和ARM中相同?
答案 0 :(得分:14)
以下讨论基于32位ARM Linux,内核源代码版本为3.9
如果您完成设置初始页面表(稍后将被函数paging_init
覆盖)并打开MMU的过程,则可以解决所有问题。
当bootloader首次启动内核时,汇编函数stext
(在arch \ arm \ kernel \ head.s中)是第一个运行的函数。请注意,此时MMU尚未开启。
除此之外,此函数stext
完成的两个导入作业是:
paging_init
)在深入研究你的问题之前,了解一下是有益的:
现在棘手的部分:
第一招:使用与位置无关的代码。
汇编函数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之前设置初始页表时的相同映射。 更具体地说,运行内核代码的相同地址范围被映射两次。
为什么这样做? 请记住,在MMU打开之前,CPU发出的每个地址都是物理地址(从0x00000000开始),MMU打开后,CPU发出的每个地址都是虚拟地址(从0xC0000000开始)。 因为ARM是一个流水线结构,所以在MMU打开的那一刻,ARM的管道中仍然有指令使用在MMU打开之前由CPU生成的(物理)地址!为了避免这些指令被炸毁,必须设置相同的映射来满足它们。
现在回到你的问题:
答:我猜你的意思是打开MMU。答案是肯定的,内核空间是1GB(实际上它也占用了0xC0000000以下的几兆字节,但这不是我们感兴趣的)
- 此时,打开页面表后,内核空间仍为1GB(从0xC0000000 - 0xFFFFFFFF开始)?
醇>
- 在内核进程的页表中,只映射了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的细节太多了)
- 打开页面表之前和之后的映射地址是一样的吗?
醇>
答:我想我已经在第二个技巧的解释中回答了这个问题:在打开MMU之前设置初始页面时的相同映射。"
4。内核空间中的页表是全局的,将在其中共享 系统中的所有进程,包括用户进程?
答:是的,不是。是的,因为所有进程共享内核页表的相同副本(内容)(更高的1GB部分)。不,因为每个进程使用自己的16KB内存来存储内核页表(尽管每个进程的1GB部分的页表内容相同)。
5。这个机制在x86 32bit和ARM中是一样的吗?
不同的架构使用不同的机制
答案 1 :(得分:9)
当Linux启用MMU时,只需要映射内核空间的虚拟地址。这种情况在启动早期发生非常。此时,没有用户空间。 MMU可以将多个虚拟地址映射到相同的物理地址没有任何限制。因此,在启用MMU时,最简单的方法是为内核代码空间和映射virt==phys
或 0xC0000000 映射设置link==phys
映射。
- 打开页面表之前和之后的映射地址是一样的吗?
醇>
如果物理代码地址为 Oxff 且最终链接地址为 0xc00000FF ,则打开MMU时会出现重复映射。 0xff 和 0xc00000ff 都映射到同一物理页面。简单的jmp
(跳转)或b
(分支)将从一个地址空间移动到另一个地址空间。此时,当我们在最终目标地址执行时,virt==phys
映射可以被删除。
我认为以上应该通过 3 回答 1 点。基本上,引导页表不是最终页表。
4。内核空间中的页表是全局的,将在系统中的所有进程中共享,包括用户进程?
是的,这是一个带有 VIVT 缓存的巨大胜利,还有很多其他原因。
5。这个机制在x86 32bit和ARM中是一样的吗?
当然,潜在的机制是不同的。即使是这些家庭中的不同处理器,它们也不同; 486 vs P4 vs Amd-K6; ARM926 vs Cortex-A5 vs Cortex-A8等。但是,语义非常相似。
请参阅:Bootmem@lwn.net - 关于早期Linux内存阶段的文章。
根据版本的不同,内存池和页表映射在引导期间处于活动状态。在init
运行之前,我们熟悉的映射不需要就绪。