﻿# mm_struct —— 进程的地址空间描述符

> 这是 [../task-struct.md](/concepts/process/task-struct.md) 里 `task->mm` 那一项的展开。`mm_struct` 是内核对"一个进程看到的整个虚拟地址空间"的完整刻画：有哪些内存段（VMA）、页表在哪、堆栈边界在哪、多少人在用它。本篇讲 `mm_struct` 结构本身、VMA 组织方式与演进、页表层级、内存统计、生命周期、上下文切换时的行为；**映射的具体玩法**转 [../../elf/mmap.md](/concepts/elf/mmap.md)，**段的地址布局**转 [../../elf/memory-layout.md](/concepts/elf/memory-layout.md)，**页表翻译**转 [../../cache/tlb.md](/concepts/cache/tlb.md) 和 [../../cache/page-table-translation.md](/concepts/cache/page-table-translation.md)。

## 零、一句话认知：mm_struct = "这个进程内存长什么样"的总账本

每个有用户地址空间的进程，`task_struct` 里的 `mm` 指针都指向一个 `mm_struct`。它把地址空间的三样核心资源攒在一起：**一组 VMA**（每段连续映射）、**一个页表根 pgd**（硬件翻译入口）、**各段的边界与统计**。

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<mm>> #E3F2FD
  BorderColor<<mm>> #1976D2
  BackgroundColor<<vma>> #C8E6C9
  BorderColor<<vma>> #388E3C
  BackgroundColor<<pg>> #FFE0B2
  BorderColor<<pg>> #EF6C00
  BackgroundColor<<rss>> #F3E5F5
  BorderColor<<rss>> #7B1FA2
}
rectangle "mm_struct（每进程一个）" <<mm>> as M {
  rectangle "mmap → VMA 集合\n（链表 + maple tree 查找）" <<vma>> as VMA
  rectangle "pgd → 页表树根\n（进程切换时装入 CR3）" <<pg>> as PGD
  rectangle "段边界\nstart_code/end_code\nstart_data/end_data\nstart_brk/brk\nstart_stack\nmmap_base\narg_start/arg_end\nenv_start/env_end" as BOUNDS
  rectangle "引用计数\nmm_users / mm_count" as REFS
  rectangle "RSS 统计\n（匿名页/文件页/shared/hugetlb）\n→ /proc/<pid>/statm" <<rss>> as RSS
  rectangle "锁\nmmap_lock（读者/写者信号量）" as LOCK
}
rectangle "VMA vm_area_struct\n（每段连续映射一个）\n代码段 / 数据段 / 堆 / 栈 / 每个 mmap 区\nvm_start/vm_end/vm_flags/vm_file" <<vma>> as V
rectangle "页表树（pgd → p4d → pud → pmd → PTE）\n真正做 VA → PA 翻译\n（CR3 装 pgd 物理地址）" <<pg>> as P
M --> V : 挂着一串 VMA
M --> P : pgd 指向页表根
note bottom of V : VMA = 软件层"这段地址归我、什么权限、后备谁"\n（见 mmap.md）
note bottom of P : 页表 = 硬件层真正翻译\n（见 tlb.md / page-table-translation.md）
@enduml
```

> **核心记忆**：`mm_struct` 是**软件层的地址空间账本**（有哪些段、边界在哪、引用计数），它挂着两样东西——**VMA 集合**（逐段描述）和**页表 pgd**（硬件翻译）。VMA 是"计划"（我打算让这段内存做什么），页表是"落地"（硬件实际怎样翻译），缺页时把计划兑现成页表项（见 [../../elf/mmap.md](/concepts/elf/mmap.md)）。

## 一、mm_struct 核心字段分组

`mm_struct` 字段众多，按功能分五组：

| 分组 | 字段（类） | 作用 | 深入篇 |
|------|----------|------|--------|
| **VMA 集合** | `mmap`（链表头）、`mm_rb`/`mm_mt`（查找结构） | 所有 `vm_area_struct` 的组织与查找 | §1.1、[mmap](/concepts/elf/mmap.md) |
| **页表根** | `pgd` | 指向进程页全局目录——四级/五级页表的根 | §1.2、[tlb](/concepts/cache/tlb.md) |
| **段边界** | `start_code`/`end_code`、`start_data`/`end_data`、`start_brk`/`brk`、`start_stack`、`mmap_base` | 各段的起止虚拟地址 | §1.3、[memory-layout](/concepts/elf/memory-layout.md) |
| **内存统计** | `rss_stat`、`total_vm`、`locked_vm`、`pinned_vm`、`hiwater_rss` | 常驻/锁定/峰值内存 | §1.4 |
| **生命周期** | `mm_users`、`mm_count`、`owner`、`exe_file` | 谁在用、何时释放 | §1.5 |
| **锁** | `mmap_lock`（读者/写者信号量） | 保护 VMA 增删改查、缺页处理 | — |

### 1.1 VMA 组织方式：从链表到红黑树到 maple tree

VMA 集合的查找结构经历了三次演进，这是现代内核中最精彩的"数据结构换性能"案例之一：

```plantuml
@startuml
left to right direction
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<old>> #FFCDD2
  BorderColor<<old>> #C62828
  BackgroundColor<<mid>> #FFF9C4
  BorderColor<<mid>> #F9A825
  BackgroundColor<<new>> #C8E6C9
  BorderColor<<new>> #388E3C
}
rectangle "≤ 2.4 纯双向链表\nmmap 单链表,\n查找 O(n)；\nVMA 多了性能崩溃\n（一个进程上千个 VMA 很常见）" <<old>> as L1
rectangle "2.6 ~ 5.x 链表 + 红黑树\nmmap 链表 + mm_rb 红黑树\n查找 O(log n)；\n但红黑树指针开销大\n每节点 3 个指针\n（left/right/parent）" <<mid>> as L2
rectangle "6.1+ maple tree\nmm_mt 一棵 maple tree\n查找 O(log n), 且：\n· 缓存友好（数组节点\n 而非链表指针）\n· 支持无锁读（RCU）\n· 范围查询更快\n· 内存开销更低" <<new>> as L3
L1 -down-> L2 : 2.6 引入，解决\n线性扫描的性能灾难
L2 -down-> L3 : 6.1 合入\n大规模 VMA 场景\n(编译/数据库/容器)
@enduml
```

#### 为什么 VMA 查找性能如此关键

一个进程的每次缺页（page fault），内核都要在 VMA 集合中查找"这个虚拟地址落在哪个 VMA 里"，以确定该段的权限和映射类型。**这发生在缺页的**热路径**上**：

```bash
缺页 → find_vma(addr) → 在 VMA 集合中定位 → 检查权限 → 建立页表
                    ↑
              O(1) ↓ O(n) 的天壤之别
