2. 进程地址空间 mm_struct 深度解析
在 Linux 内核中,每个进程都拥有一个独立的虚拟地址空间,而 mm_struct 正是描述这个地址空间的核心数据结构。它记录了进程内存的布局、映射关系、权限控制以及物理内存的映射入口。理解 mm_struct 是掌握内存管理全栈的第一步,也是后续分析页表、缺页中断和 VMA 操作的基础。
mm_struct 是进程的“内存身份证”。内核通过它来管理进程的代码段、数据段、堆、栈、内存映射文件以及页表层次结构。每个进程(除内核线程外)都拥有唯一的 mm_struct 实例。
2.1 mm_struct 核心字段全景
下表列出了 mm_struct 中最重要的字段及其功能分类,这些字段直接参与地址空间的构建、查找和权限控制。
| 字段名 | 类型 | 功能描述 | 所属类别 |
|---|---|---|---|
pgd |
pgd_t * |
指向进程页全局目录(Page Global Directory)的指针,是页表遍历的根入口 | 页表管理 |
mmap |
struct vm_area_struct * |
虚拟内存区域(VMA)链表头,按虚拟地址升序链接所有 VMA | VMA 管理 |
mm_rb |
struct rb_root |
VMA 红黑树根节点,用于快速查找指定虚拟地址所属的 VMA | VMA 管理 |
arg_start / arg_end |
unsigned long |
命令行参数(argv)在用户空间栈中的起始和结束地址 | 进程参数 |
env_start / env_end |
unsigned long |
环境变量(envp)在用户空间栈中的起始和结束地址 | 进程参数 |
start_code / end_code |
unsigned long |
代码段的起始和结束虚拟地址 | 代码段 |
start_data / end_data |
unsigned long |
初始化数据段(data segment)的起始和结束虚拟地址 | 数据段 |
start_brk / brk |
unsigned long |
堆(heap)的起始地址和当前堆顶地址,brk 用于系统调用 brk() 调整堆大小 |
堆管理 |
start_stack |
unsigned long |
用户栈的起始地址(栈顶附近) | 栈管理 |
map_count |
atomic_t |
当前进程中 VMA 的数量 | 统计 |
total_vm |
unsigned long |
进程虚拟内存页的总数(以 PAGE_SIZE 为单位) | 统计 |
mmap 链表用于遍历所有 VMA,而 mm_rb 红黑树用于快速查找。内核在查找 VMA 时优先使用红黑树(O(log n) 复杂度),只有在需要顺序遍历或合并相邻 VMA 时才使用链表。
2.2 pgd:页表遍历的根入口
pgd 是 mm_struct 中指向页全局目录的指针。在 x86-64 架构下,页表采用四级页表结构:PGD → P4D → PUD → PMD → PTE。进程切换时,内核会将当前进程的 pgd 值写入 CPU 的 CR3 寄存器,从而激活该进程的地址空间。
下面通过 SVG 图展示 pgd 在页表层次中的位置以及虚拟地址到物理地址的转换路径。
mm_struct,但 pgd 指向的页表会被写时复制(Copy-on-Write)。这意味着父子进程最初共享同一套页表,只有当一方尝试写入时才会创建新的页表副本。
2.3 mmap 与 mm_rb:VMA 的双重组织
虚拟内存区域(VMA)是进程地址空间的基本管理单元。每个 VMA 描述一段连续的虚拟地址区间及其属性(权限、文件映射、匿名等)。mm_struct 通过两种数据结构组织 VMA:
- mmap 链表: 按虚拟地址升序链接所有 VMA,用于顺序遍历和相邻 VMA 的合并操作。
- mm_rb 红黑树: 以 VMA 的起始地址为键值构建平衡二叉搜索树,用于快速查找指定虚拟地址所属的 VMA。
下面的 SVG 图展示了 mmap 链表和 mm_rb 红黑树如何共同管理进程的 VMA 集合。
find_vma() 首先尝试从 mm_rb 红黑树中查找,如果找到则返回;如果未找到,则通过 mmap 链表进行边界检查。这种双重结构兼顾了查找效率和遍历灵活性。
2.4 arg_start/arg_end 与 env_start/env_end:进程参数与环境的边界
当用户执行一个程序时,内核会将命令行参数和环境变量放置在用户栈的顶部附近。arg_start 和 arg_end 标记了参数列表(argv)在栈中的地址范围,而 env_start 和 env_end 标记了环境变量(envp)的地址范围。这些字段对于调试器(如 GDB)和 /proc/<pid>/maps 的解析至关重要。
下面的 SVG 图展示了进程地址空间的典型布局,并标注了这些关键边界字段的位置。
/proc/<pid>/maps 可以查看进程的完整地址空间布局,其中每一行对应一个 VMA。而 /proc/<pid>/stat 中包含了 arg_start、arg_end 等字段的数值,可用于分析进程参数和环境的内存占用。
2.5 其他关键字段速览
除了上述核心字段外,mm_struct 还包含以下重要成员:
- start_brk / brk: 堆的起始地址和当前堆顶。系统调用
brk()和sbrk()通过调整brk来分配或释放堆内存。 - start_stack: 用户栈的起始地址(通常接近栈的最高地址)。栈从该地址向下增长。
- map_count: 当前 VMA 的数量。当
map_count超过sysctl_max_map_count(默认 65530)时,mmap系统调用会返回ENOMEM。 - total_vm: 进程虚拟内存页的总数,包括所有已映射和未映射的页面。该值可通过
top或ps命令的VSZ字段查看。 - def_flags: 默认的 VMA 标志,例如
VM_READ、VM_WRITE、VM_EXEC,新建 VMA 时会继承这些标志。
map_count 超过 10000 时,VMA 的查找和遍历性能会显著下降。此时应考虑使用大页(Huge Pages)或调整应用程序的内存映射策略来减少 VMA 数量。
2.6 实战:通过 /proc 查看 mm_struct 字段
以下示例展示了如何通过 /proc 文件系统查看当前进程的 mm_struct 关键字段。代码使用 C 语言读取 /proc/self/stat 并解析相关字段。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
FILE *fp = fopen("/proc/self/stat", "r");
if (!fp) {
perror("fopen");
return 1;
}
char buf[1024];
if (!fgets(buf, sizeof(buf), fp)) {
perror("fgets");
fclose(fp);
return 1;
}
fclose(fp);
// /proc/self/stat 格式:pid comm state ppid ...
// 第 26 个字段是 startcode,第 27 是 endcode
// 第 28 是 startstack,第 29 是 arg_start,第 30 是 arg_end
// 第 31 是 env_start,第 32 是 env_end
unsigned long startcode, endcode, startstack;
unsigned long arg_start, arg_end, env_start, env_end;
// 跳过前 25 个字段
char *token = strtok(buf, " ");
for (int i = 1; i < 25; i++) {
token = strtok(NULL, " ");
if (!token) break;
}
if (token) {
startcode = strtoul(token, NULL, 10);
token = strtok(NULL, " "); endcode = strtoul(token, NULL, 10);
token = strtok(NULL, " "); startstack = strtoul(token, NULL, 10);
token = strtok(NULL, " "); arg_start = strtoul(token, NULL, 10);
token = strtok(NULL, " "); arg_end = strtoul(token, NULL, 10);
token = strtok(NULL, " "); env_start = strtoul(token, NULL, 10);
token = strtok(NULL, " "); env_end = strtoul(token, NULL, 10);
printf("startcode = 0x%lx\n", startcode);
printf("endcode = 0x%lx\n", endcode);
printf("startstack = 0x%lx\n", startstack);
printf("arg_start = 0x%lx\n", arg_start);
printf("arg_end = 0x%lx\n", arg_end);
printf("env_start = 0x%lx\n", env_start);
printf("env_end = 0x%lx\n", env_end);
}
return 0;
}
startcode = 0x400000
endcode = 0x401000
startstack = 0x7ffc8a000000
arg_start = 0x7ffc8a001000
arg_end = 0x7ffc8a001020
env_start = 0x7ffc8a001020
env_end = 0x7ffc8a001080
2.7 本章小结
mm_struct 是进程地址空间管理的总控结构,其核心字段覆盖了页表入口(pgd)、VMA 组织(mmap / mm_rb)、进程参数边界(arg_start / arg_end)以及代码/数据/堆/栈的地址范围。理解这些字段的语义和相互关系,是后续分析 VMA 操作、缺页异常处理和页表遍历的基础。
pgd 指向的是页全局目录的虚拟地址,而非物理地址。内核在切换进程时,需要将 pgd 转换为物理地址后写入 CR3 寄存器。这一转换通过 __pa() 宏完成。