2. 虚拟地址空间布局:用户空间与内核空间划分

虚拟地址空间布局是操作系统内存管理的基石。CPU 在 MMU 开启后,所有程序指令访问的地址都是虚拟地址,必须经过页表转换为物理地址。现代 64 位架构(x86_64、ARM64)虽然理论支持 2^64 字节的地址范围,但硬件实现通常只使用低 48 位或 52 位,并将地址空间划分为两个主要区域:用户空间和内核空间。

2.1 用户空间与内核空间划分原理

划分的核心目的是隔离与保护。用户态程序不能直接访问内核态的数据结构,必须通过系统调用陷入内核。这种划分通过页表项中的特权级标志位(如 x86 的 R/W、U/S 位,ARM64 的 AP 位)实现。

关键概念: 在 Linux 中,虚拟地址的最高位(第 63 位)决定了地址所属空间。该位为 0 表示用户空间,为 1 表示内核空间。这种设计使得地址空间切换仅需改变页表基址寄存器(CR3/TTBR0_EL1),无需刷新 TLB 中的内核映射。
虚拟地址空间划分示意图(x86_64 典型布局) 用户空间 (User Space) 0x0000 0000 0000 0000 ~ 0x0000 7FFF FFFF FFFF 大小: 128 TB | 特权级: Ring 3 (EL0) ▲ 地址空间分界线 (Address Space Boundary) 内核空间 (Kernel Space) 0xFFFF 8000 0000 0000 ~ 0xFFFF FFFF FFFF FFFF 大小: 128 TB | 特权级: Ring 0 (EL1) 说明:x86_64 使用高 16 位全 1 表示内核空间,全 0 表示用户空间。中间区域(0x00008000~0xFFFF7FFF)为 non-canonical 空洞,不可访问。

2.2 x86_64 典型布局

x86_64 架构在 Linux 中采用 3:1 非对称划分,但实际可用地址受限于 48 位虚拟地址。内核占据高地址部分,用户空间占据低地址部分,中间存在巨大的 non-canonical 空洞。

2.2.1 地址空间常量

区域 起始地址 结束地址 大小 说明
用户空间 0x0000 0000 0000 0000 0x0000 7FFF FFFF FFFF 128 TB 进程私有,页表由 mm_struct 管理
内核空间 0xFFFF 8000 0000 0000 0xFFFF FFFF FFFF FFFF 128 TB 所有进程共享,页表由 init_mm 管理
non-canonical 空洞 0x0000 8000 0000 0000 0xFFFF 7FFF FFFF FFFF ~16M TB 硬件禁止访问,用于捕获指针错误

2.2.2 内核空间内部细分

Linux 将 128 TB 的内核空间进一步划分为多个逻辑区域,每个区域有特定的用途:

  • 直接映射区(Direct Mapping / 线性映射):物理内存直接映射到内核虚拟地址空间,偏移量固定(PAGE_OFFSET)。例如,物理地址 0x1000 映射到虚拟地址 0xFFFF 8880 0000 1000。这是内核访问物理内存最常用的方式。
  • vmalloc 区域:用于非连续内存分配,虚拟地址连续但物理地址不连续。起始地址为 VMALLOC_START。
  • 模块区域:用于加载内核模块(.ko 文件),起始地址为 MODULES_VADDR。
  • 固定映射区(Fixmap):用于需要固定虚拟地址的场合,如早期 I/O 映射、APIC 访问。
  • KASAN 影子内存:当启用 Kernel Address Sanitizer 时,用于检测内存错误。
x86_64 内核空间详细布局 内核虚拟地址空间 (128 TB) 直接映射区 (Direct Mapping) 物理内存线性映射,偏移固定 vmalloc 区域 非连续内存分配 模块区域 (Module Area) 加载 .ko 内核模块 固定映射区 (Fixmap) 固定虚拟地址,早期 I/O 映射 KASAN 影子内存 内存错误检测 (可选) 提示:各区域大小由内核编译时配置决定,可通过 /proc/meminfo 查看
查看当前布局: 在运行中的 x86_64 Linux 系统上,执行 cat /proc/meminfo 查看内存使用概况;执行 sudo cat /proc/1/maps 查看 init 进程的用户空间映射;内核空间布局可通过 dmesg | grep "Virtual kernel memory layout" 查看。

