Linux 虚拟内存区域 VMA 管理:vm_area_struct 与红黑树区间查找优化
Linux 虚拟内存区域 VMA 管理:vm_area_struct 与红黑树区间查找优化

在 Linux 操作系统中,每个用户态进程都拥有独立的 64 位(或 32 位)虚拟地址空间。然而,这一庞大的地址空间绝大部分都是未被使用的荒漠,只有零散的区间被赋予了具体的物理含义与权限——例如代码段(只读可执行)、数据与堆段(可读写)、动态链接库的 mmap 映射区以及用户栈。
内核如何高效管理这数十甚至上万个不连续的虚拟内存区间?当 CPU 产生缺页异常(Page Fault)时,内核如何在数微秒内判定某个地址是否合法并找到对应的物理映射策略?本文深入剖析 vm_area_struct(VMA)的核心数据结构演进与红黑树/Maple Tree 区间查找机制。
一、VMA 与 vm_area_struct 核心结构体走读
在内核中,每一个独立的、权限一致的连续虚拟内存区间都被抽象为一个 struct vm_area_struct(简称 VMA)。
// include/linux/mm_types.h (核心字段精简)
struct vm_area_struct {
/* 虚拟地址区间的起始与结束地址 (页对齐) */
unsigned long vm_start; /* 区间首地址 (包含) */
unsigned long vm_end; /* 区间末地址 (不包含) */
struct mm_struct *vm_mm; /* 所属进程的内存描述符 */
pgprot_t vm_page_prot; /* 底层页表项的硬件访问保护属性 */
unsigned long vm_flags; /* VMA 标志位: VM_READ, VM_WRITE, VM_EXEC, VM_SHARED, VM_GROWSDOWN */
/* 区间树节点 (在旧内核中是红黑树 rb_node,现代内核中整合入 Maple Tree) */
struct rb_node vm_rb;
/* 匿名内存或文件映射的回调操作集合 */
const struct vm_operations_struct *vm_ops;
/* 映射的文件与文件偏移量 (若是文件映射) */
struct file * vm_file;
unsigned long vm_pgoff; /* 文件页偏移量 (以 PAGE_SIZE 为单位) */
void * vm_private_data; /* 驱动或共享内存私有数据 */
};
进程虚拟地址空间 (mm_struct)
0x00000000 ┌───────────────────────────────────────┐
│ 代码段 (VMA 1: READ | EXEC) │
├───────────────────────────────────────┤
│ 数据段 / 堆 (VMA 2: READ | WRITE) │
├───────────────────────────────────────┤
│ ... 未分配空间 (访问触发 SIGSEGV) ... │
├───────────────────────────────────────┤
│ mmap 共享库映射 (VMA 3: READ | WRITE) │
├───────────────────────────────────────┤
│ 用户栈 (VMA 4: READ | WRITE | GROWSDOWN)
0x7FFFFFFF └───────────────────────────────────────┘
二、VMA 查找机制的演进:从单链表到 Maple Tree
当进程执行 malloc、mmap、加载动态库或触发缺页异常时,内核必须高频执行两个操作:
- 区间查找(Lookup):给定一个虚拟地址
addr,找到满足vm_start <= addr < vm_end的 VMA。 - 空闲空洞寻找(Gap Finding):在地址空间中找到一块足够大的未被占用的连续虚拟地址来容纳新的
mmap。
1. 早期单链表($O(N)$)
在 Linux 2.4 时代,所有的 VMA 仅通过双向链表 mm->mmap 串联。当 VMA 数量较少时运行良好,但当大型程序(如 JVM、Oracle 数据库)映射了数万个 VMA 时,线性遍历导致缺页异常处理极度缓慢。
2. 红黑树 + 链表双向组织($O(\log N)$)
Linux 2.6 到 5.x 时代引入了增强型红黑树(Augmented Red-Black Tree):
- 以
vm_start作为红黑树排序的 Key,将查找时间复杂度降低至 $O(\log N)$。 - 每个树节点额外记录子树中最大的空闲空洞大小(
rb_subtree_gap),将寻找可用地址空间的时间复杂度同样优化到 $O(\log N)$。
[ Root VMA (0x7f000000) ]
/ \
/ \
[ Left VMA (0x00400000) ] [ Right VMA (0x7fff0000) ]
3. Linux 6.1+ 革命:Maple Tree(RCU 友好 B-Tree)
虽然红黑树查找是 $O(\log N)$,但其在平衡旋转时对节点锁(mmap_lock)的写争用极其严重。Linux 6.1 引入了 Maple Tree,一种专门为区间查找设计的 B-Tree 变种,原生支持无锁 RCU 读并发,显著消除了多线程高并发分配内存时的锁阻塞。
三、缺页异常中的 find_vma 核心源码走读
当 CPU 访问非法未映射地址产生 Page Fault 时,中断向量最终进入 do_page_fault() 并调用 find_vma():
// mm/mmap.c (核心逻辑精简)
struct vm_area_struct *find_vma(struct mm_struct *mm, unsigned long addr)
{
struct vm_area_struct *vma = NULL;
if (mm) {
/* 1. 优先检查最近一次命中的 VMA 缓存 (vmacache / LRU) */
vma = vmacache_find(mm, addr);
if (likely(vma))
return vma;
/* 2. 遍历红黑树/Maple Tree 进行区间匹配 */
struct rb_node *rb_node = mm->mm_rb.rb_node;
while (rb_node) {
struct vm_area_struct *vma_tmp;
vma_tmp = rb_entry(rb_node, struct vm_area_struct, vm_rb);
if (vma_tmp->vm_end > addr) {
vma = vma_tmp;
if (vma_tmp->vm_start <= addr)
break; /* 精准命中目标区间 */
rb_node = rb_node->rb_left;
} else {
rb_node = rb_node->rb_right;
}
}
if (vma)
vmacache_update(addr, vma);
}
return vma;
}
如果 find_vma 返回的 vma->vm_start > addr,内核会进一步检查该 VMA 是否具有 VM_GROWSDOWN 属性(即栈空间向下自增)。若确实是用户栈溢出,则触发扩展栈 VMA 的逻辑;否则判定为段错误,向进程发送 SIGSEGV。
四、生产环境调优与排查实战
在运行高并发 Go 服务、Elasticsearch 或大型 AI 推理服务时,频繁的 mmap 分配可能触碰系统的安全阈值:
# 1. 查看某个进程的实际 VMA 列表与内存段分布
cat /proc/12345/maps | head -n 20
# 2. 查看当前进程已分配的 VMA 数量
wc -l /proc/12345/maps
# 3. 避免 max virtual memory areas exceeded 报错
# Elasticsearch / JVM / vLLM 默认建议调大:
sysctl -w vm.max_map_count=262144
优化建议:
- VMA 合并机制(VMA Merging):内核会在
mmap或mprotect时自动尝试将地址相邻且权限(vm_flags、vm_file)完全一致的 VMA 合并为一个大区间。在应用层设计内存池时,尽量申请连续内存,保持权限一致,避免过度碎片化导致 VMA 树深度剧增。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)