```

- **链表时代的痛**：一个 JVM/Chrome 进程常有 5000~20000 个 VMA（每个 mmap 一个、每个 .so 映射几段），链表 O(n) 遍历一次缺页走 10000 步，CPU 全耗在链表中。
- **红黑树改善**：O(log n)，10000 个 VMA 约 14 次比较，解决大部分问题。
- **maple tree 的质变**：不仅查找 O(log n)，还**支持 RCU 无锁读**——并发缺页时多个 CPU 可以同时查 VMA 而不抢 `mmap_lock`。对多线程内存密集型应用（数据库、编译、容器运行时）的提升是**系统级**的。

> **关键理解**：`mmap` 链表从未被去掉——即使升级到 maple tree，链表仍然保留（用于遍历所有 VMA，如 `/proc/<pid>/maps` 的输出）。maple tree 替代的是 `mm_rb` 红黑树（查找功能），链表负责遍历功能，两者分工不同。

### 1.2 pgd —— 页表树的根

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<l0>> #E3F2FD
  BorderColor<<l0>> #1976D2
  BackgroundColor<<l1>> #BBDEFB
  BorderColor<<l1>> #1565C0
  BackgroundColor<<l2>> #C8E6C9
  BorderColor<<l2>> #388E3C
  BackgroundColor<<l3>> #A5D6A7
  BorderColor<<l3>> #2E7D32
  BackgroundColor<<l4>> #FFE0B2
  BorderColor<<l4>> #EF6C00
  BackgroundColor<<phy>> #FFCCBC
  BorderColor<<phy>> #D84315
}
rectangle "虚拟地址 48 位\n[ 9bit PML4 | 9bit PDPT | 9bit PD | 9bit PT | 12bit offset ]" as VA
rectangle "pgd\n（页全局目录）\nmm_struct->pgd\n→ CR3 物理地址" <<l0>> as PGD
rectangle "p4d / pud\n（页上级目录）" <<l1>> as PUD
rectangle "pmd\n（页中间目录）" <<l2>> as PMD
rectangle "PTE\n（页表项）\n物理页号 + 权限位\nPresent/RW/User/Accessed/Dirty/NX" <<l4>> as PTE
rectangle "物理页框\n4KB 物理页面" <<phy>> as PHY
VA --> PGD : PML4 index
PGD --> PUD : 指向下一级
PUD --> PMD : PDPT index
PMD --> PTE : PD/PT index
PTE --> PHY : 物理页号
@enduml
```