2.3 ARM64 典型布局

ARM64 架构的虚拟地址空间布局与 x86_64 类似,但具有更大的灵活性。ARMv8-A 架构支持 4KB、16KB 或 64KB 页,以及 3 级或 4 级页表,这直接影响地址空间的大小和布局。

2.3.1 地址空间配置

页大小 页表级数 虚拟地址位数 用户/内核空间大小 典型场景
4KB 4 级 48 位 256 TB / 256 TB 通用 Linux 发行版
16KB 4 级 48 位 256 TB / 256 TB 服务器、高性能计算
64KB 3 级 48 位 256 TB / 256 TB 大型数据库、特定硬件
4KB 5 级 (LVA) 52 位 4 PB / 4 PB ARMv8.2-LVA 扩展

2.3.2 ARM64 内核空间布局特点

ARM64 Linux 内核空间布局与 x86_64 有显著差异:

  • 线性映射区(Linear Mapping):物理内存直接映射,起始地址为 PAGE_OFFSET。在 ARM64 中,PAGE_OFFSET 通常为 0xFFFF 8000 0000 0000。
  • 模块区域:位于内核空间末尾,起始地址为 MODULES_VADDR,通常紧邻内核映像。
  • Fixmap 区域:用于固定映射,与 x86_64 类似。
  • PCI I/O 区域:用于映射 PCI 设备的 I/O 空间。
  • Vmemmap 区域:用于存储 struct page 的虚拟映射,是内存管理子系统的核心数据结构。
ARM64 虚拟地址空间布局 (4KB 页, 48 位) 用户空间 (256 TB) 0x0000 0000 0000 0000 ~ 0x0000 FFFF FFFF FFFF ▲ 地址空间分界线 内核空间 (256 TB) 线性映射区 (Linear Mapping) vmalloc / vmemmap / PCI I/O 区域 模块区域 / Fixmap / 内核映像 说明:ARM64 支持非对称划分(如 48 位用户 + 16 位内核),但 Linux 默认使用对称 256 TB 划分

2.4 x86_64 与 ARM64 布局对比

特性 x86_64 ARM64
虚拟地址位数 48 位(默认),57 位(5 级页表) 48 位(默认),52 位(LVA 扩展)
用户/内核空间大小 128 TB / 128 TB 256 TB / 256 TB(对称)
地址空间划分方式 高 16 位全 1 或全 0 高 16 位全 1 或全 0(类似 x86)
non-canonical 空洞 存在,约 16M TB 存在,但大小取决于地址位数
内核线性映射偏移 PAGE_OFFSET = 0xFFFF 8880 0000 0000 PAGE_OFFSET = 0xFFFF 8000 0000 0000
模块区域位置 靠近内核空间顶部 靠近内核空间顶部
页表基址寄存器 CR3 TTBR0_EL1(用户)/ TTBR1_EL1(内核)
架构差异陷阱: ARM64 使用两个独立的页表基址寄存器(TTBR0_EL1 和 TTBR1_EL1),分别用于用户空间和内核空间。这意味着 ARM64 在进程切换时只需切换 TTBR0_EL1,而 TTBR1_EL1 保持不变,从而避免了 TLB 刷新开销。x86_64 则使用单个 CR3 寄存器,切换进程时必须刷新整个 TLB(除非启用 PCID 特性)。

2.5 布局对页表分配的影响

虚拟地址空间布局直接决定了页表的层级结构和分配策略:

  • 用户空间页表:每个进程独立,由 fork() 时复制或写时复制(COW)创建。页表分配通常从 kmem_cache 或 memblock 分配。
  • 内核空间页表:所有进程共享,由 init_mm 管理。内核页表在系统启动时由 paging_init() 建立,后续通过 vmalloc()、ioremap() 等接口动态扩展。
  • 页表分配器:Linux 使用 pgd_alloc()、pud_alloc()、pmd_alloc()、pte_alloc() 等函数按需分配页表。这些函数内部调用 __get_free_page() 或 alloc_pages() 获取物理页框。
性能关键点: 内核空间页表在进程切换时不需要切换(ARM64 天然支持,x86_64 通过 PCID 优化),因此内核页表的 TLB 命中率通常很高。用户空间页表则随进程切换而切换,TLB 刷新开销是上下文切换的主要成本之一。