
switch_mm是进程切换context_switch中负责切换虚拟内存地址空间的核心函数。它的本质工作就是把新进程的页表基地址加载到硬件寄存器x86 上是CR3并处理好 TLB 刷新的相关事宜。核心职责加载新页表并管理 TLB当调度器决定运行一个新进程时switch_mm(prev, next, tsk)会被调用。它的核心逻辑围绕几个关键点展开加载next-pgd到CR3这是最根本的一步。next-pgd是next进程页全局目录的虚拟地址。执行load_cr3()后CPU 的地址翻译单元就开始使用新进程的页表了。维护mm_cpumask这是一个位掩码记录了当前哪些 CPU 正在使用这个mm。switch_mm会把自己所在的 CPU 加入到next的掩码中并从prev的掩码中移除。这确保了当页表发生变化时能准确地通知到所有正在使用该页表的 CPU 去刷新 TLB。处理 TLB 的“懒惰”策略如果prev next比如内核线程切换时借用同一个mmswitch_mm会走快速路径只更新 CPU 的 TLB 状态而不会重新加载CR3从而避免了昂贵的 TLB 刷新开销。代码逻辑与关键细节虽然不同内核版本的实现细节有差异但核心骨架是清晰的。一个经典的switch_mm实现以较新版本为例包含以下关键步骤void switch_mm(struct mm_struct *prev, struct mm_struct *next, struct task_struct *tsk) { unsigned cpu smp_processor_id(); if (likely(prev ! next)) { // 1. 设置当前CPU的TLB状态为OK并记录活跃的mm this_cpu_write(cpu_tlbstate.state, TLBSTATE_OK); this_cpu_write(cpu_tlbstate.active_mm, next); // 2. 将当前CPU加入next的mm_cpumask cpumask_set_cpu(cpu, mm_cpumask(next)); // 3. 加载新页表到CR3这是关键的同步点 // load_cr3 是序列化指令充当了全屏障的作用 load_cr3(next-pgd); // 4. 从prev的mm_cpumask中移除当前CPU cpumask_clear_cpu(cpu, mm_cpumask(prev)); // 5. 加载与mm相关的CR4状态如PCID、SMEP等 load_mm_cr4(next); } }这里有一个微妙的并发同步问题体现在mm_cpumask和load_cr3的顺序上。注释中详细解释了原因switch_mm必须保证cpumask_set_cpu的写入先于任何可能从新页表加载 TLB 条目的操作。否则另一个 CPU 在修改页表并检查mm_cpumask时可能看不到当前 CPU从而漏发 TLB 刷新 IPI导致当前 CPU 的 TLB 中残留过期的映射。幸运的是load_cr3()指令本身具有序列化效果天然充当了所需的全内存屏障。与其他核心机制的联动switch_mm并非孤立存在它和你之前问过的许多概念都紧密相连与context_switchswitch_mm是context_switch的第一步负责“换地址空间”之后的switch_to才负责“换寄存器栈”。与active_mm/ 惰性 TLB当切换到内核线程next-mm NULL时调度器不会调用switch_mm而是借用前一个进程的active_mm并进入“惰性 TLB”模式。与 PCID/ASID现代 x86 的switch_mm不会简单地往CR3里写next-pgd而是通过choose_new_asid等逻辑结合kern_pcid计算出包含 PCID 的完整CR3值并决定是否需要刷新 TLB。你之前看到的load_new_mm_cr3就是实际执行这一步的函数。与 TLB 刷新 IPImm_cpumask的维护是 TLB shootdown 的基础。当一个 CPU 修改了next的页表后它会遍历mm_cpumask找到所有正在使用该页表的 CPU并向它们发送 IPI 进行 TLB 刷新。