4. 虚拟内存区域 VMA:数据结构、操作与页表关联
虚拟内存区域(VMA,Virtual Memory Area)是 Linux 内核管理进程地址空间的核心抽象。每个 VMA 描述一段连续的、具有相同权限和映射属性的虚拟地址区间。内核通过 VMA 链表和红黑树来组织这些区域,实现高效查找、插入和权限检查。本章深入解析 VMA 的数据结构、关键操作函数以及 VMA 与页表之间的关联机制。
4.1 VMA 数据结构
VMA 在内核中使用 struct vm_area_struct 结构体表示,定义在 include/linux/mm_types.h 中。该结构体包含了描述一段虚拟内存区域所需的所有信息。
核心字段说明:
- vm_start / vm_end:虚拟地址区间的起始和结束地址(包含起始,不包含结束)。
- vm_flags:权限标志位,如
VM_READ、VM_WRITE、VM_EXEC、VM_SHARED等。 - vm_next / vm_prev:双向链表指针,用于按地址顺序遍历所有 VMA。
- vm_rb:红黑树节点,用于在进程的地址空间红黑树中快速查找。
- vm_mm:指向所属的
mm_struct(内存描述符)的指针。 - vm_ops:指向
vm_operations_struct结构体的指针,定义了针对该 VMA 的特殊操作(如缺页处理)。 - vm_file:如果 VMA 映射了文件,指向对应的
file结构体;匿名映射则为 NULL。 - vm_pgoff:文件映射时的页偏移量。
4.2 VMA 操作函数
内核提供了一组核心函数来操作 VMA,其中 find_vma 和 insert_vm_struct 是最常用的两个。
4.2.1 find_vma:查找虚拟地址所属的 VMA
find_vma 函数用于查找给定虚拟地址所属的 VMA。它首先在红黑树中搜索,如果找到则返回该 VMA;否则返回地址空间中第一个起始地址大于给定地址的 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;
/* 检查缓存(最近访问的 VMA) */
vma = vmacache_find(mm, addr);
if (likely(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)
vmacache_update(addr, vma);
return vma;
}
性能优化:内核使用 vmacache(VMA 缓存)来加速查找。对于具有良好局部性的访问模式,缓存命中率很高,可以避免红黑树遍历的开销。
4.2.2 insert_vm_struct:插入新的 VMA
insert_vm_struct 函数负责将一个新的 VMA 插入到进程的地址空间中。它同时维护链表和红黑树的一致性。
/* mm/mmap.c */
int insert_vm_struct(struct mm_struct *mm, struct vm_area_struct *vma)
{
struct vm_area_struct *prev;
struct rb_node **rb_link, *rb_parent;
/* 1. 查找插入位置(红黑树和链表) */
if (find_vma_links(mm, vma->vm_start, vma->vm_end,
&prev, &rb_link, &rb_parent))
return -ENOMEM;
/* 2. 将 VMA 插入链表 */
if ((vma->vm_next = prev ? prev->vm_next : mm->mmap))
vma->vm_next->vm_prev = vma;
if (prev)
prev->vm_next = vma;
else
mm->mmap = vma;
vma->vm_prev = prev;
/* 3. 将 VMA 插入红黑树 */
rb_link_node(&vma->vm_rb, rb_parent, rb_link);
rb_insert_color(&vma->vm_rb, &mm->mm_rb);
return 0;
}
4.3 VMA 与页表的关联
VMA 是进程地址空间的逻辑描述,而页表是 MMU 实际使用的硬件数据结构。两者通过缺页异常(page fault)机制紧密关联。
4.3.1 关联机制
| 组件 | 作用 | 关联方式 |
|---|---|---|
| VMA | 描述虚拟地址区间的属性(权限、映射类型) | 提供缺页处理所需的上下文信息 |
| 页表 | 实现虚拟地址到物理地址的转换 | 由缺页异常处理程序填充 |
| 缺页异常 | 当访问的虚拟地址未建立页表映射时触发 | 通过 find_vma 查找对应的 VMA |
| vm_ops->fault | 文件映射 VMA 的缺页回调函数 | 负责读取文件内容到物理页面 |
4.3.2 缺页处理流程
当 CPU 访问一个尚未建立页表映射的虚拟地址时,MMU 触发缺页异常,内核执行以下流程:
- 地址检查:调用
find_vma查找地址所属的 VMA。 - 权限验证:检查 VMA 的
vm_flags是否允许当前操作(读/写/执行)。 - 物理页面分配:通过伙伴系统分配一个物理页面。
- 页表填充:将物理页框号写入页表项,设置权限位。
- TLB 刷新:使 TLB 中对应的缓存项失效。
4.3.3 文件映射与匿名映射
VMA 根据映射类型分为文件映射和匿名映射,两者的缺页处理方式不同:
| 特性 | 文件映射 VMA | 匿名映射 VMA |
|---|---|---|
| vm_file | 指向映射的文件 | NULL |
| 缺页处理 | 调用 vm_ops->fault 从文件读取数据 |
分配零填充的物理页面 |
| 回写 | 脏页可写回文件 | 不需要回写(交换分区除外) |
| 典型场景 | 可执行文件、共享库、mmap 文件 | 堆、栈、匿名 mmap |
关键概念:VMA 与页表之间没有直接的指针关联。VMA 是软件层面的管理结构,页表是硬件使用的转换结构。两者通过缺页异常处理程序在运行时动态建立联系。当进程访问一个地址时,MMU 首先查询页表;如果页表项无效,则触发缺页异常,内核使用 VMA 提供的信息来决定如何填充页表。
4.4 本章小结
- VMA 数据结构:
struct vm_area_struct包含地址区间、权限标志、链表和红黑树节点,是进程地址空间管理的基本单元。 - VMA 操作函数:
find_vma通过红黑树和缓存实现高效查找;insert_vm_struct同时维护链表和红黑树的一致性。 - VMA 与页表关联:通过缺页异常机制动态建立,VMA 提供上下文信息,页表实现地址转换。
- 映射类型:文件映射和匿名映射在缺页处理方式上存在本质区别。