每个虚拟地址到物理地址的翻译，都从 `pgd` 出发，经历 4 级（或 5 级，La57）页表逐级查找到 PTE，取出物理页号 + offset 拼成物理地址。这个过程的每个细节见 [../../cache/page-table-translation.md](/concepts/cache/page-table-translation.md) 和 [../../cache/tlb.md](/concepts/cache/tlb.md)。

**pgd 与 mm_struct 的关系**：

| 要点 | 说明 |
|------|------|
| `mm->pgd` 存的是什么 | pgd 页的**内核虚拟地址**（不是物理地址，内核可以直接解引用） |
| CR3 装的是 | pgd 页的**物理地址**——`__pa(mm->pgd)` 在进程切换时写入 CR3 |
| 什么时候创建 | 进程创建时（`fork` → `mm_init` → `pgd_alloc`）分配一个物理页作为 pgd |
| 什么时候释放 | `__mmdrop` → `pgd_free`，进程死亡时最后一步 |
| 内核空间页表 | 所有进程的 pgd 中，**内核地址部分的表项完全一样**（共享内核映射，进程切换时只变用户空间部分） |

> **内核空间共享**是 Linux 的古老设计：进程切换时不需要换内核页表，只需更新用户空间部分（或直接换 CR3 让 TLB 自动区分 PCID/ASID），避免了"切到内核线程先要装内核页表"的开销。详见 §四。

### 1.3 段边界字段——把虚拟地址分成若干区间

```bash
          高地址
     ┌─────────────────────┐ ← TASK_SIZE_MAX
     │  栈 stack            │ stack = mm->start_stack（初始栈顶）
     │  ↓ 向下生长          │
     ├─────────────────────┤
     │  ↕                   │
     │  mmap 区              │ mmap 区域从 mm->mmap_base 向下排
     │  (.so / 匿名映射)     │
     │  ↕                   │
     ├─────────────────────┤
     │  堆 heap             │ brk = mm->brk（堆顶可动）
     │  ↑ 向上生长          │ start_brk（堆起始）
     ├─────────────────────┤
     │  .bss / .data        │ end_data / start_data
     │  .text（代码）        │ end_code / start_code
     └─────────────────────┘
          低地址
```

六个关键边界字段的含义：

| 字段 | 含义 | 可增长？ | 谁设的 |
|------|------|---------|--------|
| `start_code` / `end_code` | 可执行代码段（`.text`）的虚拟地址范围 | 否（固定） | `execve` 时由 ELF 加载器填写 |
| `start_data` / `end_data` | 已初始化数据段（`.data`）的虚拟地址范围 | 否（固定） | `execve` 时由 ELF 加载器填写 |
| `start_brk` | 堆起始地址（`.bss` 段末尾） | 否（起点固定） | `execve` 时设定，后续不变 |
| `brk` | 堆顶（可增长，`sbrk()`/`brk()` 系统调用移动它） | **是**——`brk()` 调大 | `brk` 系统调用修改 |
| `start_stack` | 初始栈顶（主线程的栈起点） | **是**——栈自动向下增长（缺页触发） | `execve` 时设定 |
| `mmap_base` | mmap 区域的起始地址（从高往低分配） | 否（分配基准不变） | `execve` 时由 `arch_pick_mmap_layout()` 选择 |

> **`brk` 增长 vs `mmap` 分配**：`malloc` 小内存（通常 < 128 KB）优先从堆（`brk`）扩，大块则用 `mmap` 从 mmap 区分配。`mm->brk` 只反映前一类的上限。详细逻辑见 [../../elf/memory-layout.md](/concepts/elf/memory-layout.md)。

### 1.4 RSS 统计——这个进程到底吃了多少物理内存

`mm->rss_stat` 是一个 `struct mm_rss_stat`，按页类型分组计数：

```c++
struct mm_rss_stat {
    atomic_long_t count[NR_MM_COUNTERS];
    // MM_FILEPAGES  —— 文件映射页（如 .so、mmap 文件）
    // MM_ANONPAGES  —— 匿名页（堆、栈、malloc mmap）
    // MM_SWAPENTS   —— 被换出到 swap 的页
    // MM_SHMEMPAGES —— 共享内存页
};
```

**`/proc/<pid>/statm` 和 `status` 的 Vm 系列就是从这里来的**：

```bash
cat /proc/<pid>/statm
# 输出: size resident shared text lib data dt
# size     = total_vm（总虚拟页数）        → VmSize
# resident = rss（常驻物理页数）           → VmRSS
# shared   = 共享页数                      → RssShmem + RssFile
# text     = 代码段常驻页数                → VmExe
# data     = 数据+堆常驻页数               → VmData
cat /proc/<pid>/status | grep -E 'Vm|Rss'
# VmSize / VmRSS / VmData / VmStk / VmExe / VmLib / VmPTE / VmSwap
# RssAnon / RssFile / RssShmem
```

| 统计项 | 来源 | 含义 |
|--------|------|------|
| `VmSize` | `mm->total_vm` | 虚拟地址空间总大小（含未实际分配的） |
| `VmRSS` | `mm->rss_stat[file+anon+shmem]` | 实际占用的物理页数（不含换出的） |
| `VmData` | 堆段 VMA 的虚拟大小 | 堆的大小（`brk - start_brk` + mmap 匿名段） |
| `VmStk` | 栈段 VMA 的虚拟大小 | 主线程栈大小 |
| `VmExe` | 代码段 VMA 常驻页数 | 代码在内存里的实际占用量 |
| `RssAnon` | `rss_stat[MM_ANONPAGES]` | 匿名页常驻（堆、栈、malloc 大块） |
| `RssFile` | `rss_stat[MM_FILEPAGES]` | 文件映射页常驻（.so 代码、mmap 文件） |
| `RssShmem` | `rss_stat[MM_SHMEMPAGES]` | 共享内存页 |

> **`VmRSS ≠ 进程真实的"独占"物理内存`**：RSS 包含共享库（每个进程都算一遍 glibc 的页），所以同一个 `.so` 被 100 个进程映射时，RSS 总和远大于实际物理内存。`PSS`（Proportional Set Size，见 `/proc/<pid>/smaps`）做了分摊——共享页按引用进程数等分，更接近真实开销。

### 1.5 两个引用计数为什么分开：mm_users vs mm_count

