choose_new_asid是 x86 架构 TLB 管理中的核心决策函数,它的任务是在进程切换时,为即将运行的地址空间next选择一个合适的 ASID 槽位,并决定是否需要刷新 TLB。
核心职责
当调度器决定切换到新的mm_struct时,switch_mm_irqs_off会调用choose_new_asid,它需要回答两个问题:
用哪个 ASID 槽位?
需要刷新 TLB 吗?
决策逻辑
函数的决策流程清晰且分层:
1. 无 PCID 支持(!static_cpu_has(X86_FEATURE_PCID))
直接返回 ASID=0,并强制刷新 TLB。此时硬件不支持 PCID,无法做任何缓存优化。
2. 检查“全局 ASID”
如果next是一个需要全局 ASID的特殊mm(例如init_mm或某些内核线程),直接返回该全局 ASID,且不需要刷新。全局 ASID 的 TLB 条目被所有进程共享,不存在隔离问题。
3. 缓存命中查找
遍历当前 CPU 的 6 个 ASID 槽位(ctxs[0..5]),比较每个槽位的ctx_id是否等于next->context.ctx_id。
命中:找到该
mm对应的槽位。此时需要判断是否需要刷新:如果槽位记录的tlb_gen小于next_tlb_gen(即该mm的页表自上次在此 CPU 上运行后发生过变更),则必须刷新;否则可以复用 TLB 条目,避免刷新。未命中:
next不在当前 CPU 的缓存中。
4. 缓存未命中:分配新槽位
通过this_cpu_add_return(cpu_tlbstate.next_asid, 1) - 1原子地获取下一个槽位编号。如果编号超出TLB_NR_DYN_ASIDS(6),则回绕到 0。此时必然需要刷新 TLB,因为新槽位可能残留着其他地址空间的陈旧条目。
调用关系
choose_new_asid的调用者非常集中:
switch_mm_irqs_off() └── choose_new_asid() // 在 real_prev != next 的分支中
在switch_mm_irqs_off中,当real_prev != next(即真正发生了地址空间切换)时,会调用choose_new_asid获取new_asid和need_flush,然后根据need_flush决定使用write_cr3(刷新)还是write_cr3配合CR3_NOFLUSH(不刷新)。
返回值语义
| 情况 | ASID | need_flush | 含义 |
|---|---|---|---|
| 无 PCID | 0 | true | 硬件不支持,强制刷新 |
| 全局 ASID | 全局值 | false | 共享映射,无需刷新 |
| 缓存命中,tlb_gen 最新 | 命中槽位 | false | 复用 TLB,零开销 |
| 缓存命中,tlb_gen 落后 | 命中槽位 | true | 需要刷新以更新陈旧条目 |
| 缓存未命中,分配新槽位 | 新槽位 | true | 必须刷新以清除残留 |
缓存命中且tlb_gen最新是性能最优的路径:既复用了 ASID 槽位,又完全避免了 TLB 刷新,这正是 Linux “浅缓存”策略的核心收益。
代码演进
2025 年 4 月,Borislav Petkov 提交了一个清理补丁,将choose_new_asid的输出参数(u16 *new_asid和bool *need_flush)改为返回值结构体struct new_asid。这个结构体只有两个位域(asid:16和need_flush:1),可以放入单个 32 位寄存器,既提升了代码可读性,也帮助编译器生成了更优的代码(tlb.o的 text 段从 9341 字节减小到 9213 字节)。