2. Linux 内核内存管理基础(一):虚拟地址空间布局、页表与 MMU、物理内存与虚拟内存映射

内存访问越界是 Linux 内核开发中最常见、最难定位的 Crash 类型之一。要精准分析此类问题,必须深刻理解内核如何管理内存。本章从最基础的虚拟地址空间布局入手,逐步深入到页表转换机制与 MMU 硬件原理,为后续的 Crash 现场分析打下坚实的理论基础。

2.1 虚拟地址空间布局

在 64 位 Linux 系统中,虚拟地址空间被划分为用户态和内核态两大部分。这种划分是 CPU 硬件(MMU)和操作系统共同约定的结果,也是隔离用户程序与内核的关键屏障。

2.1.1 用户态与内核态空间划分

以 x86_64 架构为例,虚拟地址总宽度为 48 位(当前实现),地址空间范围为 0x0000 0000 0000 0000 至 0xFFFF FFFF FFFF FFFF。内核将高 128TB 地址空间留给自身,低 128TB 地址空间留给用户进程。

区域 地址范围 大小 主要用途
用户空间 0x0000 0000 0000 0000 ~ 0x0000 7FFF FFFF FFFF 128 TB 用户进程代码、堆、栈、数据段、mmap 映射区
内核空间 0xFFFF 8000 0000 0000 ~ 0xFFFF FFFF FFFF FFFF 128 TB 内核代码、直接映射区、vmalloc、模块、固定映射区
Crash 分析要点: 当 Crash 工具报告的指令指针(RIP)或访问地址落在 0xFFFF 8000 0000 0000 以上时,说明异常发生在内核态;若落在用户空间地址范围,则可能是用户态程序触发了系统调用或通过异常陷入内核。地址范围的判断是 Crash 分析的第一步。

2.1.2 内核空间内部布局

内核空间内部进一步细分为多个逻辑区域,每个区域有特定的映射属性和用途。理解这些区域有助于在 Crash 日志中快速定位内存访问的上下文。

Linux x86_64 内核虚拟地址空间布局 用户空间 (0x0000 0000 0000 0000 ~ 0x0000 7FFF FFFF FFFF) 直接物理内存映射区 (PAGE_OFFSET) — 线性映射 vmalloc 区域 — 非连续内存映射 模块映射区域 (MODULES_VADDR ~ MODULES_END) 固定映射区 (FIXADDR_START ~ FIXADDR_TOP) 内核栈、陷阱门、CPU 私有数据区 Crash 分析关键:访问地址落在哪个区域,决定了内存访问的类型和可能的原因 例如:直接映射区越界通常意味着 slab/slub 或伙伴系统问题;vmalloc 区越界常见于驱动内存分配错误

2.2 页表与 MMU

虚拟地址到物理地址的转换由 MMU(Memory Management Unit) 硬件完成,转换的依据是操作系统维护的 页表(Page Table)。页表是多级结构,x86_64 使用 4 级页表(未来可扩展至 5 级)。

2.2.1 多级页表结构

一个 48 位的虚拟地址被拆分为 5 个部分,用于索引不同级别的页表项:

  • PGD(Page Global Directory):顶级页表,每个进程一个,存储在 mm_struct->pgd 中。
  • P4D(Page 4th Directory):四级页表目录(4 级页表时与 PGD 合并)。
  • PUD(Page Upper Directory):三级页表目录。
  • PMD(Page Middle Directory):二级页表目录。
  • PTE(Page Table Entry):一级页表项,直接指向物理页帧。
虚拟地址位域 位范围 页表级别 索引大小
PGD 索引 bit 47:39 Page Global Directory 9 bits (512 项)
PUD 索引 bit 38:30 Page Upper Directory 9 bits (512 项)
PMD 索引 bit 29:21 Page Middle Directory 9 bits (512 项)
PTE 索引 bit 20:12 Page Table Entry 9 bits (512 项)
页内偏移 bit 11:0 物理页内偏移 12 bits (4KB 页)
x86_64 4 级页表地址转换流程 虚拟地址 (48 bits) PGD (9) PUD (9) PMD (9) PTE (9) 偏移 (12) PGD 表 PUD 表 PMD 表 PTE 表 物理页帧 物理地址 = 页帧基址 + 页内偏移

2.2.2 TLB 与缓存

