在讲解 MMU 处理逻辑前,先理清两个基础概念:
- 直接映射区:内核将物理内存的前半部分(通常是
0~896MB,即ZONE_DMA+ZONE_NORMAL)直接映射到虚拟地址空间的PAGE_OFFSET(x86 下是0xC0000000,ARM64 下是0xFFFF000000000000)开始的区域,物理地址和虚拟地址满足虚拟地址 = 物理地址 + PAGE_OFFSET。 - MMU 的核心作用:硬件层面完成虚拟地址 → 物理地址的转换,依赖页表(Page Table)实现,直接映射区的核心是让 MMU 能通过 “固定偏移” 的页表快速完成地址转换。
内核启动:构建直接映射区的页表
内核在start_kernel()之前的汇编 / 初始化阶段,会为直接映射区构建多级页表(x86_64 是 4 级:P4D→PUD→PMD→PTE;ARM64 是 4/5 级);
关键细节:
- 直接映射区的页表项(PTE)是静态构建的,物理地址和虚拟地址的偏移是固定值(
PAGE_OFFSET),因此页表项的物理地址部分只需 “物理页框号(PFN)” 即可,无需动态计算。 - 页表项的属性位(MMU 关注的核心):
PTE_P(Present):标记页有效,MMU 可访问;PTE_RW(Read/Write):内核态可读写;PTE_US(User/Supervisor):仅内核态可访问(用户态不可见);PTE_PCD/PTE_PWT:缓存属性(直接映射区默认开启缓存,提升访问效率)。
MMU 必须遍历页表(硬件层面的硬性规则)
MMU 是硬件单元,它的核心工作机制就是通过遍历多级页表完成虚拟地址到物理地址的转换,这是 x86/ARM64 等架构的硬性规则,不会因为 “地址有固定偏移” 而改变:
- 即使你知道直接映射区的 VA 和 PA 满足
PA = VA - PAGE_OFFSET(软件层面的计算),但 MMU 硬件无法识别这个逻辑,它只能严格按照页表的索引规则去查找物理地址。 - 举个通俗例子:你知道 “100 号房间 = 1 号楼层 + 99”,但酒店的门禁系统(MMU)不会认这个公式,必须刷房卡(查页表)才能开门。
为什么 “看起来像没遍历页表”?
你之所以会有这个疑问,本质是内核做了两层关键优化,让 “页表遍历” 的开销几乎可以忽略,甚至在软件层面能绕开遍历:
1. 硬件层面:TLB(快表)缓存页表转换结果
MMU 内置了 TLB(Translation Lookaside Buffer),这是页表转换结果的高速缓存:
- 第一次访问直接映射区的某个虚拟地址时,MMU 会遍历页表完成转换,并将 “VA→PA” 的映射结果缓存到 TLB 中。
- 后续访问同一段地址(或相邻地址)时,MMU 直接从 TLB 中读取转换结果,无需再次遍历页表(TLB 访问耗时仅 1~2 个时钟周期,远快于遍历多级页表)。
- 直接映射区是内核最常访问的内存区域,TLB 命中率极高,因此实际的页表遍历操作极少发生。
2. 软件层面:直接地址计算(绕开 MMU 遍历)
内核代码中,经常直接用__pa(virt_addr)和__va(phys_addr)宏计算地址,而不是依赖 MMU 的页表遍历:
// 示例:软件层面直接计算地址,无需触发MMU页表遍历 unsigned long virt_addr = (unsigned long)kmalloc(4096, GFP_KERNEL); phys_addr_t phys_addr = __pa(virt_addr); // 直接计算:phys_addr = virt_addr - PAGE_OFFSET // 反向计算 unsigned long virt_addr2 = __va(phys_addr); // virt_addr2 = phys_addr + PAGE_OFFSET- 这种方式仅适用于直接映射区(0~896MB 物理内存),因为只有这部分内存满足 “固定偏移” 规则。
- 对于高端内存(>896MB),内核无法直接计算,必须通过 Fixmap/kmap 等机制构建页表,让 MMU 遍历页表完成转换。
大页映射:减少页表遍历的层级
内核会将直接映射区的连续物理内存映射为大页(如 x86_64 的 2MB/1GB 页):
- 普通 4KB 页需要遍历 4 级页表(P4D→PUD→PMD→PTE);
- 1GB 大页只需遍历 1 级页表(P4D)即可找到物理地址,大幅减少 MMU 遍历的层级和耗时。
- 大页还能减少 TLB 缓存的条目数,提升 TLB 命中率(比如 1GB 大页只需 1 个 TLB 条目,而 4KB 页需要 262144 个)。
直观对比:直接映射区的地址转换流程
| 场景 | MMU 行为 | 开销 |
|---|---|---|
| 首次访问某虚拟地址 | 遍历多级页表 → 缓存到 TLB | 较高(单次) |
| 后续访问同虚拟地址 | 直接查 TLB → 无需遍历页表 | 极低 |
| 软件层面计算地址 | 完全绕开 MMU,直接通过偏移计算 | 无 |
| 大页映射的地址访问 | 遍历更少层级的页表 → 缓存到 TLB | 较低 |
关键宏 / 函数(辅助 MMU 处理直接映射区)
| 宏 / 函数 | 作用 |
|---|---|
__va(phys_addr) | 物理地址转直接映射区虚拟地址(VA = PA + PAGE_OFFSET) |
__pa(virt_addr) | 直接映射区虚拟地址转物理地址(PA = VA - PAGE_OFFSET) |
pgd_offset_k() | 获取内核虚拟地址对应的 PGD(页全局目录)项地址 |
flush_tlb_all() | 刷新 MMU 的 TLB(快表),确保页表修改生效 |
总结
- 硬件层面:MMU 访问直接映射区时必须遍历页表(这是 MMU 的核心工作机制),但 TLB 会缓存转换结果,让后续访问无需遍历。
- 软件层面:内核可通过
__pa/__va宏直接计算地址,绕开 MMU 的页表遍历(仅适用于直接映射区)。 - 优化手段:大页映射减少页表遍历层级、TLB 提升缓存命中率,让直接映射区的 MMU 处理开销几乎可以忽略。
- 内核通过大页、地址宏(
__va/__pa)等优化手段,降低 MMU 地址转换的开销,提升直接映射区的访问效率。 - 直接映射区的页表属性默认开启缓存、仅内核可访问,MMU 依赖页表项的权限位控制地址访问的合法性。