Linux内核 mm_struct

mm_struct 是 Linux 内核描述进程地址空间 的核心数据结构,它管理着进程的虚拟内存映射、页表、内存访问权限、地址空间布局等关键信息。每个用户态进程(或内核线程共享的地址空间)都会关联一个 mm_struct,内核通过它实现对进程内存的分配、回收、保护等操作。

  • 进程地址空间的唯一标识 :每个独立的用户态进程都有自己的 mm_struct,内核线程则默认共享内核的 init_mm(无独立地址空间)。
  • 页表的顶层管理入口mm_struct 指向进程的顶级页表(如 pgd 指针),是用户虚拟地址到物理地址映射的总入口。
  • 内存区域的组织容器 :通过内部的 vm_area_struct 链表,管理进程的所有虚拟内存区域(如代码段、数据段、堆、栈、内存映射文件等)。
  • 内存资源的统计与限制 :记录进程的内存使用量、页表项数量、锁定内存大小等,配合 rlimit 实现内存资源限制。

核心字段分类说明

字段类型 核心字段 作用
虚拟内存区域管理 /* 虚拟内存区域(VMA)的链表头 */ struct vm_area_struct *mmap; /* VMA 的红黑树根节点(加速查找) */ struct rb_root mm_rb; /* VMA 缓存序列号,优化查找 */ u32 vmacache_seqnum; mmap 是 VMA 链表头,mm_rb 是红黑树根(加速 VMA 查找),map_count 记录 VMA 数量
页表管理 /* 指向进程的页全局目录(PGD),页表的顶层入口 */ pgd_t *pgd; /* 保护页表的自旋锁 */ spinlock_t page_table_lock; pgd 指向顶级页表,page_table_lock 保护页表的并发修改
地址空间边界 /* 进程地址空间的最大大小(如 32 位进程为 3GB) */ unsigned long task_size; /* 进程内存映射区域的起始地址(默认 TASK_UNMAPPED_BASE) */ unsigned long mmap_base; /* 代码段、数据段的起止地址 */ unsigned long start_code, end_code, start_data, end_data; /* 堆、栈的起止地址 */ unsigned long start_brk, brk, start_stack; /* 命令行参数、环境变量的地址范围 */ unsigned long arg_start, arg_end, env_start, env_end; 定义进程地址空间的最大范围、映射基址、代码 / 数据 / 堆 / 栈的边界
内存统计 /* 进程总虚拟内存页数(单位:PAGE_SIZE) */ unsigned long total_vm; /* 被 mlock() 锁定的内存页数 */ unsigned long locked_vm; /* 进程的 RSS(驻留集大小)历史峰值 */ unsigned long hiwater_rss; /* 进程的虚拟内存大小历史峰值 */ unsigned long hiwater_vm; 统计进程的虚拟内存、锁定内存、RSS 峰值等资源使用情况
共享与引用计数 /* 使用该地址空间的进程数(共享计数,如线程共享) */ atomic_t mm_users; /* 地址空间的引用计数(销毁时减到 0 才释放) */ atomic_t mm_count; mm_users 是共享该地址空间的进程数(如线程),mm_count 是内核内部引用计数
架构相关 /* 架构相关的上下文信息(如 ARM64 的 ASID) */ mm_context_t context; 存储架构特定信息(如 ARM64 的地址空间标识符 ASID,x86 的 CR3 寄存器值)

核心关联数据结构

vm_area_struct(虚拟内存区域)

mm_struct 采用 双数据结构 管理 VMA,兼顾遍历效率和查找效率:

  • 链表(mmap 字段) :所有 VMA 以 vm_area_structvm_next 指针串联成单向链表,适合全量遍历(如进程退出时释放所有 VMA)。
  • 红黑树(mm_rb 字段) :所有 VMA 按虚拟地址范围挂载到红黑树中,适合精准查找(如缺页异常时,快速定位虚拟地址所属的 VMA)。

