パイプラインでの read() 呼び出し1回で、コンテキストスイッチが連鎖します。タスクAがデータ待ちをしている間、CPUはタスクBの実行に移ります。Bがデータを書き込んでAをウェイクアップしますが、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を選択して初めて、Aは自身の待機パスから再開し、条件を再チェックしてデータを取り出し、ユーザー空間に戻ります。
1回のread:データの待機終了後も、CPUの待機が続く可能性がある
スリープ:データの待機
実行可能:CPUの待機
実行中:readの継続
待機状態へ
ウェイクアップ
再実行
リターン
データ準備完了 ≠ CPU取得済み
時間の幅は概略であり、実測の比率を表すものではありません。キュー待ち時間と低レベルの切り替えコストは、それぞれ別々に計測する必要があります。
図中の2つの待機区間は、それぞれ別々に診断する必要があります。
A がどの区間で停止しているか 不足しているもの 優先して確認すべき点
データがまだ届いていない 読み込み条件が満たされていない プロデューサ、I/O、ビジネスロジックの依存関係
ウェイクアップ済みだが未実行 CPUの実行機会 競合、クォータ、アフィニティ、プリエンプションレイテンシ
read() の実行を再開した 読み込みとリターンの完了処理中 実際の処理負荷と関連するメモリアクセスコスト
優先度を上げても、まだ生成されていないデータを先に出現させることはできません。まず適切な区間を特定して初めて、スケジューリングの最適化が有効かどうかを判断できます。
ハードウェア割り込みが発生しても、自動的にタスクの切り替えが行われるわけではありません。割り込みはAを中断できますが、処理後は再びAに戻ります。関連するエグジットパスや他のスケジューリングポイントで別のタスクの実行が決定されて初めて、タスクの引き継ぎが発生します。カーネルへの入り込み回数、システムコール回数、コンテキストスイッチ回数は、互いに代替できません。
2. context_switch:アドレス空間と実行フローは別軸
next が選択された後、kernel/sched/core.c を辿りながら、引き継ぎを3つの論理フェーズに分けることができます。
引き継ぎ前の準備。 スケジューリングの会計処理を調整し、新旧タスクの mm(メモリ記述子)の関係を処理します。
低レベルの切り替え。 アーキテクチャ固有の switch_to を経由してスタックを切り替え、必要なレジスタ状態を処理します。
引き継ぎ後の後処理。 新しいタスクのコンテキスト内で、直前に切り替えられたタスクの後処理を完了します。
ここで重要なのは、「後処理が新しいタスクのコンテキストで発生する」という点です。第3節では、2つのカーネルスタックを辿りながら、なぜこのように配置する必要があるかを説明します。
アドレス空間の切り替えは、prev と next の関係に依存します。
シナリオ mm の関係 依然として処理が必要な実行コンテキスト
同じプロセス内の2つの通常スレッド 通常、ユーザー空間の mm を共有 各々のカーネルスタック、レジスタ、TLS、FPU状態など
2つの独立したプロセス 通常、異なる 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のスタック上に残ります。低レベルの切り替えは、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 を比較すると、アドレス空間を共有していても、2つのカーネルスタックがマージされるわけではありません。
図を準備しています 再読み込み
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":"2つのタスクはどちらも実行済みで、現在実行可能です。カーネルの再開点のみを描画し、エントリフレーム、TLS、FPU、ロックプロトコルは省略します。ローカル値は帰属の例示であり、Linuxの特定のフィールドではありません。切り替えシナリオは関係の比較に使用され、実行時にスレッドの共有属性を変更するものではありません。","relation":"タスクの関係","reset":"リセット","resumed":"この呼び出しチェーンを再開","scope":"モデルの範囲と規約","separate":"2つの独立したプロセス","separateNote":"タスクを切り替える際、SP と active_mm は新しいタスクに従います。直前に切り替えられたタスクは自身のスタック上に残り、ローカル値は別のタスクの実行によって上書きされません。","shared":"同じプロセス内の2つのスレッド","sharedNote":"2つのタスクが同じ M1 を参照していますが、SP は SP_A と SP_B の間で切り替える必要があります。CPUは選択されたタスク自身のスタックとローカル値を引き続き使用します。","switch":"このタスクに切り替え","switchCount":"タスク切り替え回数","work":"現在のタスクのローカル操作を実行"}}
2種類のコンテキスト保存を、1つのレジスタ表に混同しない
保存場所 主にどの再開をサポートするか
システムコール/例外エントリフレーム(適用される 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 を共有する2つのスレッドでも、スレッドローカルストレージは異なります。x86-64 の FS/GS 関連状態などは現在のタスクと一致する必要があり、そうでなければスレッドローカル変数にアクセスする同じ命令が、他人のデータを読み込んでしまう可能性があります。perf、デバッグレジスタ、I/O ビットマップなどの状態に追加の操作が必要かどうかは、タスクがどの機能を有効にしているかによって異なります。
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 のページテーブルを変更し、2つの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":"初期状態で2つのCPUはともに M1 の P1 をキャッシュしています。まず CPU 0 を M2 に切り替えてアクセスし、その後 M1 に戻り、有効なエントリが再利用できるかを観察します。","intro":"CPU 0 は M1 / M2 を切り替えることができ、CPU 1 は M1 の使用を維持します。同じ仮想アドレスにアクセスするか、M1 のマッピングを直接変更して、キャッシュの保持と無効化を比較します。","invalid":"無効化済み","invalidateCount":"M1 マッピング更新回数","lastAccess":"前回のアクセス結果","miss":"ミス、ページテーブル参照","note":"2つのアドレス空間、1つの仮想アドレス、非グローバルエントリのみを描画し、KPTI、PCID の回収などの詳細は省略します。マッピング更新は論理的な無効化を示しており、実際の Linux では世代と遅延プロトコルによって完了します。これは、すべての更新がすべてのCPUに対して即座にIPIを送信することを意味するものではありません。","pcid":"PCID を有効にして有効なキャッシュエントリを区別","pcidNote":"キャッシュモードの切り替えによりモデルキャッシュがクリアされました。再アクセスすることで現在のマッピングが埋められ、異なる mm の切り替えを比較できます。","remap":"M1 のマッピングを変更し、旧エントリを無効化","remapNote":"M1 のページテーブルが変更されたため、2つの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":"直近3回の操作(プロトコル切り替えで順序保持)","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ラウンド交互実行、条件チェックが根拠である
以下では、2つのスレッドが1000ラウンド交互に実行するコードを示し、条件と待機の連携を検証します。handoff.c として保存し、cc -std=c11 -O2 -Wall -Wextra -Werror -pthread handoff.c -o handoff でコンパイルします。2つのスレッドは同期オブジェクトのみを変更し、システムスケジューリング設定は変更しません。
#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 に変更できません。待機リターンが条件の満たされ方を保証するわけではないため、条件こそが実行継続の根拠です。ここでは対向待機者が1人だけなので、signal で十分です。複数コンシューマ設計では、条件の分割、ウェイクアップ数、エグジットプロトコルを再考する必要があります。
出力はこの同期ワークロードの往復時間であり、ミューテックスロック、条件変数、可能な futex システムコール、スケジューリング待機、スレッドの起動と回収を含みます。純粋なコンテキストスイッチの消費時間ではありません。各ラウンドで待機が発生したとしても、イベント統計なしで各ラウンドを固定して2回の切り替えに換算することはできません。2つのスレッドが異なるCPU上で実行される可能性もあります。
6. 数値から原因へ:どの区間を計測すべきか
/proc/PID/status または getrusage() の自発的/非自発的切り替えカウントは、変化の方向性を判断するのに役立ちますが、「自発的」がビジネスコードの明示的な yield 呼び出しを意味するわけではなく、ブロック待機もその中に含まれます。「非自発的」がシステム障害を意味するわけでもありません。増分を、完了したリクエスト数、スレッド数、CPU使用率と同じウィンドウで比較する必要があります。
ウェイクアップレイテンシを研究する場合、スケジューラ tracepoint と関連付けることができます。ターゲット TID の sched_wakeup 時間を記録し、次にそれが next となる sched_switch を見つけます。重複ウェイクアップ、タスク終了と TID の再利用、CPU間タイムスタンプ、収集ウィンドウの欠如を処理する必要があり、同じ PID のログを単純に2つ引き算することはできません。得られるのは、ウェイクアップから CPU を取得するまでの待機であり、競合と関連するスケジューリングパスが含まれており、低レベルの switch_to コストは分離されていません。
指標の変化を候補の原因と対応付け、次のステップを決定します。
観察結果 継続して検証する価値のある方向
切り替え回数が増加したが、スループットはほぼ不変 スレッドの協調方法が変更されたか
尾部レイテンシが上昇し、実行キューの待機も増加 CPU競合、クォータ、配置制約
キューイングは顕著でないが、マイグレーション後に実行が遅延 キャッシュ局所性と NUMA
これらはトラブルシューティングの方向であり、単一のカウントだけで因果関係を結論付けることはできません。スレッド数の削減、バッチ調整、ロック粒度の変更は、確認された待機または実行コストに対して行われるべきです。
ソースコードを読む場合は、kernel/sched/core.c の __schedule() / context_switch() から入り、アーキテクチャ固有の switch_to、mm/TLB、FPU パスを確認します。スケジューラがなぜこの next を選択したかは、スケジューラ を参照してください。シグナルがどのように待機を終了させ、なぜ EINTR を返すことがあるかは、シグナルと ptrace を参照してください。