2. mm_struct 数据结构核心字段解析

本章深入剖析 mm_struct 中决定进程地址空间布局、页表管理、引用计数与并发控制的关键成员变量。理解这些字段是掌握内存描述符运作机制的基础。

2.1 地址空间组织:mmap 与 mm_rb

进程的虚拟地址空间由一组连续的虚拟内存区域(VMA)构成。mm_struct 通过两种数据结构管理这些 VMA:

  • mmap:单向链表,按虚拟地址升序链接所有 VMA,便于遍历全部区域。
  • mm_rb:红黑树根节点,以 VMA 起始地址为键值组织,提供 O(log n) 的快速查找。

两种结构共存:链表用于顺序访问(如 /proc/<pid>/maps),红黑树用于按地址精确查找(如缺页异常处理)。

关键设计: 链表与红黑树共享同一组 VMA 对象,不重复存储。每个 VMA 同时维护链表指针(vm_next, vm_prev)和红黑树节点(vm_rb)。
性能提示: 当进程拥有大量 VMA(如长期运行的数据库、JVM),红黑树的查找效率远高于链表。内核在 find_vma() 中优先使用红黑树。
mm_struct 地址空间组织:mmap 链表 + mm_rb 红黑树 mm_struct mmap mm_rb VMA 1 (0x1000-0x2000) VMA 2 (0x3000-0x4000) VMA 3 (0x5000-0x6000) VMA 2 (根节点) VMA 1 (左子树) VMA 3 (右子树) 链表提供顺序遍历能力,红黑树提供 O(log n) 快速查找,两者共享同一组 VMA 对象

2.2 页表根指针:pgd

pgd 指向进程页全局目录(Page Global Directory)的物理地址,是硬件 MMU 进行地址翻译的入口点。每次进程切换(context switch)时,内核将新进程的 pgd 写入 CPU 的页表基址寄存器(如 x86 的 CR3)。

字段 类型 描述
pgd pgd_t * 指向进程私有的页全局目录,每进程独立
p4d p4d_t * 四级页表模式下指向第 4 级目录(x86_64 使用)
pud pud_t * 页上级目录指针
pmd pmd_t * 页中间目录指针
pte pte_t * 页表项指针(最终映射物理页框)
注意: 内核线程(kernel thread)的 mm 为 NULL,它们借用上一个用户进程的 pgd 进行地址翻译。这是内核线程不拥有用户空间地址空间的原因。
四级页表翻译路径(x86_64) 虚拟地址 PGD P4D PUD PMD PTE 物理页框 mm_struct->pgd 指向 PGD 基址,进程切换时写入 CR3 寄存器 硬件 MMU 自动遍历各级页表完成虚拟地址到物理地址的转换

2.3 引用计数:mm_users 与 mm_count

mm_struct 使用两个原子计数器管理生命周期,分别对应不同粒度的引用场景:

字段 类型 初始值 递增场景 递减场景
mm_users atomic_t 1 clone(CLONE_VM)、get_task_mm() exit_mm()、mmput()
mm_count atomic_t 1 mmgrab()(内核模块长期引用) mmdrop()(物理释放)
核心区别: mm_users 统计共享地址空间的用户态进程数(包括线程);mm_count 统计所有持有 mm_struct 指针的内核引用。当 mm_users 降为 0 时释放用户空间页表,当 mm_count 降为 0 时释放 mm_struct 本身。
mm_users 与 mm_count 生命周期管理 mm_struct 引用计数管理 mm_users 用户态进程/线程计数 mm_count 内核引用计数 mm_users == 0 → 释放页表 mm_count == 0 → 释放 mm_struct

2.4 其他关键字段速览

字段 类型 作用
start_brk / brk unsigned long 堆的起始与当前边界,由 brk() 系统调用管理
start_stack unsigned long 用户栈的起始地址(栈顶)
arg_start / arg_end unsigned long 命令行参数在用户栈中的位置
env_start / env_end unsigned long 环境变量在用户栈中的位置
map_count unsigned long 当前 VMA 数量,用于限制 max_map_count
total_vm unsigned long 进程虚拟内存总页数(包含所有映射)
locked_vm unsigned long mlock() 锁定的不可换出页数
def_flags unsigned long VMA 默认标志位(如 VM_MAYREAD / VM_MAYWRITE)
调试技巧: 通过 /proc/<pid>/maps 查看 VMA 链表内容,通过 /proc/<pid>/smaps 获取每个 VMA 的详细内存统计(RSS、PSS、脏页等)。

2.5 字段关系总览

mm_struct 核心字段关系总览 mm_struct mmap / mm_rb pgd (页表根) mm_users/count start_brk/brk 管理 VMA 链表+红黑树 硬件地址翻译 进程切换加载 CR3 生命周期控制 用户/内核引用分离 堆/栈边界 brk/mmap 动态调整 mm_struct 是进程虚拟地址空间的唯一描述符,所有内存操作均围绕这些字段展开

2.6 代码示例:访问 mm_struct 核心字段

以下内核模块演示如何从当前进程的 mm_struct 中读取关键字段:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/sched.h>
#include <linux/mm_types.h>

static int __init mm_fields_init(void)
{
    struct mm_struct *mm = current->mm;
    if (!mm) {
        pr_err("kernel thread has no mm\n");
        return -EINVAL;
    }

    pr_info("PID: %d\n", current->pid);
    pr_info("mmap list head: %px\n", mm->mmap);
    pr_info("mm_rb root: %px\n", &mm->mm_rb);
    pr_info("pgd: %px\n", mm->pgd);
    pr_info("mm_users: %d\n", atomic_read(&mm->mm_users));
    pr_info("mm_count: %d\n", atomic_read(&mm->mm_count));
    pr_info("total_vm: %lu pages (%lu KB)\n",
            mm->total_vm, mm->total_vm * 4);
    pr_info("map_count: %lu\n", mm->map_count);
    pr_info("start_brk: 0x%lx, brk: 0x%lx\n",
            mm->start_brk, mm->brk);
    pr_info("start_stack: 0x%lx\n", mm->start_stack);

    return 0;
}

static void __exit mm_fields_exit(void)
{
    pr_info("module unloaded\n");
}

module_init(mm_fields_init);
module_exit(mm_fields_exit);
MODULE_LICENSE("GPL");
安全注意: 访问 current->mm 前必须检查是否为 NULL。内核线程(如 kworker)的 mm 字段为 NULL,直接解引用会导致 Oops。