每个 VMA 对应进程地址空间中的一段连续虚拟地址区间,且具有相同的访问权限(如可读、可写、可执行)。

  • 例如:进程的代码段对应一个 VMA(PROT_READ|PROT_EXEC),堆对应一个 VMA(PROT_READ|PROT_WRITE)。
  • 内核通过 find_vma() 函数,根据虚拟地址快速查找对应的 VMA(红黑树查找时间复杂度 O (log n))。
    • 先尝试从 VMA 缓存vmacache)中查找,缓存命中则直接返回(O (1) 开销);
    • 缓存未命中时,遍历红黑树查找对应 VMA(O (log n) 开销);
    • 更新 VMA 缓存,提升下一次查找效率。

pgd_t(页全局目录)

pgdmm_struct 指向的顶级页表项,是虚拟地址翻译的入口:

  • 对于 64 位系统(如 ARM64),页表通常分为 4 级(PGD → PUD → PMD → PTE);
  • 当进程切换时,内核会将 task_struct->mm->pgd 加载到 CPU 的页表基址寄存器(如 ARM64 的 TTBR0_EL1),完成地址空间切换。

task_struct(进程控制块)

每个进程的 task_struct 中包含一个 mm 指针,指向该进程的 mm_struct

struct task_struct {

// ... 其他字段

struct mm_struct *mm; /* 进程的地址空间 */

struct mm_struct *active_mm; /* 运行时的地址空间(内核线程借用) */

// ... 其他字段

};

mmactive_mm 是两个与进程地址空间相关的核心指针,它们的设计目标是区分进程的「自有地址空间」和「运行时使用的地址空间」,尤其为内核线程的地址空间管理提供了高效的解决方案。

  • 用户态进程:mmactive_mm 指向同一个 mm_struct
  • 内核线程:无独立地址空间,mm=NULLactive_mm 借用前一个运行进程的 mm_struct

mm 指针

  • 作用 :指向进程私有的地址空间结构体,是用户态进程的「自有地址空间」标识。
  • 适用对象
    • 用户态进程 :拥有独立的虚拟地址空间,mm 指向其专属的 mm_struct,包含页表、VMA、内存统计等信息。
    • 内核线程 :没有独立的用户态地址空间,mm = NULL

active_mm 指针

  • 作用 :指向进程运行时实际使用的地址空间结构体 ,是 CPU 页表基址寄存器(如 ARM64 的 TTBR0_EL1)对应的 mm_struct
  • 适用对象
    • 用户态进程 :运行时使用的就是自有地址空间,因此 active_mm = mm
    • 内核线程 :无自有地址空间,需要借用前一个运行在该 CPU 上的用户态进程的 mm_struct ,此时 active_mm 指向被借用的 mm_struct

内核线程的地址空间管理

内核线程本身没有独立的用户态地址空间task_struct->mm = NULL),它通过 active_mm 指针借用前一个运行在该 CPU 上的用户态进程的 mm_struct ,从而复用其页表完成内核地址空间的访问,同时避免为内核线程创建独立 mm_struct 的内存开销。

Linux 内核的地址空间是全局共享的,具体体现在:

  1. 所有进程的页表(包括用户态进程的 mm_struct->pgd)都包含内核地址空间的映射 (如 ARM64 的 TTBR1_EL1 对应内核页表,x86_64 的高 1GB 地址)。
  2. 内核线程仅运行在内核态,只需要访问内核地址空间,不需要访问用户态地址空间

因此,内核线程可以直接复用任意用户态进程的 mm_struct 中的内核页表映射,无需创建自己的 mm_struct

因此内核设计了 active_mm 机制:

  1. 内核线程本身不拥有 mm_structmm = NULL)。
  2. 当内核线程在某 CPU 上运行时,复用该 CPU 上前一个用户态进程的 mm_struct 作为 active_mm
  3. 内核线程运行时,CPU 的页表基址寄存器仍指向被借用的 mm_struct->pgd,但由于内核线程只访问内核地址空间,不会触碰到用户态页表的映射,因此不会产生地址越界。
  4. 当内核线程执行完毕,切换回用户态进程时,active_mm 会自动切回该进程的 mm,无需额外操作。
  5. 每个 CPU 上的内核线程会借用该 CPU 上最近运行的用户态进程的 mm_struct ,不同 CPU 上的内核线程的 active_mm 可以指向不同的 mm_struct,互不干扰。

通过 active_mm 访问内核地址空间

