4. 内核空间与用户空间划分:3:1 / 2:2 虚拟地址空间原理

在 32 位 Linux 系统中,虚拟地址空间总大小为 4 GB(232 字节)。为了隔离操作系统内核与用户进程,Linux 将这一 4 GB 空间划分为内核空间和用户空间两部分。划分比例由内核编译时的配置决定,最常见的是 3:1 和 2:2 两种方案。

核心概念:用户空间运行在 CPU 的 Ring 3(用户态),无法直接访问内核空间;内核空间运行在 Ring 0(内核态),可以访问整个 4 GB 地址空间。这种隔离是系统安全与稳定的基石。

4.1 3:1 划分(默认配置)

在大多数 32 位 Linux 发行版中,默认采用 3:1 划分:

  • 用户空间:0x00000000 ~ 0xBFFFFFFF(3 GB)
  • 内核空间:0xC0000000 ~ 0xFFFFFFFF(1 GB)

用户进程的代码、数据、堆、栈以及动态库都映射在 0~3 GB 范围内。内核代码、全局数据结构、页表、模块等则位于 3~4 GB 的高地址区域。

4.2 2:2 划分(特殊配置)

当内核配置了 CONFIG_VMSPLIT_2G 选项时,采用 2:2 划分:

  • 用户空间:0x00000000 ~ 0x7FFFFFFF(2 GB)
  • 内核空间:0x80000000 ~ 0xFFFFFFFF(2 GB)

这种划分通常用于需要大量内核内存映射的场景(如大型网络服务器或特定嵌入式系统),但会减少每个用户进程可用的虚拟地址空间。

4.3 划分比例对比

特性 3:1 划分 2:2 划分
用户空间大小 3 GB 2 GB
内核空间大小 1 GB 2 GB
用户空间地址范围 0x00000000 ~ 0xBFFFFFFF 0x00000000 ~ 0x7FFFFFFF
内核空间起始地址 0xC0000000 0x80000000
适用场景 通用桌面/服务器 需要大量内核内存映射
用户进程最大虚拟内存 ~3 GB ~2 GB

4.4 关键宏定义:PAGE_OFFSET 与 TASK_SIZE

Linux 内核通过两个关键宏来定义用户空间与内核空间的分界线:

4.4.1 PAGE_OFFSET

PAGE_OFFSET 定义了内核空间的起始虚拟地址,即用户空间与内核空间的分界线。在 3:1 划分中,PAGE_OFFSET 等于 0xC0000000;在 2:2 划分中,等于 0x80000000。

/* arch/x86/include/asm/page_32_types.h */
#define PAGE_OFFSET     ((unsigned long)__PAGE_OFFSET)

/* 根据 CONFIG_VMSPLIT_2G 等选项定义 __PAGE_OFFSET */
#define __PAGE_OFFSET           _AC(CONFIG_PAGE_OFFSET, UL)

/* 默认 3:1 划分时 CONFIG_PAGE_OFFSET = 0xC0000000 */
/* 2:2 划分时 CONFIG_PAGE_OFFSET = 0x80000000 */

4.4.2 TASK_SIZE

TASK_SIZE 定义了用户空间的最大地址,即用户进程可以访问的最高虚拟地址。通常 TASK_SIZE = PAGE_OFFSET,但在某些架构或配置下可能略有不同。

/* arch/x86/include/asm/processor.h */
#define TASK_SIZE_MAX   ((1UL << __VIRTUAL_MASK_SHIFT) - PAGE_SIZE)

/* 32 位默认:TASK_SIZE = PAGE_OFFSET */
#define TASK_SIZE       (test_thread_flag(TIF_ADDR32) ? \
                         IA32_PAGE_OFFSET : TASK_SIZE_MAX)

/* 简化理解:TASK_SIZE 通常等于 PAGE_OFFSET */
实战提示:在编写内核模块或驱动时,经常需要判断一个地址属于用户空间还是内核空间。标准做法是:if (addr < TASK_SIZE) { /* 用户空间地址 */ } else { /* 内核空间地址 */ }。注意不要直接硬编码 0xC0000000,应始终使用 PAGE_OFFSET 或 TASK_SIZE 宏以保证可移植性。

