一个空管道上的 read(),足以串起上下文切换:任务 A 等待数据,CPU 转去执行 B,B 写入数据后唤醒 A,A 稍后才接着读取。
沿这个例子,需要区分进入内核、等待事件、排队运行、恢复执行 。它们都会影响调用延迟,却对应不同的机制和优化方向。
你想追踪哪一段 阅读位置
read 为什么睡眠,唤醒后为什么还在等? 第 1 节:延迟时间线
换任务时,栈和地址空间怎样交接? 第 2~3 节:mm 与 switch_to
同样一次切换,成本为什么不同? 第 4 节:TLS、FPU、TLB
通知已经发生,线程为什么睡不醒? 第 5~6 节:等待协议与测量
本文以通用调度路径和 x86-64 为例,源码对照 Linux 7.2;架构细节与函数顺序不是稳定 ABI。
1. 从一个阻塞 read 开始
任务 A 调用 read(),发现管道没有数据且仍有写端。CPU 经系统调用入口进入 A 的内核执行路径,用户返回状态被保存在适用的入口帧中,此时运行者仍然是 A 。如果管道已有数据,read 可以直接完成并回到 A 的用户态,全程不必切换任务。
没有数据时,内核登记 A 的等待状态,确认条件仍不满足,再让 A 睡眠。调度器选中 B 后,才保存 A 的执行上下文并接续 B。
B 写入管道会唤醒 A,但 A 只是恢复了运行资格。等调度器再次选中 A,它才从自己的等待路径继续,重新检查条件、取出数据并返回用户态。
一次 read:数据等待结束后,还可能继续等待 CPU
睡眠:等待数据
可运行:等待 CPU
运行:继续 read
进入等待
被唤醒
重新运行
返回
数据已就绪 ≠ 已经拿到 CPU
示意时间段,宽度不代表实测比例;排队时间与低层切换成本需要分别测量。
图中的两个等待区间,需要分别诊断:
A 停在哪一段 此时缺什么 优先检查
数据还未到达 读取条件尚未满足 生产者、I/O 和业务依赖
已唤醒但尚未运行 CPU 执行机会 竞争、配额、亲和性、抢占延迟
已重新执行 read 正在完成读取与返回 实际执行工作及相关访存成本
提高优先级无法让尚未产生的数据提前出现;只有先找对时间段,才能判断调度优化是否有效。
硬件中断也不自动意味着切换任务。中断可以打断 A,处理后仍返回 A;只有相关退出路径或其他调度点决定运行另一个任务,才产生任务交接。进入内核次数、系统调用次数、上下文切换次数不能相互替代。
2. context_switch:地址空间与执行流是两条线
选出 next 后,可沿 kernel/sched/core.c 把交接分成三个逻辑阶段:
交接前准备。 协调调度记账,并处理新旧任务的 mm 关系。
低层切换。 经架构 switch_to 换栈并处理必要的寄存器状态。
交接后收尾。 已在新任务上下文中,完成对刚换下任务的收尾。
这里的“收尾发生在新任务中”很重要。第 3 节会沿两条内核栈解释它为什么必须这样安排。
地址空间切换取决于 prev 与 next 的关系:
场景 mm 关系 仍然需要处理的执行上下文
同一进程的两个普通线程 通常共享用户 mm 各自内核栈、寄存器、TLS、FPU 状态等
两个独立进程 通常使用不同 mm 执行环境和用户地址空间都要协调
切到内核线程 通常 next->mm == NULL 使用 active_mm 的借用/延迟管理协议,而非进入某个用户程序
相同 mm 与不同 mm,各省了什么
同 mm 可以减少地址空间工作,但线程切换绝不是“不用保存上下文”。相反,不同 mm 也不等于每次无条件清空整个 TLB。x86 的 CR3 关联当前页表根,启用 PCID 时还携带地址空间标识;PCID 可以区分不同地址空间的翻译条目。切换路径需要结合 CPU 上缓存的上下文与失效代次判断是否刷新。它只在条目仍有效时允许安全复用,不能使过期映射继续合法使用。
内核线程的 active_mm 让内核在没有自有用户 mm 的情况下维持合适的地址空间上下文。相关引用和延后释放必须与架构的 lazy TLB 协议协作,不能简单写成 mm = &init_mm,也不能在切换前随意释放旧 mm。
3. switch_to 为何像“在另一条调用链上返回”
每个任务有独立内核栈。A 调用调度路径时,其尚未结束的内核调用链就留在 A 栈上。低层切换保存 A 恢复执行所需的栈位置和架构状态,然后把栈指针等换到 B 的保存位置。若 B 以前也从调度路径睡眠,那么 CPU 接下来看到的是 B 上次暂停的调用链,而非继续沿 A 的局部变量执行。
从 A 切到 B:CPU 接续 B 先前暂停的内核调用链
A 的内核栈
等待事件 → schedule → switch_to
B 的内核栈
上次 schedule 保存的恢复位置
换栈
A 的恢复位置保留下来
未来被重新选中后继续返回
CPU 现在沿 B 的调用链返回
完成切换收尾,再继续 B 的逻辑
B 不是调用 A 的后续代码;首次运行的新任务通过专门的启动跳板进入执行。
直接选择 A 或 B,再改变当前任务的局部值。对比共享与独立 mm:共享地址空间不会合并两条内核栈。
正在呈现知识画面 重新加载
换到 B,为什么会沿 B 的调用链返回? 共享 mm 的线程仍有各自的内核栈。切换 SP 后,CPU 使用新任务自己的调用链与局部状态,收尾对象是本次刚换下的任务。
{"id":"context-stack-ownership","title":"换到 B,为什么会沿 B 的调用链返回?","summary":"共享 mm 的线程仍有各自的内核栈。切换 SP 后,CPU 使用新任务自己的调用链与局部状态,收尾对象是本次刚换下的任务。","height":1000,"html":"<p class=\"eyebrow\" data-i18n=\"eyebrow\"></p><h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"presets\" role=\"group\" data-i18n-label=\"relation\"><button data-mode=\"shared\" data-i18n=\"shared\"></button><button data-mode=\"separate\" data-i18n=\"separate\"></button></div><div class=\"cpu\"><header>CPU</header><div><span>current</span><strong id=\"current\"></strong></div><div><span>SP</span><strong id=\"sp\"></strong></div><div><span>active_mm</span><strong id=\"mm\"></strong></div><code id=\"continuation\"></code></div><div class=\"two\"><section class=\"card stack\" id=\"stack-A\"><header>A <small id=\"mm-A\"></small></header><div class=\"stack-frame\">worker_A()</div><div class=\"stack-frame\">__schedule()</div><div class=\"stack-frame\" id=\"frame-A\"></div><div class=\"local\"><span data-i18n=\"local\"></span><strong id=\"local-A\"></strong></div><button data-task=\"A\" data-i18n=\"switch\"></button></section><section class=\"card stack\" id=\"stack-B\"><header>B <small id=\"mm-B\"></small></header><div class=\"stack-frame\">worker_B()</div><div class=\"stack-frame\">__schedule()</div><div class=\"stack-frame\" id=\"frame-B\"></div><div class=\"local\"><span data-i18n=\"local\"></span><strong id=\"local-B\"></strong></div><button data-task=\"B\" data-i18n=\"switch\"></button></section></div><div class=\"actions\"><button id=\"work\" data-i18n=\"work\"></button><button id=\"reset\" data-i18n=\"reset\"></button></div><p class=\"caption\" id=\"switches\"></p><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"sharedNote\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"separateNote\"></p></div><details class=\"scope\"><summary data-i18n=\"scope\"></summary><p class=\"note\" data-i18n=\"note\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.note,.caption{font-size:12px;color:var(--muted);margin-top:14px}.controls{display:grid;gap:12px;margin:18px 0}.control{display:grid;grid-template-columns:1fr auto;gap:6px 12px;align-items:center;font-size:13px}.control input{grid-column:1/3;width:100%;margin:0}.control output{color:var(--accent);font-variant-numeric:tabular-nums}.presets{display:flex;flex-wrap:wrap;gap:4px}.presets button{flex:1 1 auto;min-width:min-content}.actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:14px}.actions button{flex:1;min-width:0;min-height:42px;font-size:13px}.copy{display:grid;margin-top:16px;font-size:14px}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.metrics{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin:16px 0}.metrics>div{min-width:0;border-left:2px solid var(--accent);padding-left:12px}.metrics span{display:block;color:var(--muted);font-size:12px;min-height:2.8em}.metrics strong{display:block;font-size:22px;font-weight:500;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.formula{font-size:13px;overflow-wrap:anywhere;border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-variant-numeric:tabular-nums}.formula div+div{margin-top:6px}.two{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:18px}.card{min-width:0;border:1px solid var(--rule);border-radius:9px;padding:14px}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card code{font-size:12px;overflow-wrap:anywhere}.card .value{font-size:24px;font-weight:500;font-variant-numeric:tabular-nums}.card small{display:block;color:var(--muted);font-size:12px}.card button{width:100%;min-width:0;font-size:13px;min-height:42px}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{accent-color:var(--accent);width:16px;height:16px;flex-shrink:0}.pill{display:inline-block;padding:3px 8px;border-radius:5px;background:var(--accent-soft);color:var(--accent);font-size:12px}.plot{position:relative;margin:18px 0}.plot svg{width:100%;height:auto;overflow:visible}.plot text{font-size:12px;fill:var(--muted)}.plot .ink{fill:var(--ink)}.plot .blue{fill:var(--accent);stroke:var(--accent)}.plot .gold{fill:var(--second);stroke:var(--second)}.plot .area{fill:var(--accent-soft);stroke:var(--accent);transition:all .25s}.plot .other{fill:var(--second-soft);stroke:var(--second)}.plot line,.plot path,.plot rect,.plot circle{transition:all .25s}.legend{display:flex;flex-wrap:wrap;gap:6px 16px;font-size:12px;color:var(--muted);margin-top:10px}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:5px}.legend .gold:before{background:var(--second)}details{margin-top:14px;font-size:12px;color:var(--muted)}summary{cursor:pointer;padding:8px 0}button:disabled{cursor:default}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.card button{font-size:12px}.metrics{gap:10px}.metrics strong{font-size:20px}.actions button{font-size:12px}.plot text{font-size:13px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n\n.cpu{margin-top:18px;padding:16px;background:var(--accent-soft);border:1px solid var(--accent);border-radius:10px}.cpu header{font-weight:600;margin-bottom:10px}.cpu>div{display:flex;justify-content:space-between;font-size:13px;margin:5px 0}.cpu strong{font:600 16px var(--viz-font-ui)}.cpu>code{display:block;margin-top:12px;font-size:12px;overflow-wrap:anywhere;min-height:3.4em}.stack{transition:border-color .25s,background .25s}.stack.active{border-color:var(--accent);background:var(--accent-soft)}.stack-frame{border:1px solid var(--rule);padding:9px 7px;font-size:12px;overflow-wrap:anywhere;min-height:3.4em;background:var(--paper)}.stack-frame+.stack-frame{border-top:0}.stack .local{display:flex;justify-content:space-between;gap:8px;align-items:center;margin:14px 0;font-size:12px;min-height:3.4em}.local strong{font-size:20px}.stack header small{display:block;font-weight:400}","js":"const $=s=>document.querySelector(s);let mode='shared',current='A',prev='—',locals={A:3,B:8},switches=0;\nfunction draw(){$('#current').textContent=current;$('#sp').textContent='SP_'+current;$('#mm').textContent=mode==='shared'||current==='A'?'M1':'M2';$('#continuation').textContent=prev==='—'?'worker_'+current+'()':'finish_task_switch(prev = '+prev+')';for(const k of 'AB'){$('#stack-'+k).classList.toggle('active',current===k);$('#mm-'+k).textContent='mm → '+(mode==='shared'||k==='A'?'M1':'M2');$('#frame-'+k).textContent=current===k?viz.t('resumed'):'switch_to()';$('#local-'+k).textContent=locals[k];document.querySelector('[data-task=\"'+k+'\"]').disabled=current===k;}document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));$('#switches').textContent=viz.t('switchCount')+' '+switches;$('#explanation').textContent=viz.t(mode==='shared'?'sharedNote':'separateNote');}\ndocument.querySelectorAll('[data-task]').forEach(e=>e.addEventListener('click',()=>{prev=current;current=e.dataset.task;switches++;draw();}));document.querySelectorAll('[data-mode]').forEach(e=>e.addEventListener('click',()=>{mode=e.dataset.mode;draw();}));$('#work').addEventListener('click',()=>{locals[current]++;draw();});$('#reset').addEventListener('click',()=>{current='A';prev='—';locals={A:3,B:8};switches=0;draw();});draw();\n","audio":false,"strings":{"eyebrow":"01 · 执行现场的归属","heading":"换到 B,为什么会沿 B 的调用链返回?","intro":"直接选择 CPU 接续哪条内核调用链,再执行一次该任务的局部操作。切换场景,比较共享 mm 与不同 mm。","local":"栈中示意局部值","note":"两个已经运行过、当前可运行的任务;只画内核续接点,省略入口帧、TLS、FPU 和锁协议。局部值是归属示意,不是 Linux 某字段。切换场景用于比较关系,不表示运行时修改线程共享属性。","relation":"任务关系","reset":"重置","resumed":"恢复此调用链","scope":"模型范围与约定","separate":"两个独立进程","separateNote":"换任务时,SP 与 active_mm 都跟随新的任务。刚换下的任务留在自己的栈上,局部值不会被另一任务的执行覆盖。","shared":"同进程的两个线程","sharedNote":"两个任务引用同一 M1,但 SP 必须在 SP_A 与 SP_B 之间切换。CPU 接着使用被选中任务自己的栈和局部值。","switch":"切到此任务","switchCount":"任务切换次数","work":"执行当前任务的局部操作"}}
两类保存现场,不要混在一张寄存器表里
保存位置 主要服务于哪次恢复
系统调用/异常入口帧,如适用的 pt_regs 最后回到用户执行位置
内核调用栈与低层切换帧 接续任务暂停的内核调用链
x86-64 低层汇编保存 callee-saved 寄存器和栈位置;其他值由调用约定、编译器栈帧或入口路径承担。因此只数 switch_to 中的 push/pop,不能得到“完整用户上下文有多少寄存器”的答案。
恢复的是 B,收尾对象却是刚换下的 A
假设 B 上一次被换下时让位给 D;这次 A 切到 B 后,B 继续执行的调度收尾必须知道本次刚被换下的是 A ,而不是把上一次的 D 当成待清理任务。低层切换接口与通用收尾正是在协调这种跨栈交接。新创建任务尚未走过一次普通的暂停/返回过程,因此使用预先构造的初始上下文和启动跳板进入执行。
为什么不能在退出任务自己的栈上把栈立即释放?因为 CPU 此刻还在使用它。必须先切到其他任务,再完成适用的延后释放和记账。调度锁、抢占计数及 RCU 等状态也要按专门协议跨过这段边界;不能将 switch_to 抽出来当成普通的“保存数组、恢复数组”函数使用。
4. TLS、FPU 和 TLB:成本为何随工作负载变化
两条共享 mm 的线程也有不同的线程局部存储。x86-64 的 FS/GS 相关状态等需要与当前任务匹配,否则同一条访问线程局部变量的指令可能读到别人的数据。perf、调试寄存器、I/O bitmap 等状态是否需要额外操作,还取决于任务启用了什么功能。
FPU:保存与装入不必发生在同一个时刻
FPU/SIMD 扩展状态具有任务所有权。现代 x86 Linux 的路径需要按当前实现理解,不能继续套用“设置 TS,下一次 #NM 再保存旧任务”的旧式 lazy FPU 图解。
本机 arch/x86/include/asm/fpu/sched.h 在需要时保存旧状态,通过包括 TIF_NEED_FPU_LOAD 在内的协议协调装入;某些恢复可推迟到适当的返回/使用边界。AMX 等动态扩展还涉及状态大小、权限和按需机制,进一步影响处理成本。x86 扩展状态管理
TLB:映射失效与任务切换分别分析
TLB shootdown 解决的是另一件事:页表变化后,使可能缓存旧翻译的 CPU 不再使用它。它可以在任务不发生切换时出现,也不必在每次不同 mm 切换时发生。即使低层交接已完成,下一任务仍可能遭遇缓存、TLB 或分支预测局部性损失;这些后续访存和执行代价往往比切换汇编本身更能解释吞吐下降。
下面让 CPU 1 固定使用 M1,CPU 0 则可在 M1 / M2 之间切换。先比较有效翻译的复用,再不换任务而修改 M1 的页表,观察两个 CPU 的旧翻译为何都不能继续使用。
正在呈现知识画面 重新加载
切换任务,等于清空所有翻译吗? 有效的 PCID 标记翻译可以跨 mm 切换保留;页表更新则要求旧翻译不再被使用,即使没有发生任务切换。
{"id":"context-tlb","title":"切换任务,等于清空所有翻译吗?","summary":"有效的 PCID 标记翻译可以跨 mm 切换保留;页表更新则要求旧翻译不再被使用,即使没有发生任务切换。","height":1000,"html":"<p class=\"eyebrow\" data-i18n=\"eyebrow\"></p><h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><label class=\"check\"><input id=\"pcid\" type=\"checkbox\" checked><span data-i18n=\"pcid\"></span></label><div class=\"page-tables\"><div><code>M1 · VA 0x1000</code><strong id=\"mapping\">P1 · gen 1</strong><button id=\"remap\" data-i18n=\"remap\"></button></div><div><code>M2 · VA 0x1000</code><strong>P2 · gen 1</strong></div></div><div class=\"two\"><section class=\"card cpu-cache\" id=\"cpu-0\"><header>CPU 0</header><div class=\"presets\" role=\"group\" data-i18n-label=\"current\"><button data-mm=\"0\">M1</button><button data-mm=\"1\">M2</button></div><div class=\"cache-entry\" id=\"cache-0-0\"></div><div class=\"cache-entry\" id=\"cache-0-1\"></div><button data-cpu=\"0\" data-i18n=\"access\"></button><small class=\"last-access\" data-i18n=\"lastAccess\"></small><div class=\"access-result\" id=\"result-0\"></div></section><section class=\"card cpu-cache\" id=\"cpu-1\"><header>CPU 1</header><p class=\"fixed-mm\">M1</p><div class=\"cache-entry\" id=\"cache-1-0\"></div><div class=\"cache-entry\" id=\"cache-1-1\"></div><button data-cpu=\"1\" data-i18n=\"access\"></button><small class=\"last-access\" data-i18n=\"lastAccess\"></small><div class=\"access-result\" id=\"result-1\"></div></section></div><div class=\"metrics\"><div><span data-i18n=\"switchCount\"></span><strong id=\"switch-count\">0</strong></div><div><span data-i18n=\"invalidateCount\"></span><strong id=\"invalidations\">0</strong></div></div><div class=\"actions\"><button id=\"reset\" data-i18n=\"reset\"></button></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"initial\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"switchNote\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"remapNote\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"accessNote\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"pcidNote\"></p></div><details class=\"scope\"><summary data-i18n=\"scope\"></summary><p class=\"note\" data-i18n=\"note\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.note,.caption{font-size:12px;color:var(--muted);margin-top:14px}.controls{display:grid;gap:12px;margin:18px 0}.control{display:grid;grid-template-columns:1fr auto;gap:6px 12px;align-items:center;font-size:13px}.control input{grid-column:1/3;width:100%;margin:0}.control output{color:var(--accent);font-variant-numeric:tabular-nums}.presets{display:flex;flex-wrap:wrap;gap:4px}.presets button{flex:1 1 auto;min-width:min-content}.actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:14px}.actions button{flex:1;min-width:0;min-height:42px;font-size:13px}.copy{display:grid;margin-top:16px;font-size:14px}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.metrics{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin:16px 0}.metrics>div{min-width:0;border-left:2px solid var(--accent);padding-left:12px}.metrics span{display:block;color:var(--muted);font-size:12px;min-height:2.8em}.metrics strong{display:block;font-size:22px;font-weight:500;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.formula{font-size:13px;overflow-wrap:anywhere;border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-variant-numeric:tabular-nums}.formula div+div{margin-top:6px}.two{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:18px}.card{min-width:0;border:1px solid var(--rule);border-radius:9px;padding:14px}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card code{font-size:12px;overflow-wrap:anywhere}.card .value{font-size:24px;font-weight:500;font-variant-numeric:tabular-nums}.card small{display:block;color:var(--muted);font-size:12px}.card button{width:100%;min-width:0;font-size:13px;min-height:42px}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{accent-color:var(--accent);width:16px;height:16px;flex-shrink:0}.pill{display:inline-block;padding:3px 8px;border-radius:5px;background:var(--accent-soft);color:var(--accent);font-size:12px}.plot{position:relative;margin:18px 0}.plot svg{width:100%;height:auto;overflow:visible}.plot text{font-size:12px;fill:var(--muted)}.plot .ink{fill:var(--ink)}.plot .blue{fill:var(--accent);stroke:var(--accent)}.plot .gold{fill:var(--second);stroke:var(--second)}.plot .area{fill:var(--accent-soft);stroke:var(--accent);transition:all .25s}.plot .other{fill:var(--second-soft);stroke:var(--second)}.plot line,.plot path,.plot rect,.plot circle{transition:all .25s}.legend{display:flex;flex-wrap:wrap;gap:6px 16px;font-size:12px;color:var(--muted);margin-top:10px}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:5px}.legend .gold:before{background:var(--second)}details{margin-top:14px;font-size:12px;color:var(--muted)}summary{cursor:pointer;padding:8px 0}button:disabled{cursor:default}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.card button{font-size:12px}.metrics{gap:10px}.metrics strong{font-size:20px}.actions button{font-size:12px}.plot text{font-size:13px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n\n.page-tables{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:14px}.page-tables>div{border:1px solid var(--rule);border-radius:8px;padding:12px;min-width:0;display:flex;flex-direction:column;gap:8px}.page-tables code{font-size:11px;overflow-wrap:anywhere}.page-tables strong{font-size:14px}.page-tables button{font-size:12px;min-width:0;margin-top:auto}.cache-entry{padding:10px 6px;margin:10px 0;border:1px solid var(--rule);background:var(--surface);font-size:12px;min-height:5.1em;overflow-wrap:anywhere}.cache-entry.valid{border-color:var(--accent);background:var(--accent-soft)}.cache-entry.stale{text-decoration:line-through;color:var(--muted)}.cache-entry span{display:block;font-size:11px;color:var(--muted);min-height:3.2em}.fixed-mm{display:flex;align-items:center;justify-content:center;min-height:50px;margin:0}.last-access{margin-top:12px;min-height:2.8em}.access-result{font-size:12px;margin-top:4px;min-height:5.1em;overflow-wrap:anywhere}.cpu-cache .presets{min-height:50px}.cpu-cache .presets button{padding-inline:4px;font-size:12px}.cpu-cache .presets button{width:auto}","js":"const $=s=>document.querySelector(s);let current=0,gen=1,page='P1',cache=[[{page:'P1',gen:1,valid:true},null],[{page:'P1',gen:1,valid:true},null]],results=['—','—'],switches=0,invalidations=0,message='initial';\nfunction draw(){$('#mapping').textContent=page+' · gen '+gen;document.querySelectorAll('[data-mm]').forEach(e=>e.setAttribute('aria-pressed',String(+e.dataset.mm===current)));for(let c=0;c<2;c++){for(let m=0;m<2;m++){const e=$('#cache-'+c+'-'+m),v=cache[c][m];e.className='cache-entry '+(v?(v.valid?'valid':'stale'):'');e.textContent='M'+(m+1)+' → '+(v?v.page:'—');const span=document.createElement('span');span.textContent=v?'gen '+v.gen+' · '+viz.t(v.valid?'valid':'invalid'):viz.t('empty');e.append(span);}$('#result-'+c).textContent=results[c];}$('#switch-count').textContent=switches;$('#invalidations').textContent=invalidations;$('#explanation').textContent=viz.t(message);}\ndocument.querySelectorAll('[data-mm]').forEach(e=>e.addEventListener('click',()=>{const next=+e.dataset.mm;if(next!==current){current=next;switches++;if(!$('#pcid').checked)cache[0]=[null,null];}message='switchNote';draw();}));document.querySelectorAll('[data-cpu]').forEach(e=>e.addEventListener('click',()=>{const c=+e.dataset.cpu,m=c===0?current:0,target=m===0?page:'P2',generation=m===0?gen:1,v=cache[c][m],hit=!!v&&v.valid&&v.gen===generation;cache[c][m]={page:target,gen:generation,valid:true};results[c]=viz.t(hit?'hit':'miss')+' → '+target;message='accessNote';draw();}));$('#remap').addEventListener('click',()=>{gen++;page=page==='P1'?'P3':'P1';for(const c of cache)if(c[0])c[0].valid=false;invalidations++;message='remapNote';draw();});$('#pcid').addEventListener('change',()=>{cache=[[null,null],[null,null]];results=['—','—'];message='pcidNote';draw();});$('#reset').addEventListener('click',()=>{current=0;gen=1;page='P1';cache=[[{page:'P1',gen:1,valid:true},null],[{page:'P1',gen:1,valid:true},null]];results=['—','—'];switches=invalidations=0;message='initial';draw();});draw();\n","audio":false,"strings":{"access":"访问 0x1000","accessNote":"命中只复用标签和代次都有效的翻译。下方保留上次访问的结果;修改页表不会修改此前已经读出的结果。","current":"CPU 0 当前地址空间","empty":"未缓存","eyebrow":"02 · TLB 与映射失效","heading":"切换任务,等于清空所有翻译吗?","hit":"命中","initial":"初始两个 CPU 都缓存了 M1 的 P1。先切换 CPU 0 到 M2 再访问,随后切回 M1,观察有效条目能否复用。","intro":"CPU 0 可以切换 M1 / M2,CPU 1 保持使用 M1。分别访问同一虚拟地址,或直接修改 M1 的映射,比较缓存保留与失效。","invalid":"已失效","invalidateCount":"M1 映射更新次数","lastAccess":"上次访问结果","miss":"缺失,查页表","note":"只画两个地址空间、一个虚拟地址和非全局条目,省略 KPTI、PCID 回收等细节。映射更新展示逻辑失效,实际 Linux 可通过代次和延迟协议完成;不代表每次更新都立即对所有 CPU 发 IPI。","pcid":"允许 PCID 区分有效的缓存条目","pcidNote":"缓存模式切换清空了模型缓存。重新访问即可填入当前映射,再比较不同 mm 的切换。","remap":"更改 M1 映射并使旧条目失效","remapNote":"M1 的页表已改变,两个 CPU 的旧 M1 翻译都不能再使用。切换计数没有因此增加;下一次访问必须取得新映射。","reset":"重置","scope":"模型范围与约定","switchCount":"CPU 0 的 mm 切换次数","switchNote":"改变 CPU 0 的地址空间,不会要求 CPU 1 清空其缓存。允许标签复用时,M1 的有效条目可以留下;未启用时,本模型清除 CPU 0 的普通条目。","valid":"有效"}}
因此可将观察到的损失分成直接交接工作与恢复局部性的间接代价,并另外计算运行队列等待。它们存在联系,但只有在明确了测量边界后,才适合比较不同优化方案。
5. 等待协议:调度器不会替应用补上丢失的通知
错误协议很容易写出来:消费者先检查 ready == false,准备睡眠;生产者随后写入数据并唤醒等待队列;消费者最后才把自己放入队列。生产者看到空队列,没有人可唤醒,消费者却在条件已经为真的情况下睡下。若没有后续事件,程序可能永远等不到下一次通知。
丢失唤醒:通知发生时,等待者尚未登记
消费者
生产者
检查 ready = false
登记等待并睡眠
写入数据,令 ready = true
wake:此时队列为空
若没有下一次通知,可能一直等待
正确协议让“登记等待 + 检查条件 + 进入睡眠”与发布者协调;单加 volatile 无法修补。
内核等待接口通过登记等待项、设置适当状态、检查条件并在需要时调度来协调这个窗口。条件的发布还要使用匹配的锁或内存序;READ_ONCE/WRITE_ONCE 防止特定编译访问变形,并不单独建立完整的跨 CPU 发布和等待协议。调用 wake_up() 也不是把一个持久事件塞进“通知邮箱”:被唤醒者应重新检查业务条件,多个等待者还可能竞争同一份数据。
修复窗口:让条件检查与进入等待协调起来
用户态条件变量表达相似约束。消费者持锁检查条件,pthread_cond_wait() 原子地释放互斥锁并进入等待;返回时重新持有锁,再用循环检查条件。生产者在同一把锁下改变条件。这样,即使某次通知发生时消费者尚未等待,消费者后来也能看到持久的条件值,不会仅因错过通知就必然阻塞。
尝试“检查 → 发布 → 等待”,再直接切换到同锁保护协议。操作顺序会保留,但锁改变了生产者能否在这个窗口内完成发布。
正在呈现知识画面 重新加载
条件为真,消费者为什么还在睡? 检查后、登记等待前发生通知,可能让消费者在条件为真时睡下。同锁保护条件,并协调释放锁与进入等待,可以关闭这个窗口。
{"id":"context-wakeup","title":"条件为真,消费者为什么还在睡?","summary":"检查后、登记等待前发生通知,可能让消费者在条件为真时睡下。同锁保护条件,并协调释放锁与进入等待,可以关闭这个窗口。","height":1000,"html":"<p class=\"eyebrow\" data-i18n=\"eyebrow\"></p><h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"presets\" role=\"group\" data-i18n-label=\"protocol\"><button data-mode=\"unsafe\" data-i18n=\"unsafe\"></button><button data-mode=\"safe\" data-i18n=\"safe\"></button></div><p class=\"caption\" data-i18n=\"history\"></p><div id=\"history\" class=\"history\"></div><div class=\"condition\"><span>ready</span><strong id=\"ready\">false</strong><span id=\"mutex\"></span></div><div class=\"two\"><section class=\"card\"><header data-i18n=\"consumer\"></header><div class=\"state\" id=\"consumer-state\"></div><div class=\"wait-slot\" id=\"wait-slot\"></div><button id=\"check\" data-i18n=\"check\"></button><button id=\"wait\" data-i18n=\"wait\"></button></section><section class=\"card\"><header data-i18n=\"producer\"></header><div class=\"state\" id=\"producer-state\"></div><div class=\"wait-slot\" id=\"notification\"></div><button id=\"publish\" data-i18n=\"publish\"></button></section></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"initial\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"checked\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"published\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"sleeping\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"blocked\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"lost\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"woken\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"done\"></p></div><div class=\"actions\"><button id=\"reset\" data-i18n=\"reset\"></button></div><details class=\"scope\"><summary data-i18n=\"scope\"></summary><p class=\"note\" data-i18n=\"note\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.note,.caption{font-size:12px;color:var(--muted);margin-top:14px}.controls{display:grid;gap:12px;margin:18px 0}.control{display:grid;grid-template-columns:1fr auto;gap:6px 12px;align-items:center;font-size:13px}.control input{grid-column:1/3;width:100%;margin:0}.control output{color:var(--accent);font-variant-numeric:tabular-nums}.presets{display:flex;flex-wrap:wrap;gap:4px}.presets button{flex:1 1 auto;min-width:min-content}.actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:14px}.actions button{flex:1;min-width:0;min-height:42px;font-size:13px}.copy{display:grid;margin-top:16px;font-size:14px}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.metrics{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin:16px 0}.metrics>div{min-width:0;border-left:2px solid var(--accent);padding-left:12px}.metrics span{display:block;color:var(--muted);font-size:12px;min-height:2.8em}.metrics strong{display:block;font-size:22px;font-weight:500;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.formula{font-size:13px;overflow-wrap:anywhere;border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-variant-numeric:tabular-nums}.formula div+div{margin-top:6px}.two{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:18px}.card{min-width:0;border:1px solid var(--rule);border-radius:9px;padding:14px}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card code{font-size:12px;overflow-wrap:anywhere}.card .value{font-size:24px;font-weight:500;font-variant-numeric:tabular-nums}.card small{display:block;color:var(--muted);font-size:12px}.card button{width:100%;min-width:0;font-size:13px;min-height:42px}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{accent-color:var(--accent);width:16px;height:16px;flex-shrink:0}.pill{display:inline-block;padding:3px 8px;border-radius:5px;background:var(--accent-soft);color:var(--accent);font-size:12px}.plot{position:relative;margin:18px 0}.plot svg{width:100%;height:auto;overflow:visible}.plot text{font-size:12px;fill:var(--muted)}.plot .ink{fill:var(--ink)}.plot .blue{fill:var(--accent);stroke:var(--accent)}.plot .gold{fill:var(--second);stroke:var(--second)}.plot .area{fill:var(--accent-soft);stroke:var(--accent);transition:all .25s}.plot .other{fill:var(--second-soft);stroke:var(--second)}.plot line,.plot path,.plot rect,.plot circle{transition:all .25s}.legend{display:flex;flex-wrap:wrap;gap:6px 16px;font-size:12px;color:var(--muted);margin-top:10px}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:5px}.legend .gold:before{background:var(--second)}details{margin-top:14px;font-size:12px;color:var(--muted)}summary{cursor:pointer;padding:8px 0}button:disabled{cursor:default}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.card button{font-size:12px}.metrics{gap:10px}.metrics strong{font-size:20px}.actions button{font-size:12px}.plot text{font-size:13px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n\n.history{display:grid;grid-template-columns:repeat(3,1fr);gap:6px}.history span{min-width:0;background:var(--surface);border-radius:5px;padding:8px;font-size:11px;min-height:6em;overflow-wrap:anywhere}.condition{display:flex;align-items:center;gap:12px;padding:14px;background:var(--accent-soft);border-radius:8px;margin-top:18px;flex-wrap:wrap;font-size:13px}.condition strong{font-family:var(--viz-font-ui)}.condition span:last-child{margin-left:auto;color:var(--muted);font-size:12px}.state{font-size:14px;min-height:4.8em}.wait-slot{padding:12px 8px;border:1px dashed var(--rule);border-radius:7px;font-size:12px;color:var(--muted);min-height:7em;margin:10px 0}.wait-slot.occupied{background:var(--second-soft);border-color:var(--second);color:var(--ink)}.two .card button+button{margin-top:8px}.card{display:flex;flex-direction:column}.card button:last-child{margin-top:auto}.card #wait{margin-top:8px}","js":"const $=s=>document.querySelector(s);let mode='unsafe',history=[];\nfunction state(){const s={ready:false,checked:false,asleep:false,lock:false,queued:false,done:false,notified:false,message:'initial'};function publish(){s.ready=true;s.queued=false;s.notified=s.asleep;if(s.asleep){s.asleep=false;s.checked=false;s.message='woken';}else s.message='published';}\nfor(const a of history){if(a==='check'&&!s.asleep&&!s.done){if(mode==='safe')s.lock=true;if(s.ready){s.done=true;s.lock=false;s.checked=false;s.message='done';}else{s.checked=true;s.message='checked';}}\nif(a==='publish'&&!s.done){if(mode==='safe'&&s.lock){s.queued=true;s.message='blocked';}else publish();}\nif(a==='wait'&&s.checked&&!s.asleep&&!s.done){s.asleep=true;s.checked=false;s.lock=false;s.message=s.ready?'lost':'sleeping';if(s.queued)publish();}}\nreturn s;}\nfunction draw(){const s=state();$('#history').replaceChildren(...Array.from({length:3},(_,i)=>{const e=document.createElement('span'),start=Math.max(0,history.length-3),action=history[start+i];e.textContent=action?(start+i+1)+'. '+viz.t(action):'—';return e;}));$('#ready').textContent=String(s.ready);$('#mutex').textContent=viz.t(mode==='unsafe'?'noMutex':s.lock?'locked':'unlocked');$('#consumer-state').textContent=viz.t(s.done?'consumed':s.asleep?'asleep':s.checked?'checkedState':'awake');$('#producer-state').textContent=viz.t(s.queued?'waitingLock':s.ready?'dataPublished':'notPublished');$('#wait-slot').textContent=viz.t(s.asleep?'registered':'unregistered');$('#wait-slot').classList.toggle('occupied',s.asleep);$('#notification').textContent=viz.t(s.notified?'wakeDelivered':s.ready?'noWaiter':'noNotification');$('#notification').classList.toggle('occupied',s.notified);$('#check').disabled=s.asleep||s.done;$('#wait').disabled=!s.checked||s.asleep||s.done;$('#publish').disabled=s.queued||s.done;$('#explanation').textContent=viz.t(s.message);document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}\nfor(const id of ['check','wait','publish'])$('#'+id).addEventListener('click',()=>{history.push(id);draw();});document.querySelectorAll('[data-mode]').forEach(e=>e.addEventListener('click',()=>{mode=e.dataset.mode;draw();}));$('#reset').addEventListener('click',()=>{history=[];draw();});draw();\n","audio":false,"strings":{"asleep":"睡眠中","awake":"可执行检查","blocked":"生产者被互斥锁挡住。消费者进入等待并释放锁后,生产者才能发布和通知。","check":"检查 ready","checked":"消费者看见 ready=false。无锁版本留下窗口;同锁版本继续持锁,生产者暂不能改变条件。","checkedState":"检查过:条件为假","consumed":"读到数据,结束等待","consumer":"消费者","dataPublished":"数据已发布","done":"消费者检查到数据,结束等待。持久的条件决定是否继续,通知本身不是数据。","eyebrow":"03 · 丢失唤醒","heading":"条件为真,消费者为什么还在睡?","history":"最近三次操作(切换协议保留顺序)","initial":"先检查,再发布,最后等待,能暴露错误协议的窗口;也可以尝试其他顺序。","intro":"自由安排检查、发布和等待的先后。切换协议会重放同一串操作,比较通知是否落在等待窗口之外。","locked":"消费者持锁","lost":"ready 已经为真,消费者却睡着了:此前的通知没有等待者,之后登记也不会补发。","noMutex":"无互斥锁","noNotification":"尚无通知","noWaiter":"通知时没有等待者","notPublished":"尚未发布","note":"图把操作分开供你安排交错。安全协议中,检查持锁,等待原子地释放锁并登记;被挡住的生产者随后完成发布。唤醒表示可继续竞争锁,下一次检查包含重新取得锁。省略伪唤醒和多个消费者;真实代码仍须用 while 检查。","producer":"生产者","protocol":"等待协议","publish":"发布数据并通知","published":"数据已经发布,但当时没有等待者。通知不会保存成一张未来可领取的票。","registered":"等待队列里有消费者","reset":"清空操作","safe":"同锁保护条件与等待","scope":"模型范围与约定","sleeping":"消费者已登记等待。后续发布可以找到它并唤醒。","unlocked":"锁可获取","unregistered":"等待队列为空","unsafe":"无锁检查后等待","wait":"登记并等待","waitingLock":"等待消费者释放锁","wakeDelivered":"通知已唤醒消费者","woken":"通知找到了等待者,消费者恢复为可运行。它仍须重新取得锁并检查条件,才能继续。"}}
实验:交替 1000 轮,检查条件才是依据
下面用两线程交替执行 1000 轮,验证条件和等待的配合。保存为 handoff.c,编译命令为 cc -std=c11 -O2 -Wall -Wextra -Werror -pthread handoff.c -o handoff。两条线程只改自己的同步对象,不修改系统调度设置。
#define _POSIX_C_SOURCE 200809L
#include < assert.h>
#include < pthread.h>
#include < stdio.h>
#include < time.h>
static pthread_mutex_t mu = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t cv = PTHREAD_COND_INITIALIZER;
static int turn;
static int consumed;
enum { ROUNDS = 1000 } ;
static void * worker ( void * unused ) {
( void ) unused;
for ( int i = 0 ; i < ROUNDS; + + i) {
assert ( pthread_mutex_lock ( & mu) == 0 ) ;
while ( turn != 1 )
assert ( pthread_cond_wait ( & cv, & mu) == 0 ) ;
+ + consumed;
turn = 0 ;
assert ( pthread_cond_signal ( & cv) == 0 ) ;
assert ( pthread_mutex_unlock ( & mu) == 0 ) ;
}
return NULL ;
}
int main ( void ) {
pthread_t thread;
struct timespec start, end;
assert ( clock_gettime ( CLOCK_MONOTONIC, & start ) == 0 ) ;
assert ( pthread_create ( & thread, NULL , worker, NULL ) == 0 ) ;
assert ( pthread_mutex_lock ( & mu) == 0 ) ;
for ( int i = 0 ; i < ROUNDS; + + i) {
while ( turn != 0 )
assert ( pthread_cond_wait ( & cv, & mu) == 0 ) ;
turn = 1 ;
assert ( pthread_cond_signal ( & cv) == 0 ) ;
}
while ( turn != 0 )
assert ( pthread_cond_wait ( & cv, & mu) == 0 ) ;
assert ( consumed == ROUNDS ) ;
assert ( pthread_mutex_unlock ( & mu) == 0 ) ;
assert ( pthread_join ( thread, NULL ) == 0 ) ;
assert ( clock_gettime ( CLOCK_MONOTONIC, & end ) == 0 ) ;
double seconds = ( end. tv_sec - start. tv_sec )
+ ( end. tv_nsec - start. tv_nsec ) / 1e9 ;
printf ( " %d round trips; %.0f ns/round including startup and join\n " ,
ROUNDS, seconds * 1e9 / ROUNDS) ;
assert ( pthread_cond_destroy ( & cv) == 0 ) ;
assert ( pthread_mutex_destroy ( & mu) == 0 ) ;
}
不要用 NDEBUG 编译这个断言驱动的实验。while 不能改为 if:等待返回并不保证条件必然满足,条件才是继续执行的依据。这里仅有一位对端等待者,signal 足够;多消费者设计需要重新考虑条件划分、唤醒数量和退出协议。
输出是这项同步工作负载的往返时间,包含互斥锁、条件变量、可能的 futex 系统调用、调度等待、线程启动与回收,不是纯上下文切换耗时。即使每轮都发生等待,也不能未经事件统计就把每轮固定折算成两次切换;两个线程还可能在不同 CPU 上执行。
6. 从计数到原因:应该测量哪一段
/proc/PID/status 或 getrusage() 的自愿/非自愿切换计数可以帮助判断变化方向,但“自愿”不等于业务代码主动调用 yield,阻塞等待也常在其中;“非自愿”也不等于系统发生故障。应把增量与完成的请求数、线程数、CPU 使用量放在同一窗口比较。
若研究唤醒延迟,可关联调度 tracepoint:记录目标 TID 的 sched_wakeup 时间,再找到它作为 next 的 sched_switch。需要处理重复唤醒、任务退出与 TID 复用、跨 CPU 时间戳以及采集窗口缺失,不能简单把任何两条同 PID 日志相减。得到的是唤醒到获得 CPU 的等待,包含竞争和相关调度路径,并没有隔离低层 switch_to 成本。
把指标变化与候选原因对应起来,再决定下一步:
观察结果 值得继续验证的方向
切换更多,吞吐基本不变 线程协作方式是否改变
尾延迟上升,运行队列等待也增长 CPU 竞争、quota 与放置约束
排队不明显,迁移后执行变慢 缓存局部性与 NUMA
这些是排查方向,不能单凭一项计数就下因果结论。减少线程、调整批量或锁粒度,都应针对已经确认的等待或执行成本。
阅读源码可依次从 kernel/sched/core.c 的 __schedule() / context_switch() 进入,再看架构 switch_to、mm/TLB 和 FPU 路径。调度器为何选择这个 next,回到调度器 ;信号如何使某些等待结束,以及为什么可能返回 EINTR,继续看信号与 ptrace 。