3. VMA 数据结构:struct vm_area_struct 核心字段解析

虚拟内存区域(VMA)是 Linux 内核管理进程地址空间的核心抽象。每个 VMA 描述一段连续的、具有相同属性的虚拟地址区间。内核使用 struct vm_area_struct 结构体来管理这些区域,它是内存映射、缺页中断、权限检查等操作的基础数据结构。

3.1 VMA 在进程地址空间中的角色

每个进程的 mm_struct 维护一棵红黑树(mmap 链表与红黑树并存),树中的每个节点就是一个 VMA。VMA 定义了虚拟地址的起始、结束、权限、映射文件、操作函数集等关键信息。

核心概念: VMA 是虚拟地址空间管理的“最小管理单元”。内核通过 VMA 实现按需分配、按需加载、写时复制(COW)等高级内存特性。
进程地址空间与 VMA 关系图 mm_struct mmap (链表头) 红黑树根节点 vm_area_struct VMA 1: 代码段 0x400000 - 0x401000 VMA 2: 堆 0x402000 - 0x403000 VMA 3: 栈 0x7fff0000 - 0x7fff1000 说明: 每个 VMA 对应一段连续的虚拟地址区间,具有相同的权限和映射属性。 内核通过红黑树快速查找给定虚拟地址所属的 VMA。

3.2 核心字段详解

struct vm_area_struct 包含数十个字段,以下是最核心的 6 个字段,它们直接决定了 VMA 的行为和属性。

字段名 类型 作用描述 典型值/示例
vm_start unsigned long VMA 覆盖的虚拟地址起始(包含) 0x400000
vm_end unsigned long VMA 覆盖的虚拟地址结束(不包含) 0x401000
vm_flags unsigned long VMA 的访问权限与属性标志位 VM_READ | VM_WRITE | VM_EXEC
vm_ops const struct vm_operations_struct * 指向 VMA 操作函数集的指针 &file_ops 或 NULL
vm_file struct file * 映射的文件对象(文件映射时有效) NULL(匿名映射)
vm_next / vm_prev struct vm_area_struct * 链表指针,用于遍历进程的所有 VMA 双向链表节点

3.3 vm_start 与 vm_end:地址区间定义

这两个字段定义了 VMA 管理的虚拟地址范围:

  • 区间表示: [vm_start, vm_end),即包含起始地址,不包含结束地址。
  • 对齐要求: 起始地址必须按页对齐(PAGE_SIZE 对齐),长度是页大小的整数倍。
  • 区间不重叠: 同一进程的不同 VMA 覆盖的地址区间不会重叠。
调试技巧: 通过 /proc/[pid]/maps 可以查看进程的所有 VMA 信息,每行对应一个 VMA,格式为 vm_start-vm_end 权限 偏移 主设备:次设备 inode 路径。

3.4 vm_flags:权限与属性标志

vm_flags 是一个位图字段,每个比特位代表一种属性。以下是常用的标志位:

标志位 值 含义 典型场景
VM_READ 0x00000001 可读 所有映射
VM_WRITE 0x00000002 可写 堆、栈、数据段
VM_EXEC 0x00000004 可执行 代码段
VM_SHARED 0x00000008 共享映射(多进程共享物理页) 共享内存、文件映射
VM_GROWSDOWN 0x00000100 可向下增长(栈) 栈区域
VM_DENYWRITE 0x00000800 禁止其他进程写映射文件 文件映射
VM_IO 0x00004000 映射设备 I/O 空间 设备驱动 mmap
注意: vm_flags 中的权限位与硬件页表项中的权限位是两套独立的机制。VMA 的权限是“软件权限”,页表项中的权限是“硬件权限”。缺页中断处理时,内核会检查 VMA 权限,然后设置页表项权限。
vm_flags 位图结构示意 31 ... 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 位 0 VM_READ 位 1 VM_WRITE 位 2 VM_EXEC 位 3 VM_SHARED 位 8 VM_GROWSDOWN 位 14 VM_IO vm_flags 是一个 32 位(或 64 位)位图,每个比特位代表一种 VMA 属性。 内核通过位运算(&、|、~)快速检查和设置属性。

3.5 vm_ops:VMA 操作函数集

vm_ops 是一个指向 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 等)
};
关键点: fault 回调是缺页中断处理的核心。当进程访问一个尚未分配物理页的虚拟地址时,内核会调用 VMA 的 fault 函数来分配物理页并建立页表映射。文件映射的 fault 函数会从文件系统读取数据;匿名映射的 fault 函数会分配零填充页。

3.6 其他重要字段

字段 说明
vm_pgoff 文件映射时的文件偏移(以页为单位),匿名映射时为 0
vm_rb 红黑树节点,用于快速查找 VMA
shared 共享映射的链表,用于管理共享物理页
anon_vma 匿名映射的反向映射结构,用于写时复制
vm_mm 指向所属进程的 mm_struct 的指针

3.7 VMA 操作流程示例:缺页中断处理

当进程访问一个虚拟地址,但页表中没有对应的页表项时,CPU 触发缺页中断。内核的缺页中断处理函数 handle_mm_fault 会执行以下步骤:

  1. 查找 VMA: 通过红黑树查找虚拟地址所属的 VMA。
  2. 权限检查: 检查访问类型(读/写/执行)是否与 vm_flags 匹配。
  3. 调用 fault 回调: 调用 VMA 的 vm_ops->fault 函数。
  4. 分配物理页: fault 函数分配物理页,并从文件读取数据(文件映射)或填充零(匿名映射)。
  5. 建立页表: 将物理页的地址写入页表项,设置权限位。
基于 VMA 的缺页中断处理流程 1. 触发缺页中断 CPU 访问无效页表项 2. 查找 VMA 红黑树查找 vm_area_struct 3. 权限检查 检查 vm_flags 与访问类型 4. 调用 fault 回调 vm_ops->fault(vmf) 5. 建立页表映射 分配物理页 + 设置页表项

3.8 总结:VMA 字段与内存管理的关系

内存管理功能 依赖的 VMA 字段 说明
地址空间划分 vm_start, vm_end 定义进程地址空间的布局
权限控制 vm_flags 决定可读/可写/可执行权限
缺页中断处理 vm_ops->fault 按需分配物理页
写时复制 anon_vma, vm_flags fork 后父子进程共享物理页
文件映射 vm_file, vm_pgoff 将文件内容映射到进程地址空间
栈自动增长 VM_GROWSDOWN 栈向下扩展时自动创建新 VMA
学习建议: 阅读内核源码时,重点关注 mm/mmap.c 中的 do_mmap 函数(创建 VMA)和 mm/memory.c 中的 handle_mm_fault 函数(缺页中断处理)。这两个函数是 VMA 相关逻辑的核心入口。