A read() on an empty pipe provides a concrete example: A waits for data, the CPU runs B, B writes and wakes A, and only later does A resume its read.
Distinguish kernel entry, waiting for an event, waiting for CPU time, and resuming execution. They all affect call latency but suggest different remedies.
Question
Start here
Why sleep in read, then wait again after wakeup?
Section 1: latency timeline
Who owns the stack and address space?
Sections 2–3: mm and switch_to
Why does switch cost depend on workload?
Section 4: TLS, FPU, and TLB
Why sleep forever after a notification?
Sections 5–6: waiting and measurement
Examples use the generic scheduler and x86-64, checked against Linux 7.2. Internal function ordering and architecture details are not a stable ABI.
1. Start with a blocking read
A calls read() on a pipe with no data and at least one writer. System-call entry enters A's kernel execution path, saving the applicable user return state. The current task is still A. If data were already available, the call could return without switching tasks at all.
With no data, the kernel arranges the wait, checks the condition, and lets A sleep when necessary. Only after the scheduler selects B does the CPU save A's execution context and resume B.
B's write wakes A, restoring its opportunity to run. When A is selected later, it resumes its own waiting path, rechecks the condition, reads the data, and returns to user space.
Where A waits
What is missing
Investigate
Before data arrives
The read condition
Producer, I/O, application dependencies
Woken but not running
CPU execution opportunity
Competition, quota, affinity, preemption delay
Running read again
Remaining execution
Read/return work and memory costs
Higher priority cannot make unproduced data appear. Find the interval before choosing a scheduling remedy.
A hardware interrupt does not automatically switch tasks either. It can interrupt A and return to A. A task handoff occurs only when an applicable exit path or another scheduling point selects a different task. Kernel entries, syscalls, and context switches count different things.
2. context_switch: address space and execution flow
After choosing next, follow three logical stages in kernel/sched/core.c:
Prepare accounting and the old/new mm relationship.
Use architecture switch_to to change stacks and the necessary register state.
Finish the handoff in the new task's context, accounting for the task just switched out.
The next section shows why finishing on the new stack matters.
Relationship
Address-space work
Execution state still required
Ordinary threads in one process
Usually share a user mm
Separate kernel stacks, registers, TLS, FPU state
Independent processes
Usually have different mm objects
Both execution context and user address space
Switching to a kernel thread
Usually next->mm == NULL
active_mm borrowing and deferred management
Sharing mm can save address-space work; it does not eliminate context preservation. Different mm objects do not imply unconditional flushing of the entire TLB. On x86, CR3 identifies the page-table root and, when enabled, a PCID. Tagged translations can coexist, but cached context and invalidation generations still determine whether reuse is valid. A tag cannot legitimize an obsolete mapping.
A kernel thread's active_mm maintains a suitable address-space context without an owned user mm. References and deferred release cooperate with architecture lazy-TLB protocols. This cannot be reduced to assigning mm = &init_mm or freeing the old mm before the handoff.
3. Why switch_to seems to return on another call chain
Every task has its own kernel stack. When A calls the scheduler, its unfinished kernel call chain remains on A's stack. The low-level switch saves the stack position and architecture state needed to resume A, then installs B's saved stack position. If B previously paused through the scheduling path, execution continues on B's suspended call chain and local state.
Choose A or B directly, then change a local value on the selected task. Compare shared and separate mm scenarios: sharing an address space never merges these kernel stacks.
Preparing the visual
Why does switching to B resume B’s call chain?
Threads sharing mm still have separate kernel stacks. After SP changes, the CPU resumes the selected task’s call chain and local state, finishing the task just switched out.
{"id":"context-stack-ownership","title":"Why does switching to B resume B’s call chain?","summary":"Threads sharing mm still have separate kernel stacks. After SP changes, the CPU resumes the selected task’s call chain and local state, finishing the task just switched out.","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 · Ownership of execution state","heading":"Why does switching to B resume B’s call chain?","intro":"Choose the kernel call chain for the CPU to resume, then perform a local operation. Compare a shared mm with separate address spaces.","local":"Illustrative stack-local value","note":"Two previously running, currently runnable tasks. Only kernel continuation points are shown; entry frames, TLS, FPU, and lock protocols are omitted. Local values illustrate ownership, not a Linux field. Changing the scenario compares relationships rather than mutating live thread sharing.","relation":"Task relationship","reset":"Reset","resumed":"Resume this call chain","scope":"Model scope and assumptions","separate":"Separate processes","separateNote":"SP and active_mm follow the newly selected task. The previous task’s call chain remains on its own stack, and its local value is not overwritten by the other task.","shared":"Threads sharing one process","sharedNote":"Both tasks reference M1, but SP still switches between SP_A and SP_B. The CPU uses the selected task’s own stack and local value.","switch":"Switch to this task","switchCount":"Task switches","work":"Run the current task’s local operation"}}
Two saved contexts serve different returns
Saved location
What it resumes
Syscall/exception entry frame, such as applicable pt_regs
The eventual return to user execution
Kernel call stack and low-level switch frame
The suspended kernel call chain
x86-64 switch assembly saves callee-saved registers and the stack position. Calling conventions, compiler frames, and entry paths preserve other required values. Counting switch_to's pushes and pops does not count the complete user context.
B resumes, but A is the task to finish switching out
Suppose B previously switched out in favor of D. When A now switches to B, the resumed scheduler cleanup must identify A, not the old D, as the task just switched out. The low-level interface and common cleanup coordinate this cross-stack handoff. A newly created task has no ordinary previous scheduler return to resume; it uses a prepared initial context and startup trampoline.
An exiting task cannot immediately free the stack the CPU is still using. Switch to another task first, then perform the applicable deferred release and accounting. Scheduler locks, preemption counts, and RCU also cross this boundary through dedicated protocols. switch_to is not an isolated “save one array, restore another” utility.
4. TLS, FPU, and TLB make costs workload-dependent
Threads sharing mm still have separate thread-local storage. x86-64 FS/GS-related state must match the current task, or a thread-local access could reach someone else's data. Performance monitoring, debug registers, and I/O bitmaps can add work depending on enabled features.
Saving and loading FPU state need not coincide
FPU/SIMD extended state belongs to individual tasks. Modern x86 Linux should not be explained with the old “set TS, save the previous task on the next #NM” lazy-FPU diagram.
Local arch/x86/include/asm/fpu/sched.h saves old state when required and coordinates loading through protocols including TIF_NEED_FPU_LOAD. Some restoration may be deferred to appropriate return/use boundaries. Dynamic extensions such as AMX add state-size, permission, and on-demand handling considerations. x86 extended state
Mapping invalidation is separate from task switching
TLB shootdown prevents CPUs from using obsolete translations after a page-table change. It can happen without a task switch and need not happen on every different-mm switch.
The next figure keeps CPU 1 in M1 while CPU 0 can select M1 or M2. First test reuse of a valid tagged translation, then change M1's page table without changing either CPU's task. Both old M1 translations must become unusable.
Preparing the visual
Does a task switch clear every translation?
Valid PCID-tagged translations can survive mm switches. A page-table update must make obsolete translations unusable, even without a task switch.
{"id":"context-tlb","title":"Does a task switch clear every translation?","summary":"Valid PCID-tagged translations can survive mm switches. A page-table update must make obsolete translations unusable, even without a task switch.","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":"Access 0x1000","accessNote":"A hit requires a valid tag and generation. Each result is the last completed access; changing a page table does not change a previously obtained result.","current":"CPU 0 address space","empty":"Not cached","eyebrow":"02 · TLB and invalidation","heading":"Does a task switch clear every translation?","hit":"Hit","initial":"Both CPUs initially cache M1 → P1. Switch CPU 0 to M2, access it, then return to M1 and see whether its valid entry can be reused.","intro":"CPU 0 can select M1 or M2; CPU 1 stays in M1. Access the same virtual address or change M1’s mapping to compare retention with invalidation.","invalid":"Invalidated","invalidateCount":"M1 mapping updates","lastAccess":"Last access result","miss":"Miss; read page table","note":"Two address spaces, one virtual address, and non-global entries only; KPTI and PCID recycling are omitted. Mapping changes show logical invalidation; Linux may implement it with generations and deferred work. Not every update immediately sends every CPU an IPI.","pcid":"Allow PCID-tagged valid translations","pcidNote":"Changing the cache mode cleared the model’s caches. Access again to fill the current mapping, then compare address-space switches.","remap":"Change M1 and invalidate old entries","remapNote":"M1’s page table changed, so neither CPU may reuse the old M1 translation. The switch count did not increase. The next access must obtain the new mapping.","reset":"Reset","scope":"Model scope and assumptions","switchCount":"CPU 0 mm switches","switchNote":"Changing CPU 0’s address space does not clear CPU 1’s cache. With tagged reuse, M1’s valid entry can remain. Without it, this model clears CPU 0’s ordinary entries.","valid":"Valid"}}
After the low-level handoff, the resumed task may still pay for lost cache, TLB, or branch-predictor locality. These later costs can explain throughput loss better than switch assembly alone. Separate direct handoff work, indirect locality recovery, and run-queue waiting before comparing optimizations.
5. Waiting protocols: a scheduler cannot recover a lost notification
An unsafe consumer checks ready == false, intending to sleep. A producer publishes data and wakes the queue before the consumer registers. The producer sees no waiter; the consumer then registers and sleeps even though the condition is true. Without another event, it may wait indefinitely.
Kernel waiting interfaces coordinate registration, task state, condition checks, and scheduling. Publication also needs matching locks or memory ordering. READ_ONCE/WRITE_ONCE constrain particular accesses but do not alone establish a complete cross-CPU publication/wait protocol. wake_up() does not deposit a persistent ticket in a notification mailbox: the woken task rechecks the condition, and multiple consumers may compete for one item.
Coordinate checking with entering the wait
A user-space condition variable expresses a similar constraint. The consumer checks under a mutex; pthread_cond_wait() atomically releases it and enters the wait, then reacquires it before returning. A loop checks the condition again. The producer updates the condition under the same mutex. Even a notification that precedes waiting need not cause a hang: a later protected check can observe the persistent condition.
Try check → publish → wait in the next figure, then switch to the protected protocol without resetting. The operation order remains the same; the mutex changes which action can complete in the vulnerable window.
Preparing the visual
Why sleep when the condition is already true?
Notification between checking and registering can leave a consumer asleep with a true condition. Protecting the condition and coordinating mutex release with waiting closes this window.
{"id":"context-wakeup","title":"Why sleep when the condition is already true?","summary":"Notification between checking and registering can leave a consumer asleep with a true condition. Protecting the condition and coordinating mutex release with waiting closes this window.","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":"Asleep","awake":"Ready to check","blocked":"The producer is blocked on the mutex. It can publish and notify after the consumer enters the wait and releases the lock.","check":"Check ready","checked":"The consumer saw ready=false. The unlocked version leaves a window; the protected version keeps the mutex so the producer cannot change the condition yet.","checkedState":"Checked: condition false","consumed":"Found data; wait finished","consumer":"Consumer","dataPublished":"Data published","done":"The consumer found the data and finished waiting. The persistent condition permits progress; a notification is not the data.","eyebrow":"03 · Lost wakeups","heading":"Why sleep when the condition is already true?","history":"Last three operations (order survives protocol changes)","initial":"Try check, publish, then wait to expose the unsafe window, or arrange another order.","intro":"Choose the order of checking, publishing, and waiting. Switching protocols replays the same operations to expose the waiting window.","locked":"Consumer holds mutex","lost":"ready is true but the consumer is asleep. The earlier notification had no waiter, and registering later does not replay it.","noMutex":"No mutex","noNotification":"No notification yet","noWaiter":"No waiter at notification","notPublished":"Not published","note":"Operations are separated so you can choose the interleaving. The protected check holds the mutex; waiting atomically releases it and registers the waiter. A blocked producer then completes its publication. A woken consumer can compete for the mutex; its next check includes reacquiring it. Spurious wakeups and multiple consumers are omitted; real code still needs a while loop.","producer":"Producer","protocol":"Waiting protocol","publish":"Publish and notify","published":"Data was published with nobody waiting. A notification is not a ticket saved for a future waiter.","registered":"Consumer in wait queue","reset":"Clear operations","safe":"One lock protects condition and wait","scope":"Model scope and assumptions","sleeping":"The consumer is registered and asleep. A later publication can find and wake it.","unlocked":"Mutex available","unregistered":"Wait queue empty","unsafe":"Check, then wait without a lock","wait":"Register and wait","waitingLock":"Waiting for mutex","wakeDelivered":"Notification woke consumer","woken":"The notification found a waiter, which is runnable again. It must reacquire the mutex and recheck the condition before continuing."}}
A 1000-round handoff using the condition
Save as handoff.c and compile with cc -std=c11 -O2 -Wall -Wextra -Werror -pthread handoff.c -o handoff. The two threads alter only their own synchronization objects, not system scheduling settings.
#define_POSIX_C_SOURCE200809L#include<assert.h>#include<pthread.h>#include<stdio.h>#include<time.h>staticpthread_mutex_t mu = PTHREAD_MUTEX_INITIALIZER;staticpthread_cond_t cv = PTHREAD_COND_INITIALIZER;staticint turn;/* 0: main may publish; 1: worker may consume */staticint consumed;enum{ ROUNDS =1000};staticvoid*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);}returnNULL;}intmain(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);}
Do not define NDEBUG for this assertion-driven example. while cannot be replaced by if: returning from a wait does not establish that the condition holds. With one peer waiter, signal suffices; multiple consumers require a fresh design for conditions, wakeup counts, and shutdown.
The output measures round-trip latency for this synchronization workload, including mutexes, condition variables, possible futex calls, scheduling waits, startup, and join. It is not pure context-switch time. Do not assume exactly two switches per round without event counts; the threads may also run on different CPUs.
6. From counts to causes
Voluntary/involuntary counts in /proc/PID/status or getrusage() can show trends. Voluntary includes blocking and does not mean the application explicitly called yield; involuntary does not imply a system fault. Compare increments with completed requests, thread counts, and CPU use over the same interval.
For wakeup latency, associate a target TID's sched_wakeup with its later selection as next in sched_switch. Handle repeated wakeups, exit and TID reuse, cross-CPU timestamps, and missing capture boundaries. Arbitrarily subtracting two same-PID records is insufficient. The result includes competition and scheduling paths, not isolated switch_to cost.
Observation
A hypothesis to investigate
More switches, unchanged throughput
A different cooperation pattern
Worse tail latency and more run-queue waiting
CPU competition, quota, placement constraints
Little queueing, slower execution after migration
Cache locality and NUMA
These are investigation paths, not conclusions from a single counter. Change thread counts, batching, or lock granularity to address measured waiting or execution costs.
Read __schedule() / context_switch() in kernel/sched/core.c, then architecture switch_to, mm/TLB, and FPU paths. For why this next task was selected, revisit the scheduler. For signal-interrupted waits and EINTR, continue to signals and ptrace.