这是 `mm_struct` 最容易搞混的点：

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<users>> #C8E6C9
  BorderColor<<users>> #388E3C
  BackgroundColor<<count>> #FFE0B2
  BorderColor<<count>> #EF6C00
  BackgroundColor<<k>> #E1BEE7
  BorderColor<<k>> #7B1FA2
}
rectangle "同进程 3 个线程\nthread1/thread2/thread3\n→ 共享同一个 mm" <<users>> as T
rectangle "mm_struct\nmm_users = 3\nmm_count  = 4\n（3 个线程 + 1 个内核 lazy 借用）" as MM
rectangle "内核线程 kworker\nmm = NULL\n但临时借用了该 mm 的页表\n（mm_count + 1）" <<k>> as K
T --> MM : mm_users 计数
K --> MM : mm_count 计数（不增加 mm_users）
@enduml
```

- **`mm_users`**：多少**任务（线程）**把它当自己的地址空间在用。同进程 3 个线程 → `mm_users = 3`。归零表示"没有用户线程了"，地址空间可以拆（VMA、页表可回收）。
- **`mm_count`**：多少**引用**（包括 `mm_users` 整体算 1 个引用，加上内核临时借用的 lazy 引用）。归零才 `free` 掉 `mm_struct` 结构体本身。
- **为什么分两层**：内核线程没有自己的地址空间（`mm == NULL`），但运行时**临时借用**上一个进程的 mm 页表（lazy TLB，省一次 CR3 切换）。这种借用只加 `mm_count`、不加 `mm_users`——保证"用户都走光了就能拆地址空间"，但 `mm_struct` 结构体本身要等借用也还了才释放。

| 操作 | mm_users 变化 | mm_count 变化 | 说明 |
|------|:---:|:---:|------|
| `pthread_create`（同进程线程） | +1 | +1 | 线程共享 mm |
| 线程退出 | -1 | -1 | `mm_users` 归零 → 可拆地址空间 |
| 内核线程借用（lazy TLB） | 不变 | +1 | 只借页表，不算用户 |
| 内核线程归还 | 不变 | -1 | `mm_count` 归零 → 可 free 结构体 |
| `fork()` 子进程 | 新 mm，初始 = 1 | 新 mm，初始 = 1 | 独立计数 |
| `execve()` | 不变（换的是 mm 内容） | 不变 | 旧 mm 的 count 减，新 mm 独立 |

## 二、线程共享、进程复制

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<a>> #C8E6C9
  BorderColor<<a>> #388E3C
  BackgroundColor<<b>> #FFE0B2
  BorderColor<<b>> #EF6C00
  BackgroundColor<<c>> #E1BEE7
  BorderColor<<c>> #7B1FA2
}
rectangle "同进程的多个线程\nthread1/thread2/thread3\n→ 指向**同一个 mm_struct**\n（CLONE_VM，mm_users=3）" <<a>> as T
rectangle "fork 出的子进程\n→ **复制一个新 mm_struct**\n（VMA 复制，物理页 CoW 共享，\n写时才分裂）" <<b>> as F
rectangle "vfork 出的子进程\n→ **直接借用父进程 mm_struct**\n（不复制，不 CoW，mm_users 不加）\n父进程同时阻塞在\nTASK_UNINTERRUPTIBLE" <<c>> as VF
note bottom of T : 所以线程间指针可互传、\n改全局变量互相可见
note bottom of F : 所以父子进程地址空间隔离，\n改变量互不影响（见 process-creation §二）
note bottom of VF : 子进程直接写父进程地址空间，\n直到 execve()/_exit() 释放父进程\n（详见 vfork.md）
@enduml
```

| 场景 | mm_struct | 后果 |
|------|-----------|------|
| **同进程线程** | 共享同一个（`CLONE_VM`） | 地址互通、全局变量共享、`mm_users` 累加 |
| **fork 子进程** | 复制新的（VMA 复制、物理页 CoW） | 地址空间隔离，写时复制分裂（见 [../process-creation.md](/concepts/process/process-creation.md) §二） |
| **vfork 子进程** | 借用父进程 mm（不复制，不 CoW） | 子进程可直接读写父进程地址空间；父进程阻塞直到子进程 `execve()`/`_exit()`（详见 [../vfork.md](/concepts/process/vfork.md)） |
| **内核线程** | `mm == NULL` | 无用户地址空间，借用上个进程页表（lazy TLB） |
| **execve** | 换成全新 mm | 旧地址空间整个丢弃，装入新程序的段 |

### 2.1 fork 复制 mm——VMA 全量拷贝，物理页 CoW

`fork()` 时 `mm_struct` 的复制是真拷贝：**VMA 列表逐段复制**，每个 `vm_area_struct` 都要新建一份。但物理页不走真复制——走**写时复制（Copy-on-Write）**：

```plantuml
@startuml
skinparam shadowing false
skinparam sequence {
  ParticipantBackgroundColor #E3F2FD
  ParticipantBorderColor #1976D2
}
participant "进程 A" as A
participant "mm_struct (A)" as MA
participant "PTE (A)" as PA
participant "物理页" as PHY
participant "PTE (B)" as PB
participant "mm_struct (B)" as MB
participant "进程 B（fork 子）" as B
A -> MA : fork()
MA -> MA : 复制 VMA 链表\n（每个 vm_area_struct 新建一份，\nvm_start/vm_end/vm_flags 照抄）
MA -> PA : 遍历所有 PTE
PA -> PB : 在子进程页表中重建 PTE\n物理页号相同（指向同一 PFN）\n但**两边 PTE 的 Write 位都清掉**\n→ 谁写谁触发缺页
note over PA, PB : CoW 的核心：\nPTE 只读，指向同一物理页
== 进程 A 要写 ==
A -> PA : write 到某页
PA -> PA : PTE 的 Write 位 = 0 → #PF（缺页）
PA -> PHY : 缺页处理程序：分配新物理页\n把旧页内容 memcpy 过来
PA -> PA : 更新 A 的 PTE：指向新页，Write 位置 1
note over PA : A 现在有自己的物理页副本
== 进程 B 也要写（同一页） ==
B -> PB : write 到该页
PB -> PB : PTE 的 Write 位 = 0 → #PF
PB -> PHY : 根据 PTE 的引用计数判断：\nRefCount 已降为 1 → 无需新分配\n直接把 Write 位置 1 即可
note over PB : B 可以原地写原物理页\n（因为没有别人还在共享它）
@enduml
```

> **核心**：`fork` 复制 VMA（软件层的"段描述"），但物理页不拷贝——PTE 指向同一 PFN 且都设只读。哪个进程先写，就在缺页处理中拷贝一份物理页。这个过程对用户态完全透明。详见 [../process-creation.md](/concepts/process/process-creation.md) §二。

## 三、mm_struct 的生命周期

### 3.1 诞生——execve 时创建全新 mm

进程被 `execve` 换脑时，旧 `mm_struct` 被整个丢弃（`mmput`），新 `mm_struct` 由 `bprm_mm_init` 分配和初始化：

```plantuml
@startuml
skinparam shadowing false
skinparam sequence {
  ParticipantBackgroundColor #E3F2FD
  ParticipantBorderColor #1976D2
}
participant "execve()" as E
participant "VFS/ELF Loader" as LD
participant "新 mm_struct" as NM
participant "页表" as PT
E -> E : 创建 linux_binprm（封装新程序信息）
E -> NM : bprm_mm_init()\n分配全新 mm_struct
NM -> NM : mm_init()\n· 初始化 mmap/VMA 链表（空）\n· pgd_alloc() 分配页表根\n· mmap_base 选位置\n· mm_users=1, mm_count=1
E -> LD : load_elf_binary()
LD -> NM : 设置段边界\n· start_code/end_code（.text）\n· start_data/end_data（.data）\n· start_brk（堆起点）\n· start_stack（栈顶）\n· mmap_base（mmap 起始）
LD -> PT : 建初始映射\n· 代码段 → 文件映射 + 只读可执行\n· 数据段 → 文件映射 + 私有读写\n· 栈 → 匿名映射 + 读写\n· vdso/vvar → 特殊映射
E -> E : 释放旧 mm_struct\n(set_task_mm: current->mm = new_mm)
@enduml
```