内核线程运行期间,active_mm 指向被借用的 mm_struct,核心特性如下:

  1. 页表复用 :CPU 的页表基址寄存器仍指向 active_mm->pgd,内核线程通过该页表访问内核地址空间(无需关心用户态页表映射)。
  2. 用户态地址访问限制 :内核线程运行在内核态,CPU 的特权级(如 ARM64 的 EL1、x86 的 Ring 0)会阻止其访问用户态地址,因此不会触发地址越界错误。
  3. 无需修改 VMA / 页表 :内核线程不会操作 active_mm 中的 VMA 链表或用户态页表项,仅利用其内核页表映射。

归还 active_mm

  • 归还引用 :内核线程的 active_mm 被置为 NULL,同时递减被借用 mm_structmm_count
  • 页表切换 :CPU 的页表基址寄存器更新为目标用户态进程的 mm->pgd,恢复其完整的地址空间映射。

主动借用指定进程的 mm_struct

除了进程切换时的自动借用,内核线程还可以通过 use_mm()/unuse_mm() 函数主动借用指定用户态进程的 mm_struct ,适用于需要访问该进程用户态地址空间的场景(如内核态数据拷贝 copy_to_user()/copy_from_user())。

内核通过 use_mm()unuse_mm() 两个函数完成 active_mm 的借用与归还,核心流程如下:

  • 内核线程通过 use_mm() 显式借用某个用户态进程的 mm_struct
  • 借用时会递增 mm->mm_count(内核态引用计数),防止该 mm_struct 被提前销毁。
  • 内核线程退出时,通过 unuse_mm() 归还借用的 mm_struct
  • 递减 mm_count,若计数归 0,则可以释放 mm_struct

核心操作 API

mm_struct 的创建与销毁

函数 作用
alloc_mm() 分配 mm_struct 内存(从 slab 分配器中分配)
mm_init() 初始化 mm_struct 的字段,设置默认值
copy_mm() 进程 fork 时,复制父进程的 mm_struct 和页表(写时复制)
exit_mm() 进程退出时,销毁 mm_struct 并释放关联的页表和 VMA

虚拟内存区域操作

函数 作用
do_mmap() 核心函数,创建新的 VMA 并映射到虚拟地址空间(用户态 mmap() 系统调用的底层实现)
do_munmap() 释放指定的虚拟地址区间,删除对应的 VMA(用户态 munmap() 系统调用的底层实现)
find_vma() 根据虚拟地址查找对应的 VMA
find_vma_intersection() 查找与指定地址区间重叠的 VMA

地址空间切换

函数 作用
switch_mm() 进程切换时,切换 mm_struct,加载新进程的页表基址到 CPU 寄存器
activate_mm() 激活 mm_struct,用于内核线程切换到用户态进程时的地址空间加载

进程切换时的页表切换流程

  1. 内核从 task_struct 中取出新进程的 mm_struct
  2. 调用 switch_mm() 函数,将 mm_struct->pgd 加载到 CPU 的页表基址寄存器;
  3. 刷新 TLB(Translation Lookaside Buffer),避免旧页表缓存的干扰;
  4. 新进程的虚拟地址空间生效。

引用计数与地址空间共享

mm_struct 包含两个核心引用计数,用于管理地址空间的生命周期和共享:

  • mm_users用户态引用计数 ,表示共享该地址空间的进程数量(如线程组内的所有线程共享同一个 mm_structmm_users 等于线程数)。
    • fork() 时,子进程与父进程共享 mm_structmm_users++
    • 线程退出时,mm_users--,当 mm_users 减至 0 时,触发页表和 VMA 的释放。
  • mm_count内核态引用计数 ,表示内核对该 mm_struct 的引用次数(如内核线程借用地址空间时,mm_count++)。只有当 mm_count 减至 0 时,才会调用 free_mm() 释放 mm_struct 本身的内存。

写时复制(Copy-On-Write, COW)

进程 fork() 时,内核不会直接复制父进程的物理页,其实现完全依赖 mm_struct 和页表的配合:

  1. fork() 时,内核复制父进程的 mm_struct(浅拷贝),共享页表和 VMA,mm_users++
  2. 将父、子进程的所有页表项标记为 只读 ,并将 mm_struct 中的 VMA 权限也标记为只读;
  3. 当任一进程尝试写入 某页面时,触发缺页异常page_fault);
  4. 内核在缺页异常处理函数中,为该进程分配新的物理页,复制原页面数据,更新页表项为可写,解除 COW 限制。