每次内存访问都遍历 4 级页表会带来巨大的性能开销。MMU 内部集成了 TLB(Translation Lookaside Buffer),即页表缓存,用于缓存最近使用的虚拟地址到物理地址的映射关系。

  • TLB 命中:直接获得物理地址,无需遍历页表。
  • TLB 未命中:硬件遍历页表,更新 TLB,然后重试访问。
  • TLB 刷新的触发场景:进程切换(CR3 寄存器写入)、内核修改页表(如 set_pte_at)、内存回收、大页映射等。
Crash 分析陷阱: 某些内存访问越界 Crash 的现场地址看起来“合理”,但实际访问的是过时的 TLB 映射。例如,释放物理页后未正确刷新 TLB,导致后续访问仍然命中旧的 TLB 项,访问到已被重新分配的内存。这种问题在 Crash 日志中通常表现为“访问了已释放的内存”,但地址转换后指向的物理页已被其他模块使用。

2.3 物理内存与虚拟内存映射

物理内存是有限的硬件资源,虚拟内存通过页表机制为每个进程提供独立的、连续的地址空间。内核需要管理物理内存的分配与回收,并建立虚拟地址到物理地址的映射关系。

2.3.1 直接映射区(线性映射)

内核将大部分物理内存直接映射到内核虚拟地址空间的高位区域,称为 直接映射区 或 线性映射区。在 x86_64 上,该区域起始地址为 PAGE_OFFSET(通常为 0xFFFF 8800 0000 0000)。

  • 映射关系:虚拟地址 = PAGE_OFFSET + 物理地址(或减去一个固定偏移)。
  • 优点:转换速度快,只需一次加减运算,无需遍历页表(但页表仍需存在以支持 MMU 权限检查)。
  • 缺点:占用连续的虚拟地址空间,无法映射超过虚拟地址空间大小的物理内存。

2.3.2 vmalloc 映射区

对于需要非连续物理内存的场景(如大型内核模块、动态分配的大块内存),内核使用 vmalloc 机制。vmalloc 在虚拟地址空间中找到一段连续的区域,然后将不连续的物理页帧映射到该区域。

  • 特点:虚拟地址连续,物理地址不连续。
  • 性能:每次访问都需要遍历页表,TLB 命中率较低。
  • 典型用途:内核模块加载、某些驱动的大块 DMA 缓冲区。

2.3.3 物理内存管理单元

内核使用 伙伴系统(Buddy System) 管理物理页帧的分配与释放,使用 slab/slub 分配器 管理小对象的内存分配。这些分配器返回的物理内存,最终通过页表映射到内核或用户虚拟地址空间。

分配器 管理对象 典型大小 映射方式
伙伴系统 物理页帧(order 0 ~ 10) 4KB ~ 4MB 直接映射区或 vmalloc
slab/slub 内核对象(如 task_struct, inode) 几十字节 ~ 几 KB 从伙伴系统分配的页帧中切分,映射到直接映射区
vmalloc 非连续物理内存块 任意大小 vmalloc 映射区
物理内存与虚拟内存映射关系 物理内存 页帧 0 (4KB) 页帧 1 (4KB) 页帧 2 (4KB) 页帧 3 (4KB) 虚拟地址空间 虚拟页 A (映射到页帧 0) 虚拟页 B (映射到页帧 1) 虚拟页 C (映射到页帧 2) 虚拟页 D (映射到页帧 3) 页表 虚拟页 A → 页帧 0 虚拟页 B → 页帧 1 虚拟页 C → 页帧 2 虚拟页 D → 页帧 3 Crash 分析关键:访问越界时,需要区分是虚拟地址越界还是物理地址越界 虚拟地址越界:访问了未映射的虚拟页(页表项无效)→ 触发缺页异常(Page Fault)

2.4 本章小结

  • 虚拟地址空间布局:用户态与内核态严格隔离,内核空间内部划分为直接映射区、vmalloc 区、模块区、固定映射区等,每个区域有特定的用途和访问属性。
  • 页表与 MMU:x86_64 使用 4 级页表完成虚拟地址到物理地址的转换,TLB 缓存加速地址转换,页表刷新是内存管理的关键操作。
  • 物理内存与虚拟内存映射:直接映射区提供快速线性映射,vmalloc 提供非连续物理内存的虚拟连续映射,伙伴系统和 slab/slub 分配器管理物理内存的分配与释放。
Crash 分析实战预备: 在后续章节中,我们将看到大量 Crash 现场与本章知识点直接相关。例如,访问越界导致访问了未映射的虚拟地址(触发缺页异常),或者访问了直接映射区中已释放的物理页(触发 slab 越界检测)。掌握本章内容是理解这些 Crash 的根本前提。