2. 虚拟内存系统:虚拟地址空间布局、用户空间与内核空间划分、32位与64位系统差异

2.1 虚拟地址空间概述

虚拟内存是现代操作系统最核心的抽象之一。它为每个进程提供独立的、连续的、私有的地址空间,使进程误以为自己独占整个内存。Linux 内核通过 内存管理单元(MMU) 和 页表 将虚拟地址转换为物理地址,从而实现隔离、保护和高效利用。

虚拟地址空间的大小由 CPU 的地址总线宽度决定:

  • 32位系统:地址总线宽度为 32 位,虚拟地址空间大小为 2³² = 4 GB。
  • 64位系统:地址总线宽度为 64 位,理论上虚拟地址空间大小为 2⁶⁴ = 16 EB(Exabyte)。但当前硬件和操作系统通常只使用 48 位或 57 位地址线,实际可寻址空间远小于理论值。
关键概念: 虚拟地址空间的大小并不等于物理内存的大小。物理内存可能远小于虚拟地址空间(例如 4GB 物理内存运行 64 位系统),也可能通过交换分区(Swap)扩展物理内存的可用性。

2.2 用户空间与内核空间划分

Linux 将虚拟地址空间划分为两个主要区域:用户空间 和 内核空间。这种划分基于 CPU 的 特权级别(Ring):

  • 用户空间(Ring 3):运行用户态进程,权限受限,不能直接访问硬件或内核数据结构。
  • 内核空间(Ring 0):运行内核代码、驱动程序和核心子系统,拥有最高权限,可以访问所有内存和硬件。

划分比例因系统架构而异:

系统架构 用户空间 内核空间 划分方式
32位(默认) 3 GB(0x00000000 – 0xBFFFFFFF) 1 GB(0xC0000000 – 0xFFFFFFFF) 3:1 划分
32位(内核选项) 2 GB / 1 GB 2 GB / 3 GB 可配置(如 CONFIG_PAGE_OFFSET)
64位(x86_64) 低地址区域(通常 0x0000000000000000 – 0x00007FFFFFFFFFFF) 高地址区域(通常 0xFFFF800000000000 – 0xFFFFFFFFFFFFFFFF) 非对称划分,内核空间占据高位
为什么 32 位系统通常采用 3:1 划分? 这是 Linux 内核的默认配置。3 GB 用户空间可以满足大多数应用程序的需求,1 GB 内核空间足够容纳内核代码、驱动、页表、缓存等。如果内核空间不足(例如需要大量 ZONE_NORMAL 或 ZONE_HIGHMEM),可以通过内核编译选项调整比例。

2.3 32位系统虚拟地址空间布局

在 32 位 Linux 系统中,虚拟地址空间布局如下:

32位Linux虚拟地址空间布局 32位Linux虚拟地址空间布局(3:1划分) 用户空间(3 GB) 0x00000000 – 0xBFFFFFFF 代码段(.text) 数据段(.data/.bss) 堆(Heap) 栈(Stack) 共享库(mmap) vsyscall / vDSO 分界线(0xC0000000) 内核空间(1 GB) 0xC0000000 – 0xFFFFFFFF 内核代码 模块(Modules) 页表(Page Tables) 固定映射 直接映射(ZONE_NORMAL / ZONE_HIGHMEM) vmalloc / ioremap 区域 说明:用户空间每个进程独立,内核空间所有进程共享

2.4 64位系统虚拟地址空间布局

64 位系统(以 x86_64 为例)的虚拟地址空间远大于 32 位系统。当前 Linux 内核使用 48 位地址线,虚拟地址空间被分为两个巨大的区域:

  • 用户空间:0x0000000000000000 – 0x00007FFFFFFFFFFF(128 TB)
  • 内核空间:0xFFFF800000000000 – 0xFFFFFFFFFFFFFFFF(128 TB)

中间区域(0x0000800000000000 – 0xFFFF7FFFFFFFFFFF)是 非规范地址(Non-canonical),CPU 无法访问,用于捕获错误指针。

64位Linux虚拟地址空间布局 64位Linux虚拟地址空间布局(x86_64,48位) 用户空间(128 TB) 0x0000000000000000 – 0x00007FFFFFFFFFFF 代码段、数据段、堆、栈、共享库、mmap 区域 非规范地址区域(不可访问) 0x0000800000000000 – 0xFFFF7FFFFFFFFFFF 内核空间(128 TB) 0xFFFF800000000000 – 0xFFFFFFFFFFFFFFFF 内核代码段 模块区域 vmalloc 区域 直接映射(线性映射) 固定映射(Fixmap) 页表(Page Tables) 说明:64位系统用户空间和内核空间大小相等(128 TB),中间非规范区域用于捕获错误指针

2.5 32位与64位系统差异对比

对比维度 32位系统 64位系统
虚拟地址空间大小 4 GB(2³²) 128 TB(2⁴⁷,当前实现)
用户空间大小 3 GB(默认) 128 TB
内核空间大小 1 GB(默认) 128 TB
页表级数 2 级(PGD + PT) 4 级(PGD + PUD + PMD + PT)或 5 级
直接映射区域 ZONE_NORMAL(896 MB)+ ZONE_HIGHMEM 线性映射整个物理内存(无 HIGHMEM)
进程最大内存 约 3 GB 约 128 TB
内核模块地址 固定区域(0xC0000000 以上) 独立模块区域(通常靠近内核代码)
vmalloc 区域 有限(约 128 MB) 巨大(数 TB)
物理内存限制 4 GB(无 PAE)/ 64 GB(PAE) 理论上 4 PB(实际受硬件限制)

2.6 内核空间关键区域详解

2.6.1 直接映射区域(Linear Mapping / Direct Mapping)

内核空间中最核心的区域是 直接映射,也称为 线性映射。它将物理内存直接映射到内核虚拟地址空间,映射关系为:

虚拟地址 = 物理地址 + PAGE_OFFSET

在 32 位系统中,PAGE_OFFSET 通常为 0xC0000000,直接映射区域覆盖物理内存的前 896 MB(ZONE_NORMAL)。超过 896 MB 的物理内存属于 ZONE_HIGHMEM,需要通过 kmap() 临时映射。

在 64 位系统中,PAGE_OFFSET 通常为 0xFFFF880000000000(或类似值),直接映射区域可以覆盖整个物理内存,无需 HIGHMEM 机制。

注意: 直接映射区域虽然线性,但并不意味着所有物理内存都永久映射。内核仍然需要动态管理页表,尤其是在内存热插拔或大内存系统中。

2.6.2 vmalloc 区域

vmalloc 区域用于分配 虚拟地址连续但物理地址不连续 的内存。它位于直接映射区域之后,适用于需要大块连续虚拟地址但物理内存碎片化的场景(如内核模块、驱动缓冲区)。

  • 32位:vmalloc 区域通常从 VMALLOC_START 到 VMALLOC_END,大小约 128 MB。
  • 64位:vmalloc 区域巨大(数 TB),从 VMALLOC_START 到 VMALLOC_END,几乎不受限制。

2.6.3 固定映射区域(Fixmap)

固定映射区域用于 永久映射 特定的物理地址到内核虚拟地址空间,通常用于访问硬件寄存器、ACPI 表、APIC 等。它位于内核空间的高位,地址固定不变。

2.6.4 模块区域

内核模块(.ko 文件)被加载到 模块区域。在 32 位系统中,模块区域紧邻内核代码段;在 64 位系统中,模块区域通常位于内核代码段附近,大小约 2 GB。

2.7 用户空间布局细节

每个用户进程的虚拟地址空间布局如下:

区域 起始地址(典型) 说明
代码段(.text) 0x08048000(32位) 可执行指令,只读
数据段(.data / .bss) 代码段之后 已初始化/未初始化全局变量
堆(Heap) 数据段之后 通过 brk() / sbrk() 扩展,向上增长
mmap 区域 堆之上(随机化) 共享库、文件映射、匿名映射,向下增长
栈(Stack) 用户空间顶部(随机化) 局部变量、函数调用帧,向下增长
vDSO / vsyscall 栈之下 内核提供的用户空间加速接口
地址空间布局随机化(ASLR): 现代 Linux 内核默认启用 ASLR,随机化堆、栈、mmap 区域和 vDSO 的基地址,以增加攻击者预测内存布局的难度。可以通过 /proc/sys/kernel/randomize_va_space 控制 ASLR 级别(0=关闭,1=随机化,2=完全随机化)。

2.8 进程切换与地址空间

当 CPU 从一个进程切换到另一个进程时,内核必须切换 地址空间。这通过修改 页表基地址寄存器(CR3 在 x86 中) 实现。每个进程拥有独立的页表,切换进程即切换页表。

关键数据结构:

  • struct mm_struct:描述进程的虚拟地址空间,包含页表基地址、代码段/数据段/堆/栈的边界、mmap 链表等。
  • struct vm_area_struct:描述地址空间中的一个连续区域(VMA),包含起始地址、结束地址、权限标志、文件映射信息等。
// 进程地址空间核心结构(简化)
struct mm_struct {
    unsigned long start_code, end_code;   // 代码段范围
    unsigned long start_data, end_data;   // 数据段范围
    unsigned long start_brk, brk;         // 堆范围
    unsigned long start_stack;            // 栈起始地址
    unsigned long arg_start, arg_end;     // 命令行参数
    unsigned long env_start, env_end;     // 环境变量
    pgd_t *pgd;                           // 页全局目录(页表基地址)
    struct vm_area_struct *mmap;          // VMA 链表
    struct rb_root mm_rb;                 // VMA 红黑树
    unsigned long total_vm;               // 总虚拟页面数
    unsigned long hiwater_rss;            // 最大驻留集大小
    ...
};

2.9 内核空间与用户空间的数据交换

内核与用户空间之间的数据交换必须通过 系统调用 或 驱动程序接口 进行。由于用户空间不能直接访问内核空间,内核提供以下机制:

  • copy_from_user() / copy_to_user():安全地在用户空间和内核空间之间复制数据,会检查用户空间地址的有效性。
  • get_user() / put_user():用于复制单个整数或指针。
  • mmap:允许用户空间直接映射内核内存(如帧缓冲、DMA 缓冲区),但需要内核显式支持。
  • procfs / sysfs / debugfs:通过文件系统接口暴露内核数据。
安全提示: 内核代码永远不能直接解引用用户空间指针。必须使用 copy_from_user() 等安全函数,否则可能导致内核崩溃或安全漏洞(如 Spectre、Meltdown 相关攻击)。

2.10 本章小结

  • 虚拟地址空间是 Linux 内存管理的基石,通过 MMU 和页表实现虚拟地址到物理地址的转换。
  • 用户空间与内核空间的划分基于 CPU 特权级别,32 位系统通常采用 3:1 划分,64 位系统提供 128 TB 用户空间和 128 TB 内核空间。
  • 32 位系统受限于 4 GB 地址空间,存在 HIGHMEM 问题;64 位系统地址空间巨大,无需 HIGHMEM,页表级数更多。
  • 内核空间包含直接映射、vmalloc、固定映射、模块区域等关键区域,每个区域有特定的用途和管理方式。
  • 用户空间布局包括代码段、数据段、堆、mmap 区域、栈和 vDSO,ASLR 提供安全保护。
  • 进程切换涉及地址空间切换,内核通过 struct mm_struct 和 struct vm_area_struct 管理进程地址空间。