4.5 虚拟地址空间划分 SVG 架构图

下图展示了 32 位 Linux 系统中 3:1 和 2:2 两种划分方案的虚拟地址空间布局:

32位Linux虚拟地址空间划分:3:1 vs 2:2 32位Linux虚拟地址空间划分对比 3:1 划分(默认) 内核空间 (1 GB) 0xC0000000 ~ 0xFFFFFFFF Ring 0 (内核态) PAGE_OFFSET = 0xC0000000 用户空间 (3 GB) 0x00000000 ~ 0xBFFFFFFF Ring 3 (用户态) 代码段 (.text) 数据段 (.data / .bss) 堆 (Heap) 栈 (Stack) 2:2 划分(特殊配置) 内核空间 (2 GB) 0x80000000 ~ 0xFFFFFFFF Ring 0 (内核态) PAGE_OFFSET = 0x80000000 用户空间 (2 GB) 0x00000000 ~ 0x7FFFFFFF Ring 3 (用户态) 💡 关键理解: • PAGE_OFFSET 是内核空间起始地址,也是用户/内核分界线 • TASK_SIZE 通常等于 PAGE_OFFSET,表示用户进程可访问的最大地址

4.6 64 位系统的地址空间划分

在 64 位架构(如 x86_64)中,虚拟地址空间远大于 4 GB,但 Linux 仍然保持用户空间与内核空间的划分原则。以 x86_64 为例:

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

中间巨大的地址空洞(0x0000800000000000 ~ 0xFFFF7FFFFFFFFFFF)用于捕获非法访问。在 64 位系统中,PAGE_OFFSET 被定义为 0xFFFF800000000000,TASK_SIZE 为 0x00007FFFFFFFFFFF。

避坑指南:在 64 位系统上编写可移植代码时,不要假设 PAGE_OFFSET 是 32 位的 0xC0000000。始终使用内核提供的宏定义,并注意地址类型使用 unsigned long 而非 int,否则会导致地址截断错误。

4.7 用户态与内核态切换的硬件支持

用户空间与内核空间的隔离不仅依赖软件约定,更需要 CPU 硬件机制的支持:

硬件机制 作用 与地址空间划分的关系
特权级(Ring 0/3) 控制指令执行权限 用户态只能访问用户空间,内核态可访问全部空间
MMU / 页表 虚拟地址到物理地址的转换 每个进程有独立页表,内核空间映射在所有页表中
系统调用(syscall) 用户态请求内核服务 通过 int 0x80 或 syscall 指令切换特权级
TLB 隔离 缓存地址转换结果 进程切换时刷新 TLB,防止用户态访问内核映射

4.8 内核空间映射的物理内存区域

内核空间 1 GB(或 2 GB)并非全部用于映射物理内存,而是划分为多个逻辑区域:

内核空间内部区域划分 内核空间内部区域划分(3:1 方案,32位) 内核空间 0xC0000000 ~ 0xFFFFFFFF (1 GB) 直接映射区 (896 MB, 低端内存) vmalloc (非连续映射) 持久映射 (PKMAP) 固定映射 (FIXADDR) 高端内存 (临时映射) 📌 说明: • 直接映射区:物理地址 = 虚拟地址 - PAGE_OFFSET,用于低端内存(< 896 MB) • vmalloc/持久映射/固定映射/高端内存:用于映射高端物理内存(> 896 MB)

4.9 本章小结

  • 3:1 划分是 32 位 Linux 的默认方案,用户空间 3 GB,内核空间 1 GB。
  • 2:2 划分通过 CONFIG_VMSPLIT_2G 启用,用户空间和内核空间各 2 GB。
  • PAGE_OFFSET 定义内核空间起始地址,是用户/内核空间的分界线。
  • TASK_SIZE 定义用户进程可访问的最大地址,通常等于 PAGE_OFFSET。
  • 64 位系统地址空间更大,但划分原则相同,PAGE_OFFSET 和 TASK_SIZE 的值相应变化。
  • 内核空间内部进一步划分为直接映射区、vmalloc 区、持久映射区、固定映射区和高端内存映射区。