3、内存区域VMA(vm_area_struct)详解:VMA结构体定义、红黑树与链表双重组织方式
进程的虚拟地址空间并非一整块连续的线性区域,而是被划分为多个独立的、具有不同属性的内存区域。每个这样的区域在Linux内核中由 vm_area_struct 结构体描述,简称 VMA。VMA是内存管理中最核心的数据结构之一,它记录了进程地址空间中每一段映射的起始地址、大小、权限、标志以及对应的物理后备存储(文件或匿名页)。
3.1 VMA结构体核心字段解析
vm_area_struct 定义在 include/linux/mm_types.h 中,其字段可划分为几个功能组:地址范围、访问权限、组织方式、后备存储、操作函数集。
struct vm_area_struct {
/* 地址范围:描述该VMA在虚拟地址空间中的区间 */
unsigned long vm_start; // 起始地址(包含)
unsigned long vm_end; // 结束地址(不包含)
/* 组织方式:红黑树节点与链表节点 */
struct rb_node vm_rb; // 红黑树节点,用于快速查找
struct list_head vm_list; // 链表节点,用于顺序遍历
/* 所属进程的mm_struct */
struct mm_struct *vm_mm; // 指向所属进程的内存描述符
/* 访问权限与标志 */
pgprot_t vm_page_prot; // 页表项中的权限位(读/写/执行)
unsigned long vm_flags; // VMA标志位集合
/* 后备存储:文件映射或匿名映射 */
struct file *vm_file; // 映射的文件对象(NULL表示匿名映射)
unsigned long vm_pgoff; // 文件映射的页偏移量
/* 操作函数集:VMA支持的操作 */
const struct vm_operations_struct *vm_ops;
/* 匿名页反向映射 */
struct list_head anon_vma_chain; // 匿名VMA链
struct anon_vma *anon_vma; // 匿名页反向映射结构
/* NUMA策略 */
struct mempolicy *vm_policy; // 内存分配策略
/* 用户空间与内核空间辅助字段 */
unsigned long vm_truncate_count; // 截断计数
struct vm_userfaultfd_ctx vm_userfaultfd_ctx; // 用户态缺页处理
};
vm_start 和 vm_end 定义了VMA的边界,vm_end - vm_start 即为该区域的大小。内核保证同一进程的所有VMA不会重叠,且按地址升序排列。
3.2 VMA标志位(vm_flags)详解
vm_flags 是一个位掩码,定义了该内存区域的访问权限和特殊行为。每个标志位对应一个宏定义,常见的标志如下:
| 标志宏 | 值 | 含义 |
|---|---|---|
VM_READ |
0x00000001 | 区域可读 |
VM_WRITE |
0x00000002 | 区域可写 |
VM_EXEC |
0x00000004 | 区域可执行 |
VM_SHARED |
0x00000008 | 区域可共享(多个进程映射同一物理页) |
VM_MAYREAD |
0x00000010 | 将来可能设置可读权限 |
VM_MAYWRITE |
0x00000020 | 将来可能设置可写权限 |
VM_MAYEXEC |
0x00000040 | 将来可能设置可执行权限 |
VM_GROWSDOWN |
0x00000100 | 区域可以向低地址增长(如栈) |
VM_GROWSUP |
0x00000200 | 区域可以向高地址增长 |
VM_PFNMAP |
0x00000400 | 区域映射的是物理页帧号,而非页缓存 |
VM_IO |
0x00001000 | 区域映射的是I/O设备内存 |
VM_DONTEXPAND |
0x00002000 | 禁止通过mremap()扩展该区域 |
VM_DONTDUMP |
0x00004000 | 该区域不包含在coredump中 |
VM_ACCOUNT |
0x00100000 | 该区域占用进程的虚拟内存限额 |
VM_HUGETLB |
0x00400000 | 该区域使用大页(HugeTLB) |
pmap -x <pid> 查看进程的VMA信息时,每一行对应一个VMA。其中权限列 rwxp 分别对应 VM_READ、VM_WRITE、VM_EXEC、VM_SHARED 的组合。
3.3 VMA的双重组织方式:红黑树与链表
内核使用两种数据结构同时组织进程的所有VMA,以满足不同的查找需求:
- 链表(vm_list): 按地址升序连接所有VMA,用于顺序遍历整个地址空间。链表头存储在
mm_struct->mmap中。 - 红黑树(vm_rb): 以
vm_start为键值构建平衡二叉搜索树,用于快速查找指定虚拟地址所属的VMA。红黑树根节点存储在mm_struct->mm_rb中。
两种组织方式共享同一组VMA对象,通过结构体中的 vm_list 和 vm_rb 字段分别链接。这种设计兼顾了遍历效率和查找效率:
| 操作 | 使用数据结构 | 时间复杂度 | 典型场景 |
|---|---|---|---|
| 顺序遍历所有VMA | 链表 | O(n) | /proc/<pid>/maps 读取、coredump生成 |
| 查找地址所属VMA | 红黑树 | O(log n) | 缺页异常处理、munmap()、mprotect() |
| 插入/删除VMA | 两者同时更新 | O(log n) | mmap()、munmap()、brk() |
3.4 VMA双重组织SVG结构图
下图展示了进程 mm_struct 中VMA的双重组织方式:链表将所有VMA按地址顺序串联,红黑树则提供快速查找能力。
3.5 VMA操作函数集(vm_operations_struct)
每个VMA可以关联一组操作函数,定义在 struct vm_operations_struct 中。这些函数是VMA与内核内存管理子系统的回调接口,在缺页、页面回收、页面迁移等事件中被调用。
struct vm_operations_struct {
void (*open)(struct vm_area_struct *vma); // VMA被复制时调用
void (*close)(struct vm_area_struct *vma); // VMA被删除时调用
vm_fault_t (*fault)(struct vm_fault *vmf); // 缺页处理(核心函数)
vm_fault_t (*page_mkwrite)(struct vm_fault *vmf); // 页面写保护触发
vm_fault_t (*pfn_mkwrite)(struct vm_fault *vmf); // PFN映射写保护触发
int (*access)(struct vm_area_struct *vma, unsigned long addr,
void *buf, int len, int write); // 跨进程内存访问(ptrace等)
const char *(*name)(struct vm_area_struct *vma); // 返回VMA名称(/proc/maps显示)
#ifdef CONFIG_NUMA
int (*set_policy)(struct vm_area_struct *vma, struct mempolicy *new);
struct mempolicy *(*get_policy)(struct vm_area_struct *vma, unsigned long addr);
#endif
};
fault() 是VMA最重要的操作函数。当进程访问一个尚未分配物理页的虚拟地址时,内核会调用该VMA的 fault() 函数来分配物理页并建立页表映射。文件映射的VMA会调用文件系统的 fault() 实现,匿名映射则调用内核的缺页处理函数。
3.6 VMA的生命周期与典型操作
VMA的创建、查找、合并、分裂和删除是内存管理的基本操作,下表总结了这些操作的触发场景和核心函数:
| 操作 | 触发系统调用 | 核心内核函数 | 说明 |
|---|---|---|---|
| 创建VMA | mmap()、brk() |
do_mmap() → vma_merge() / insert_vm_struct() |
尝试与相邻VMA合并,否则新建节点插入红黑树和链表 |
| 查找VMA | 缺页异常、munmap() |
find_vma()、find_vma_intersection() |
红黑树查找,返回包含指定地址或大于指定地址的第一个VMA |
| 合并VMA | mmap()(相邻区域) |
vma_merge() |
当新映射与现有VMA相邻且权限一致时,合并为一个VMA |
| 分裂VMA | mprotect()、munmap()(部分区域) |
split_vma() |
将一个VMA在指定地址处分裂为两个VMA |
| 删除VMA | munmap()、进程退出 |
do_munmap() → remove_vma() |
从红黑树和链表中移除,释放关联资源 |
mmap() 和 munmap() 操作会导致VMA数量激增,增加红黑树查找和链表遍历的开销。对于需要大量内存映射的场景(如JVM、数据库),建议使用大页(HugeTLB)或预分配大块内存来减少VMA数量。
3.7 VMA与进程地址空间布局
一个典型的Linux用户态进程包含以下几种类型的VMA:
- 代码段(Text): 映射可执行文件的代码部分,权限为
r-xp(读、执行、私有)。 - 数据段(Data): 映射已初始化的全局变量,权限为
rw-p(读、写、私有)。 - BSS段: 映射未初始化的全局变量(匿名映射),权限为
rw-p。 - 堆(Heap): 通过
brk()扩展的匿名映射,权限为rw-p,支持向高地址增长。 - 栈(Stack): 自动创建的匿名映射,权限为
rw-p,支持向低地址增长(VM_GROWSDOWN)。 - 共享库: 映射动态链接库的代码和数据段,权限为
r-xp或rw-p。 - 共享内存: 通过
mmap()创建的MAP_SHARED映射,用于进程间通信。 - vdso/vvar: 内核映射的虚拟动态共享对象,用于加速系统调用。
cat /proc/<pid>/maps 或 pmap -x <pid> 可以查看进程的所有VMA信息,包括地址范围、权限、偏移量、设备号和文件路径。
3.8 VMA操作流程SVG图:缺页异常处理
下图展示了当进程访问一个尚未分配物理页的虚拟地址时,内核如何通过VMA的红黑树查找和操作函数集完成缺页处理:
3.9 VMA与反向映射
VMA还参与了反向映射(Reverse Mapping)机制,用于在页面回收或迁移时快速找到映射了某个物理页的所有VMA。对于匿名页,VMA通过 anon_vma 和 anon_vma_chain 链接到反向映射结构;对于文件页,则通过 address_space 的基数树(xarray)进行反向查找。
3.10 本章小结
vm_area_struct 是Linux内核内存管理的基石数据结构,它通过红黑树和链表双重组织方式,在查找效率和遍历效率之间取得了平衡。理解VMA的结构、标志位、操作函数集以及生命周期管理,是深入掌握进程地址空间、缺页异常处理、内存映射和反向映射等核心机制的前提。