2. 虚拟地址空间布局
在 x86_64 架构下,虚拟地址空间被划分为用户空间与内核空间两大部分。这种划分是现代操作系统实现进程隔离与内核保护的基础。每个进程拥有独立的虚拟地址空间,而内核则映射到所有进程的高地址区域。
2.1 用户空间与内核空间的基本划分
虚拟地址空间的高位部分由内核使用,低位部分由用户进程使用。划分的边界由内核配置决定,通常以 PAGE_OFFSET 宏定义。在 x86_64 架构中,典型的划分比例为:用户空间占低 47 位(约 128TB),内核空间占高 47 位(约 128TB),中间保留巨大的非规范区域(non-canonical hole)。
2.2 x86_64 架构下的地址空间布局
x86_64 架构使用 48 位虚拟地址,但通过符号扩展(sign-extension)形成 64 位地址。有效地址范围分为两个区域:低 128TB(用户空间)和高 128TB(内核空间)。中间的非规范区域(0x0000800000000000 – 0xFFFF7FFFFFFFFFFF)在硬件层面被视为非法地址,任何访问都会引发异常。
下表列出了 x86_64 架构下虚拟地址空间的主要区域及其用途:
| 地址范围 | 大小 | 区域名称 | 用途 |
|---|---|---|---|
| 0x0000000000000000 – 0x00007FFFFFFFFFFF | 128 TB | 用户空间 | 用户进程代码、堆、栈、数据段、mmap 映射 |
| 0x0000800000000000 – 0xFFFF7FFFFFFFFFFF | ~16M TB | 非规范区域 | 硬件禁止访问,用于捕获指针错误 |
| 0xFFFF800000000000 – 0xFFFF87FFFFFFFFFF | 8 TB | 内核空间(低端) | 直接映射物理内存(线性映射) |
| 0xFFFF880000000000 – 0xFFFFC7FFFFFFFFFF | 64 TB | vmalloc 区域 | 非连续内存分配、模块加载 |
| 0xFFFFC80000000000 – 0xFFFFE8FFFFFFFFFF | 32 TB | 内核高内存映射 | 固定映射、持久内核映射、临时映射 |
| 0xFFFFE90000000000 – 0xFFFFFEFFFFFFFFFF | 56 TB | 内核栈、模块、fixmap | 每个 CPU 栈、内核模块、固定映射页表 |
| 0xFFFFFF0000000000 – 0xFFFFFFFFFFFFFFFF | 4 TB | 内核页表与保留 | 页表自映射、内核代码段、数据段 |
下面通过一张 SVG 图展示 x86_64 虚拟地址空间的整体布局:
2.3 内核高内存映射
在 x86_64 架构中,由于物理内存可能远大于内核直接映射区域(8TB),内核需要额外的机制来访问超出直接映射范围的物理内存。这就是内核高内存映射(High Memory Mapping)的由来。虽然 x86_64 的 8TB 直接映射已经覆盖了绝大多数服务器物理内存,但在某些极端场景(如配备大量内存的 NUMA 系统)下,高内存映射仍然必要。
内核高内存映射主要包含以下三种映射方式:
- 固定映射(Fixed Mapping, fixmap): 在编译时确定虚拟地址,用于访问特定物理地址(如 I/O 寄存器、APIC)。这些映射在系统启动时建立,运行期间不会改变。
- 持久内核映射(Persistent Kernel Mapping, kmap): 用于将高内存物理页框长期映射到内核虚拟地址空间。通过
kmap()函数建立,通过kunmap()解除。适用于需要频繁访问的高内存页。 - 临时内核映射(Temporary Kernel Mapping, kmap_atomic): 用于短时间、原子操作的高内存访问。映射建立后不能被阻塞或睡眠,适用于中断上下文或临界区。通过
kmap_atomic()建立,kunmap_atomic()解除。
下表总结了三种高内存映射方式的关键差异:
| 映射类型 | 建立函数 | 解除函数 | 可睡眠 | 适用场景 | 性能 |
|---|---|---|---|---|---|
| 固定映射 (fixmap) | 编译时 / 启动时 | 无(永久存在) | — | I/O 寄存器、APIC、早期页表 | 极高(无运行时开销) |
| 持久内核映射 (kmap) | kmap() |
kunmap() |
是 | 文件系统、块设备、长时间访问 | 中等(需锁保护) |
| 临时内核映射 (kmap_atomic) | kmap_atomic() |
kunmap_atomic() |
否 | 中断处理、临界区、短时访问 | 高(无锁、无阻塞) |
下面通过一张 SVG 图展示内核高内存映射的架构与数据流:
2.4 用户空间布局细节
用户空间的布局由内核的 mm_struct 管理,每个进程拥有独立的布局。典型的用户空间地址从低到高依次为:
- 代码段(Text Segment): 从
0x400000开始(可配置),包含可执行指令,只读。 - 数据段(Data Segment): 包含已初始化全局变量和静态变量,可读写。
- BSS 段: 未初始化全局变量,在加载时清零。
- 堆(Heap): 通过
brk()/sbrk()系统调用扩展,向上增长。 - 内存映射区域(mmap): 通过
mmap()系统调用映射文件或匿名内存,通常从高地址向下增长。 - 栈(Stack): 从高地址向下增长,每个线程拥有独立栈。
- 环境变量与参数: 位于栈的顶部。
/proc/sys/kernel/randomize_va_space 控制 ASLR 级别(0=关闭,1=部分随机化,2=完全随机化)。
下面通过一张 SVG 图展示用户空间的典型布局:
2.5 内核空间布局细节
内核空间的布局在 x86_64 架构下由 arch/x86/include/asm/page_64_types.h 和 arch/x86/mm/init_64.c 定义。主要区域包括:
- 直接物理映射区域(Linear Mapping): 从
PAGE_OFFSET开始,将物理内存直接映射到内核虚拟地址空间。映射公式为:虚拟地址 = 物理地址 + PAGE_OFFSET。该区域用于内核访问大部分物理内存。 - vmalloc 区域: 用于分配非连续物理内存的虚拟地址空间。内核模块、某些驱动程序使用此区域。
- 持久内核映射区域(PKMAP): 用于
kmap()函数映射高内存页框。 - 固定映射区域(FIXMAP): 用于编译时确定的特殊映射,如 I/O 端口、APIC、早期页表。
- 内核代码段与数据段: 包含内核本身的代码、全局变量、静态变量。
- 模块区域: 用于加载内核模块(动态可加载内核模块)。
struct vm_struct 管理 vmalloc 区域,使用 struct fixmap 管理固定映射,使用 struct page 的 mapping 字段跟踪高内存页框的映射状态。这些数据结构共同构成了内核虚拟内存管理的骨架。
下面通过一张 SVG 图展示内核空间的详细布局: