3. 内存描述符 mm_struct:进程地址空间描述

在 Linux 内核中,每个进程(包括线程)都拥有一个独立的虚拟地址空间。内核使用 mm_struct 结构体来描述和管理这个地址空间。它是内存管理子系统中最重要的数据结构之一,连接了进程、虚拟内存区域(VMA)和页表。

核心概念: mm_struct 是进程地址空间的“总指挥”,它记录了进程的代码段、数据段、堆、栈、内存映射等所有虚拟内存区域的布局和属性。

3.1 进程地址空间布局

一个典型的用户态进程的虚拟地址空间布局如下(以 x86-64 架构为例,地址空间分为用户空间和内核空间):

进程虚拟地址空间布局 (x86-64) 内核空间 (0xFFFF8000 00000000 - 0xFFFFFFFF FFFFFFFF) 内核代码、数据、模块、页表等(所有进程共享) 用户空间 (0x00000000 00000000 - 0x00007FFF FFFFFFFF) 栈 (Stack) — 向下增长 内存映射段 (Memory Mapping Segment) — 文件映射、共享库、mmap 堆 (Heap) — 向上增长 (brk/sbrk) BSS 段 (未初始化全局/静态变量) 数据段 (已初始化全局/静态变量) 代码段 (Text Segment) — 只读、可执行 栈向下 堆向上

3.2 mm_struct 关键字段解析

mm_struct 定义在 include/linux/mm_types.h 中。以下是其核心字段的解析:

字段名 类型 描述
mmap struct vm_area_struct * 指向虚拟内存区域(VMA)链表的头节点,用于遍历进程的所有内存区域。
mm_rb struct rb_root VMA 的红黑树根节点,用于快速查找特定地址所在的 VMA(时间复杂度 O(log n))。
pgd pgd_t * 指向进程页全局目录(Page Global Directory)的指针,是页表层次结构的根。
mm_users atomic_t 共享该 mm_struct 的用户进程/线程数量(包括内核线程借用的情况)。
mm_count atomic_t mm_struct 结构体本身的内存引用计数,用于决定何时释放该结构体。
map_count int 当前进程中 VMA 的数量。
start_code, end_code unsigned long 代码段的起始和结束虚拟地址。
start_data, end_data unsigned long 数据段的起始和结束虚拟地址。
start_brk, brk unsigned long 堆的起始地址(start_brk)和当前堆的结束地址(brk,即堆顶)。
start_stack unsigned long 栈的起始地址。
arg_start, arg_end unsigned long 命令行参数(argv)在栈中的起始和结束地址。
env_start, env_end unsigned long 环境变量(envp)在栈中的起始和结束地址。
total_vm unsigned long 进程总共使用的虚拟内存页数(所有 VMA 的页数总和)。
locked_vm unsigned long 被锁定在物理内存中、不允许换出的页数(mlock)。
rss_stat struct mm_rss_stat 记录进程的常驻内存(Resident Set Size)统计信息。
设计要点: mmap 链表和 mm_rb 红黑树同时维护了 VMA 的两种组织方式。链表用于遍历所有 VMA(如 /proc/pid/maps 的读取),红黑树用于按地址快速查找(如缺页异常处理)。

3.3 mm_struct 与进程、线程的关系

在 Linux 中,进程和线程都使用 task_struct 表示。它们与 mm_struct 的关系如下:

  • 进程: 每个进程拥有一个独立的 mm_struct,即独立的地址空间。
  • 线程: 同一进程内的所有线程共享同一个 mm_struct,即共享地址空间。线程的 task_struct 中的 mm 指针指向同一个 mm_struct。
  • 内核线程: 内核线程没有用户地址空间,其 task_struct 中的 mm 字段为 NULL。
进程、线程与 mm_struct 的关系 进程 A (task_struct) mm_struct A 进程 B (task_struct) mm_struct B 线程 A1 (task_struct) 线程 A2 (task_struct) 内核线程 (task_struct) mm = NULL

3.4 内核线程与用户进程的 mm 区别

内核线程(kthread)和用户进程在 mm_struct 的使用上存在根本区别:

特性 用户进程 内核线程
mm 字段 指向自己独立的 mm_struct 通常为 NULL
地址空间 拥有完整的用户空间 + 内核空间 只拥有内核空间,没有用户空间
页表 使用自己的页表(通过 pgd) 直接使用内核页表(init_mm.pgd)
缺页异常 可以触发用户空间缺页,内核会为其分配物理页 不会触发用户空间缺页;内核空间缺页通常是 BUG
VMA 有完整的 VMA 链表和红黑树 没有 VMA(因为不需要管理用户空间)
内存回收 参与用户页面回收和交换 不参与用户页面回收,但内核内存可回收
上下文切换 切换 mm 和 pgd,刷新 TLB 不切换 mm,但会借用上一个进程的 mm(active_mm)
关键机制:active_mm
当内核线程被调度运行时,它的 mm 为 NULL。为了访问内核空间,内核线程会“借用”上一个用户进程的 mm_struct,并将其指针保存在 task_struct 的 active_mm 字段中。这避免了为内核线程单独维护页表,同时保证了 TLB 的正确性。

3.5 mm_struct 的生命周期与操作

mm_struct 的生命周期由引用计数管理:

  1. 创建: 通过 fork() 系统调用创建进程时,内核调用 copy_mm() 复制父进程的 mm_struct(写时复制)。
  2. 共享: 通过 clone() 创建线程时,CLONE_VM 标志使子线程共享父线程的 mm_struct(mm_users 递增)。
  3. 借用: 内核线程调度时,设置 active_mm 借用其他进程的 mm_struct(mm_count 递增)。
  4. 释放: 当 mm_users 降为 0 时,内核开始清理地址空间(释放所有 VMA 和页表)。当 mm_count 也降为 0 时,mm_struct 本身被释放。
关键函数调用链:
fork() → copy_process() → copy_mm() → dup_mm() → dup_mmap()(复制 VMA)
exit() → exit_mm() → mmput() → mmdrop() → free_mm()

3.6 总结