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 中的内核映射。
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 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 的虚拟映射,是内存管理子系统的核心数据结构。
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 刷新开销是上下文切换的主要成本之一。