3、虚拟内存区域 VMA:vm_area_struct 结构体、VMA 链表与红黑树组织、VMA 的查找与遍历
虚拟内存区域(VMA,Virtual Memory Area)是 Linux 内核管理进程地址空间的核心抽象。每个 VMA 描述一段连续的、具有相同权限和映射属性的虚拟地址区间。进程的整个用户空间由若干 VMA 拼接而成,内核通过 vm_area_struct 结构体来刻画每一个区域。
3.1 vm_area_struct 结构体
vm_area_struct 是 VMA 在内核中的具体表示,定义在 include/linux/mm_types.h 中。它包含了 VMA 的起始地址、结束地址、访问权限、标志位、关联的文件映射以及用于组织管理的链表/红黑树节点。
| 字段 | 类型 | 说明 |
|---|---|---|
vm_start |
unsigned long | VMA 的起始虚拟地址(包含) |
vm_end |
unsigned long | VMA 的结束虚拟地址(不包含) |
vm_flags |
unsigned long | VMA 的权限与属性标志位(如读、写、执行、共享、匿名等) |
vm_page_prot |
pgprot_t | 页表项使用的物理页保护位(由 vm_flags 转换而来) |
vm_file |
struct file * | 映射的文件对象指针(匿名映射为 NULL) |
vm_pgoff |
unsigned long | 文件映射的页偏移量(以 PAGE_SIZE 为单位) |
vm_ops |
const struct vm_operations_struct * | VMA 操作函数表(open、close、fault 等) |
vm_next / vm_prev |
struct vm_area_struct * | 双向链表指针,用于按地址顺序串联所有 VMA |
vm_rb |
struct rb_node | 红黑树节点,用于快速查找 |
rb_subtree_gap |
unsigned long | 子树中最大空闲间隙(用于 VMA 合并与分配优化) |
vm_mm |
struct mm_struct * | 指向所属进程的内存描述符 |
VM_READ(0x01) — 可读VM_WRITE(0x02) — 可写VM_EXEC(0x04) — 可执行VM_SHARED(0x08) — 共享映射VM_GROWSDOWN(0x100) — 可向下扩展(如栈)VM_ANON(0x800) — 匿名映射(无文件关联)
3.2 VMA 的组织结构:链表与红黑树
每个进程的 mm_struct 中维护了两种 VMA 组织方式:双向链表用于顺序遍历,红黑树用于高效查找。两者共享同一组 vm_area_struct 实例,通过不同的指针字段链接。
- 链表:按
vm_start升序排列,适合遍历全部 VMA(如/proc/<pid>/maps读取、fork 时复制)。 - 红黑树:自平衡二叉搜索树,查找、插入、删除的时间复杂度为 O(log n),适合按虚拟地址快速定位 VMA(如缺页异常处理)。
下图展示了 mm_struct 与 VMA 链表、红黑树之间的关系:
vm_next/vm_prev 指针和红黑树节点。任何不一致都会导致查找错误或内核崩溃。
3.3 VMA 的查找与遍历
3.3.1 按虚拟地址查找 VMA
最常用的查找函数是 find_vma(),它通过红黑树在 O(log n) 时间内找到第一个 vm_end > addr 的 VMA。内核源码实现如下:
/* mm/mmap.c */
struct vm_area_struct *find_vma(struct mm_struct *mm, unsigned long addr)
{
struct rb_node *rb_node;
struct vm_area_struct *vma;
/* 先检查缓存(last_vma) */
vma = vma_lookup(mm, addr);
if (vma)
return vma;
/* 红黑树查找 */
rb_node = mm->mm_rb.rb_node;
vma = NULL;
while (rb_node) {
struct vm_area_struct *tmp = rb_entry(rb_node,
struct vm_area_struct, vm_rb);
if (tmp->vm_end > addr) {
vma = tmp;
if (tmp->vm_start <= addr)
break;
rb_node = rb_node->rb_left;
} else {
rb_node = rb_node->rb_right;
}
}
if (vma)
mm->mmap_last_vma = vma; /* 更新缓存 */
return vma;
}
mm_struct 中维护了 mmap_last_vma 缓存,记录上一次查找命中的 VMA。由于缺页异常和访问通常具有空间局部性,缓存命中率很高,可避免频繁的红黑树遍历。
3.3.2 遍历所有 VMA
遍历 VMA 通常使用链表,因为顺序访问不需要搜索特性。内核提供了 vma_next() 宏和 for_each_vma() 遍历宏:
/* 使用链表遍历所有 VMA */
struct vm_area_struct *vma;
for (vma = mm->mmap; vma; vma = vma->vm_next) {
/* 处理每个 VMA */
printk("VMA: 0x%lx - 0x%lx, flags: 0x%lx\n",
vma->vm_start, vma->vm_end, vma->vm_flags);
}
/* 内核提供的遍历宏(include/linux/mm.h) */
#define for_each_vma(vmi, vma) \
while ((vma = vma_iter_next(vmi)) != NULL)
3.3.3 查找与遍历的对比
| 操作 | 数据结构 | 时间复杂度 | 典型场景 |
|---|---|---|---|
| 按地址查找单个 VMA | 红黑树 | O(log n) | 缺页异常、权限检查、地址合法性校验 |
| 遍历全部 VMA | 双向链表 | O(n) | 进程地址空间转储(/proc/<pid>/maps)、fork 时复制、mremap 合并 |
| 查找空闲地址区间 | 红黑树 + rb_subtree_gap | O(log n) | mmap 分配新 VMA 时寻找合适空洞 |
3.4 VMA 操作流程示例
下图展示了缺页异常发生时,内核如何通过红黑树查找 VMA 并判断访问权限:
3.5 VMA 的合并与分裂
当新的 VMA 插入或现有 VMA 的属性发生变化时,内核会尝试与相邻 VMA 进行合并,以减少 VMA 数量、降低管理开销。合并条件如下:
- 两个 VMA 的
vm_start和vm_end地址连续(无间隙)。 - 两个 VMA 的
vm_flags完全相同。 - 两个 VMA 的
vm_file指向同一个文件(或均为匿名映射)。 - 两个 VMA 的
vm_pgoff连续(文件映射时)。 - 两个 VMA 的
vm_ops相同。
分裂操作则发生在 mprotect、munmap 等系统调用需要改变部分 VMA 属性时。内核将原 VMA 拆分为两个或三个独立的 VMA,分别应用不同的权限或映射策略。
3.6 实战:通过 /proc/<pid>/maps 观察 VMA
用户态可以通过 /proc/<pid>/maps 文件查看进程的 VMA 布局。内核在读取该文件时,正是通过遍历 mm->mmap 链表来逐行输出的:
$ cat /proc/self/maps
00400000-00452000 r-xp 00000000 08:01 1234567 /usr/bin/cat
00651000-00652000 r--p 00051000 08:01 1234567 /usr/bin/cat
00652000-00653000 rw-p 00052000 08:01 1234567 /usr/bin/cat
7f8c1a200000-7f8c1a3b1000 r-xp 00000000 08:01 7654321 /usr/lib/libc-2.31.so
7f8c1a5b0000-7f8c1a5b1000 r--p 001b0000 08:01 7654321 /usr/lib/libc-2.31.so
7f8c1a5b1000-7f8c1a5b4000 rw-p 001b1000 08:01 7654321 /usr/lib/libc-2.31.so
7ffd8e200000-7ffd8e421000 rw-p 00000000 00:00 0 [stack]
7ffd8e5e8000-7ffd8e5eb000 r--p 00000000 00:00 0 [vvar]
7ffd8e5eb000-7ffd8e5ed000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]
每一行对应一个 VMA,格式为:起始地址-结束地址 权限 偏移 主设备号:次设备号 inode 路径。权限字段的 r/w/x/p/s 分别对应 VM_READ、VM_WRITE、VM_EXEC、私有映射 (VM_SHARED 未设置) 和共享映射 (VM_SHARED 已设置)。
strace -e mmap,munmap,mprotect 跟踪程序的 VMA 操作,结合 /proc/<pid>/maps 观察 VMA 的创建、删除和权限变化,是理解 VMA 管理最直观的实战方法。