### 3.2 消亡——exit 时的逐步拆解

进程退出（`do_exit`）时 mm_struct 不是瞬间释放的，而是逐步拆：

```bash
do_exit()
  → mmput(mm)              // 减 mm_users，通常归零
    → if mm_users == 0:
        exit_mmap()        // ① 先拆 VMA：遍历释放所有 vm_area_struct
                            //    同时 zapping 页表（清空用户空间 PTE）
        set_task_mm(NULL)  // ② 切断 task->mm 指针
        mmdrop(mm)         // ③ 减 mm_count
          → if mm_count == 0:
              __mmdrop()
                → pgd_free(mm->pgd)   // ④ 释放页表根页
                → free_mm(mm)         // ⑤ 释放 mm_struct 本身
```

为什么要分 `exit_mmap` 和 `mmdrop` 两步？因为可能有内核线程还在借用页表（mm_count > mm_users）：先拆 VMA 和页表内容（反正用户地址空间没人用了），但 pgd 页和 mm_struct 结构体要等所有借用者归还后才真正释放。

### 3.3 上下文切换时的 mm 行为

```plantuml
@startuml
skinparam shadowing false
skinparam sequence {
  ParticipantBackgroundColor #E3F2FD
  ParticipantBorderColor #1976D2
}
participant "进程 A（用户态）" as A
participant "调度器" as SCHED
participant "进程 B（用户态）" as B
participant "内核线程 K" as K
== A → B 切换（两个普通进程） ==
A -> SCHED : 时间片到 / 阻塞
SCHED -> SCHED : switch_mm(A->mm, B->mm)
note over SCHED
1. 如果 A 和 B 的 mm 不同：
   · 写 B->mm->pgd 物理地址到 CR3
   · 本地 TLB 中用户部分自动失效
   （内核部分不受影响，因为内核页表共享）
2. 如果 A 和 B 共享 mm（同进程线程）：
   不写 CR3，不刷 TLB
end note
SCHED -> B : 恢复 B 的用户态上下文
== B → K 切换（切到内核线程） ==
B -> SCHED : 系统调用 / 中断返回时调度
SCHED -> K : switch_to(B, K)
note over SCHED
K->mm == NULL，但 CR3 保持 B 的页表不变
（lazy TLB：内核线程运行在 B 的页表下，
能用内核空间映射访问所有物理内存，
而用户空间映射虽然在 TLB 里但内核态无权访问）
K->active_mm = B->mm（借用，mm_count++）
end note
== K → A 切换（内核线程回到普通进程） ==
K -> SCHED : 内核线程让出
SCHED -> SCHED : switch_mm(K->active_mm, A->mm)
note over SCHED
内核线程归还借用（mm_count--），
写入 A->mm->pgd 到 CR3
end note
SCHED -> A : 恢复 A 的用户态上下文
@enduml
```

**切换优化——两个不写 CR3 的场景**：

| 场景 | CR3 是否写 | 是否刷 TLB | 条件 |
|------|:---:|:---:|------|
| 前后进程共享同一个 mm（同进程线程） | 否 | 否 | `next->mm == prev->mm` |
| 切到内核线程 | 否 | 否 | `next->mm == NULL`，沿用 prev 页表 |
| 前后进程不同 mm | **是** | 是（用户部分） | `next->mm != prev->mm`，需换地址空间 |
| PCID/ASID 启用时 | 是（但写的是 PCID） | 否（硬件按 PCID 区分） | 现代 x86/ARM 均有支持 |

> **PCID（Process Context Identifier）**：x86 的优化——CR3 低 12 位存一个 PCID 标签，TLB 按标签区分不同进程的条目。切换时只写新 PCID 到 CR3，TLB 里的旧条目不会立即失效，而是自然过期或被新条目挤出。这样避免了"一切换就全局刷 TLB"的性能灾难。ARM 对应的机制是 ASID。

