switch_mm_irqs_off是 x86 架构中进程地址空间切换的最终执行者。它是switch_mm在中断关闭(IRQs off)上下文中的变体,也是现代内核中真正干活的函数。
核心职责:加载新页表并协调 TLB 与缓解措施
当调度器决定运行一个新任务时,context_switch会调用switch_mm_irqs_off。它需要完成三件大事:
加载新地址空间的页表到 CR3
决定并执行必要的 TLB 刷新
协调投机执行缓解措施(IBPB、L1D 刷新)
核心执行流程
根据内核源码,其逻辑可概括为以下几个关键阶段:
1. 快速路径:同一地址空间
if (next == real_prev) { /* 同一个 mm,通常直接返回,无需切换 CR3 */ ... return; }如果next和当前加载的mm相同(例如同一进程内的线程切换),函数会走快速路径,仅更新 LDT 等少量状态,避免昂贵的 CR3 重载和 TLB 刷新。
2. 为新 mm 选择 ASID 并决定是否刷新
new_asid = choose_new_asid(next, next_tlb_gen, &need_flush);
这是 PCID/ASID 机制的核心:choose_new_asid遍历当前 CPU 的 6 个 ASID 槽位,判断next是否在缓存中。如果命中且tlb_gen最新,则need_flush = false,可以复用 TLB 条目;否则需要分配新槽位并刷新。
3. 执行 CR3 加载
if (need_flush) { this_cpu_write(cpu_tlbstate.ctxs[new_asid].ctx_id, next->context.ctx_id); this_cpu_write(cpu_tlbstate.ctxs[new_asid].tlb_gen, next_tlb_gen); load_new_mm_cr3(next->pgd, new_asid, lam, true); trace_tlb_flush(TLB_FLUSH_ON_TASK_SWITCH, TLB_FLUSH_ALL); } else { load_new_mm_cr3(next->pgd, new_asid, lam, false); trace_tlb_flush(TLB_FLUSH_ON_TASK_SWITCH, 0); }load_new_mm_cr3负责构建包含 PCID 的 CR3 值并写入硬件。如果need_flush为真,还会更新ctxs[]槽位的元数据。
4. 更新mm_cpumask并协调缓解措施
/* 将当前 CPU 加入新 mm 的掩码 */ cpumask_set_cpu(cpu, mm_cpumask(next)); /* 条件化执行 IBPB / L1D 刷新 */ cond_mitigation(next);
cond_mitigation是你在之前分析过的函数,负责根据任务标志执行 IBPB 和 L1D 刷新。
5. 清理旧 mm 的掩码
if (real_prev != &init_mm) cpumask_clear_cpu(cpu, mm_cpumask(real_prev));
与switch_mm的关系
在较新的内核中,switch_mm通常是一个简单的包装函数:
void switch_mm(struct mm_struct *prev, struct mm_struct *next, struct task_struct *tsk) { unsigned long flags; local_irq_save(flags); switch_mm_irqs_off(prev, next, tsk); local_irq_restore(flags); }switch_mm负责保存中断状态并调用switch_mm_irqs_off,而后者在中断已关闭的上下文中执行实际的切换逻辑。这种拆分允许调度器在已经关闭中断的情况下直接调用switch_mm_irqs_off,避免不必要的开关中断开销。
性能关键路径
switch_mm_irqs_off是内核中最频繁执行的性能关键路径之一。每次进程切换都会经过它,因此代码中充斥着likely()/unlikely()分支提示、静态键(static keys)和 Per-CPU 缓存,以最小化开销。l1d_flush_evaluate、cond_mitigation、cr4_update_pce_mm等函数,都是在这个路径上被调用的。