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() 中优先使用红黑树。
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 进行地址翻译。这是内核线程不拥有用户空间地址空间的原因。
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 本身。
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 字段关系总览
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。