## 四、观测

```bash
cat /proc/<pid>/maps       # 逐段列 VMA：地址范围/权限(rwxp)/后备文件——mm_struct 的 VMA 集合
cat /proc/<pid>/smaps      # 每段更细：RSS/PSS/脏页/是否 THP/匿名页 vs 文件页
cat /proc/<pid>/statm      # 页数统计（总/RSS/共享/代码/数据）——来自 rss_stat
cat /proc/<pid>/status | grep -E 'Vm|Rss'   # VmSize/VmRSS/VmData/VmStk... 人类可读
cat /proc/<pid>/numa_maps  # NUMA 维度的内存分布
```

- `maps` 的每一行就是一个 VMA；权限第 4 位 `p`（private）/ `s`（shared）对应映射类型（见 [../../elf/mmap.md](/concepts/elf/mmap.md)）。
- `smaps` 能看到**每个 VMA 的 RSS/PSS/Swap/THP**，是排查内存占用最实用的工具。`PSS` 把共享页按引用进程数等分，比 `RSS` 更真实。
- 地址落在哪段、怎么看地址判类型见 [../../elf/memory-layout.md](/concepts/elf/memory-layout.md)。

**进阶观测——pmap 与 perf trace**：

```bash
pmap -x <pid>              # 进程内存全景：各段地址/大小/RSS/Dirty，比 maps 更直观
pmap -XX <pid>             # 比 smaps 更精简的汇总
# 缺页观测——看谁在触发缺页（CoW 分裂、按需加载）
perf stat -e page-faults,minor-faults,major-faults -p <pid> sleep 10
# 查看某个地址落在哪个 VMA 里
cat /proc/<pid>/maps | grep -w <hex_addr>
```

## 五、和其他文档的关系

- **父篇**：[../task-struct.md](/concepts/process/task-struct.md)（mm 是 task 的资源对象之一）。
- **映射玩法**：[../../elf/mmap.md](/concepts/elf/mmap.md)（VMA 怎么建、四象限映射分类、惰性缺页触发机制）。
- **段布局**：[../../elf/memory-layout.md](/concepts/elf/memory-layout.md)（各段地址范围、32/64 位布局差异、看地址秒判类型）。
- **页表翻译**：[../../cache/tlb.md](/concepts/cache/tlb.md)（TLB 结构与命中机制）、[../../cache/page-table-translation.md](/concepts/cache/page-table-translation.md)（完整的 VA→PA 四级页表转换流程）。
- **创建时**：[../process-creation.md](/concepts/process/process-creation.md)（fork 复制 mm + CoW 机制）、[../thread-creation.md](/concepts/process/thread-creation.md)（线程共享 mm 的 clone 标志）。
- **进程间共享**：[../fork-and-threads.md](/concepts/process/fork-and-threads.md)（多线程程序 fork 的坑——锁状态残留、async-signal-safe 约束）。
- **内存统计**：[../../tools/memory/free.md](/tools/memory/free.md)（系统级 RSS/VIRT 统计解读）。

## 六、一句话总结

> **`mm_struct` 是进程地址空间的软件总账本：攒着一组 VMA（逐段描述内存，从链表演进到红黑树再到 RCU 无锁 maple tree）、一个页表根 pgd（硬件翻译入口，进程切换时写入 CR3）、各段边界和两个引用计数（`mm_users` 数用它的线程，`mm_count` 数所有引用含内核 lazy 借用）。同进程线程共享一个 mm（地址互通），fork 复制新 mm（VMA 全量拷贝 + 物理页 CoW），内核线程无 mm（借页表），execve 完全换新 mm，exit 逐步拆解（先 VMA 后 pgd 最后结构体）。透过 `/proc/<pid>/maps`/`smaps`/`numa_maps`/`pmap` 就能看到它的 VMA 与内存统计的全貌。**