内核版本差异

差异点 Linux 4.4 Linux 5.4
页表缓存 pgt_cache 字段直接在 mm_struct 页表缓存逻辑迁移到 mm_context_t,更贴合架构设计
NUMA 相关字段 numa_scan_seqnuma_masks 直接定义 新增 numa_state 结构体,统一管理 NUMA 扫描状态
内存控制组 mem_cgroup 字段为可选(CONFIG_MEMCG 默认启用 mem_cgroup,新增 memcg_batch 优化批量统计
VMA 查找优化 vmacache_seqnum 基础实现 新增 vmacache_fault() 函数,进一步优化缺页异常时的 VMA 查找

ARM64 架构适配

ARM64 架构的 mm_struct 中,context 字段(mm_context_t)包含关键的地址空间标识符 ASID

  • ASID 用于区分不同进程的页表,避免进程切换时刷新整个 TLB;
  • 内核通过 ASID 实现 TLB 条目共享,提升地址翻译性能。

mm_struct 的创建

execve() 系统调用为例,新进程 mm_struct 的创建流程:

  1. 调用 alloc_mm()slab 分配器中分配 mm_struct 内存;
  2. 调用 mm_init() 初始化 mm_struct 的默认字段(如 mmap_basetask_size、信号量等);
  3. 解析可执行文件的段表(如 ELF 的 .text.data 段),调用 do_mmap() 创建对应的 VMA;
  4. 初始化页表(pgd),建立代码段、数据段的虚拟地址到物理地址的映射;
  5. 设置栈的 VMA(start_stack),将命令行参数和环境变量拷贝到栈中;
  6. mm_struct 挂载到 task_structmm 指针上。

mm_struct 的销毁

进程调用 exit() 时,mm_struct 的销毁流程:

  1. 执行 exit_mm() 函数,获取 task_struct->mm 指针;
  2. 调用 mmput() 递减 mm_users 计数;
  3. mm_users 减至 0,触发 mm_release()
    • 遍历 mmap 链表,调用 do_munmap() 释放所有 VMA;
    • 调用 free_pgtables() 释放所有页表(PGD/PUD/PMD/PTE);
    • 递减 mm_count 计数,若 mm_count 减至 0,调用 free_mm() 释放 mm_struct 内存;
  4. task_struct->mm 置为 NULL,完成地址空间的销毁。

虚拟内存映射

用户态调用 mmap() 时,内核的底层实现流程:

  1. 系统调用陷入内核,执行 sys_mmap(),最终调用 do_mmap()
  2. 根据传入的参数(映射地址、长度、权限),在 mm_struct 的地址空间中分配一段空闲虚拟地址;
  3. 创建新的 vm_area_struct,设置其权限、映射类型(文件映射 / 匿名映射)等属性;
  4. 将新 VMA 挂载到 mm_structmmap 链表和 mm_rb 红黑树中;
  5. 若为文件映射 ,建立 VMA 与文件 struct address_space 的关联;若为匿名映射,暂不分配物理页(延迟到缺页异常时分配)。
相关推荐
leiming62 小时前
手写Linux C UDP通信
linux·c语言·udp
明天就是Friday2 小时前
(五)Linux 调度器 - CFS调度器
linux·linux内核·linux 调度器
阿拉伯柠檬2 小时前
网络层与网络层协议IP(一)
linux·网络·网络协议·tcp/ip·面试
lcreek2 小时前
Linux 信号机制详解:从硬件异常到安全编程实践
linux·系统编程
南 阳2 小时前
Python从入门到精通day10
linux·windows·python
xdpcxq10292 小时前
Apache 详解 在 Ubuntu 24 中安装和配置 Apache
linux·ubuntu·apache
General_G2 小时前
irobot_benchmark的编译和使用
linux·中间件·机器人·ros2
独隅2 小时前
Linux 正则表达式 的简介
linux·mysql·正则表达式
chinesegf2 小时前
虚拟机ubuntu中磁盘满了 + 镜像损坏,如何解决
linux·运维·ubuntu