4. 进程地址空间结构:task_struct中的mm_struct,内存描述符,VMA(虚拟内存区域)的概念
在Linux内核中,每个进程都拥有独立的虚拟地址空间,这是实现进程隔离与内存保护的基础。内核通过一套精妙的数据结构来管理这片虚拟地址空间,其核心是内存描述符与虚拟内存区域。本章将深入剖析这些结构,揭示进程地址空间的骨架与血肉。
4.1 内存描述符:mm_struct
每个进程的task_struct(进程描述符)中都包含一个指向mm_struct的指针,称为mm。这个结构体是进程地址空间的“总管家”,它描述了整个虚拟地址空间的全局属性。
mm_struct 记录了进程的代码段、数据段、堆、栈、内存映射区域的起始与结束地址,以及页表、引用计数等信息。内核线程没有用户地址空间,其 mm 指针为 NULL。
| 字段 | 类型 | 描述 |
|---|---|---|
mmap |
struct vm_area_struct * |
指向虚拟内存区域(VMA)链表的头节点 |
mm_rb |
struct rb_root |
VMA 红黑树的根节点,用于快速查找 |
pgd |
pgd_t * |
指向进程页全局目录(页表)的指针 |
start_code, end_code |
unsigned long |
代码段的起始与结束地址 |
start_data, end_data |
unsigned long |
数据段的起始与结束地址 |
start_brk, brk |
unsigned long |
堆的起始地址(start_brk)与当前堆顶(brk) |
start_stack |
unsigned long |
用户栈的起始地址 |
map_count |
unsigned long |
当前进程中 VMA 的数量 |
total_vm |
unsigned long |
进程虚拟内存页的总数 |
/proc/[pid]/maps 文件的内容直接来源于 mm_struct 中的 VMA 链表。通过解析该文件,可以精确掌握进程的地址空间布局。
4.2 虚拟内存区域:VMA
虚拟地址空间并非一个连续的整块,而是由多个虚拟内存区域(VMA)拼接而成。每个VMA代表一段连续的、具有相同权限和属性的虚拟地址区间。例如,代码段是一个VMA,数据段是另一个VMA,堆、栈、mmap映射区各自对应独立的VMA。
VMA在内核中使用struct vm_area_struct结构体描述,它同时存在于一个双向链表(用于遍历)和一个红黑树(用于快速查找)中。
// 内核源码 include/linux/mm_types.h (简化结构)
struct vm_area_struct {
unsigned long vm_start; // VMA 起始地址
unsigned long vm_end; // VMA 结束地址
struct vm_area_struct *vm_next, *vm_prev; // 链表指针
struct rb_node vm_rb; // 红黑树节点
unsigned long vm_flags; // 访问权限标志 (读、写、执行、共享等)
struct file *vm_file; // 映射的文件 (如果是文件映射)
unsigned long vm_pgoff; // 文件内的偏移量 (页对齐)
struct mm_struct *vm_mm; // 所属的内存描述符
// ... 其他字段 (如操作函数集 vm_ops)
};
4.3 VMA 的权限与标志
每个VMA都通过vm_flags字段定义其访问权限和行为特性。这些标志决定了进程能否读取、写入或执行该区域的内存,以及该区域是否为私有或共享映射。
| 标志 | 含义 | 说明 |
|---|---|---|
VM_READ |
可读 | 允许从该区域读取数据 |
VM_WRITE |
可写 | 允许向该区域写入数据 |
VM_EXEC |
可执行 | 允许执行该区域中的指令 |
VM_SHARED |
共享映射 | 对该区域的修改会写回文件(文件映射)或与其他进程共享(匿名映射) |
VM_GROWSDOWN |
向下增长 | 该区域可以向低地址扩展(典型用于栈) |
VM_GROWSUP |
向上增长 | 该区域可以向高地址扩展 |
mmap 创建匿名映射时,如果不指定 MAP_SHARED,则默认是 MAP_PRIVATE。私有映射的写操作会触发“写时复制”(Copy-on-Write),不会影响原始文件或与其他进程共享内存。理解 VM_SHARED 与 VM_WRITE 的组合是掌握进程间通信(IPC)的关键。
4.4 进程地址空间结构总览
下图展示了从task_struct到mm_struct,再到VMA链表和红黑树的整体架构关系。
4.5 VMA 的操作与内核栈分配
内核通过一系列API来操作VMA,这些操作是内存管理的基础。例如,当进程调用mmap()系统调用时,内核会创建一个新的VMA并将其插入到进程的VMA链表和红黑树中。当进程调用munmap()时,对应的VMA会被删除。
值得注意的是,内核栈的分配与用户态栈不同。每个进程在内核态运行时,使用的是固定大小的内核栈(通常为8KB或16KB),这部分内存并不通过VMA管理,而是由内核在创建进程时直接从物理内存中分配。内核栈位于内核地址空间,与用户态的VMA完全隔离。
- 用户栈: 由VMA描述,通过缺页异常动态增长(
VM_GROWSDOWN标志),地址空间随机化(ASLR)。 - 内核栈: 固定大小,分配于内核地址空间,不经过VMA,用于处理系统调用和中断上下文。
4.6 实战:查看进程的VMA布局
在Linux系统中,我们可以通过/proc/[pid]/maps文件直观地查看进程的VMA布局。以下是一个示例输出及其对应的VMA结构解释:
# 查看当前bash进程的地址空间
$ cat /proc/$$/maps
00400000-004ef000 r-xp 00000000 08:01 1234567 /bin/bash
006ef000-006f0000 r--p 000ef000 08:01 1234567 /bin/bash
006f0000-006f5000 rw-p 000f0000 08:01 1234567 /bin/bash
006f5000-006fc000 rw-p 00000000 00:00 0 [heap]
7f8c1a000000-7f8c1a021000 rw-p 00000000 00:00 0 [heap]
7f8c1c000000-7f8c1c002000 rw-s 00000000 00:00 0 [vvar]
7f8c1c002000-7f8c1c004000 r-xp 00000000 00:00 0 [vdso]
7ffd5e800000-7ffd5ea00000 rw-p 00000000 00:00 0 [stack]
7ffd5ebf0000-7ffd5ebf3000 r--p 00000000 00:00 0 [vvar]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
每一行对应一个VMA,其格式为:起始地址-结束地址 权限 偏移量 主设备号:次设备号 inode 路径。权限字段的r、w、x、p或s分别对应VM_READ、VM_WRITE、VM_EXEC、私有(p)或共享(s)。
pmap -x [pid] 命令可以获得更友好的VMA汇总信息,包括每个区域的RSS(驻留内存大小)和脏页大小。这是分析进程内存占用最直接的工具之一。