このページの目次
コンテキストスイッチ
カバー範囲: context_switch() の完全パス → switch_mm (ページテーブル/TLB/PCID) → switch_to (レジスタ/FPU) → 遅延TLB → 切り替えオーバーヘッド分析 カーネルバージョン: 2.6 ~ 6.x。5.x以降のPCID/ASIDおよびFPU最適化を重点的に注記
概要
コンテキストスイッチはスケジューラの「実行側」です。スケジューラが「誰が実行するか」を決定し、コンテキストスイッチは「どのように切り替えるか」を担当します。毎回の切り替えにおいて、カーネルは現在実行中のプロセスのCPUレジスタ状態を保存し、次プロセスのレジスタを復元すると同時に、アドレス空間の切り替え(ページテーブル、TLB)およびFPU/SIMDコンテキストの処理を行います。
1回のコンテキストスイッチの典型的なオーバーヘッドは 1~5 μs の範囲にあり、以下に依存します:
- アドレス空間を跨ぐかどうか(プロセス切り替え vs スレッド切り替え)
- TLBのフラッシュが必要かどうか(PCID/ASIDにより削減可能)
- FPUがダーティかどうか(XSAVE領域の保存/復元が必要)
- キャッシュの局所性(直前にウェイクアップされたプロセスは、データがまだキャッシュ内にある可能性がある)
エントリポイント: context_switch()
// kernel/sched/core.c
// schedule() → __schedule() から呼び出される
static __always_inline struct rq *
重要な洞察:context_switch() は switch_to() を呼び出した後、実行主体はすでにnextになっている ことです。switch_to は(Cの視点からは)戻りません。これは、制御フローをnextプロセスが最後に切り替えられた時の switch_to の復帰ポイントに渡します。次にprevがスケジューリングされて実行に戻ってくると、switch_to の復帰地点から finish_task_switch() の実行を再開します。
switch_mm: アドレス空間の切り替え
これはコンテキストスイッチの中で最も高価な部分であり、特にアドレス空間を跨ぐ場合に顕著です。
x86-64 実装
// arch/x86/mm/tlb.c
void
PCID: TLBフラッシュの回避
// x86 PCID (Process-Context Identifier):
// 各アドレス空間に12ビットのASID (Address Space Identifier) を割り当て
// CR3への書き込み時にASIDを付与 → TLB内の他のASIDのエントリはヒットせず
// フラッシュする必要もない
//
// PCIDがない場合: CR3への書き込みごとにすべてのnon-global TLBエントリをフラッシュ → キャッシュミスが非常に多発
// PCIDがある場合: アドレス空間の切り替え → CR3 + new ASIDへの書き込みのみ → TLBは旧エントリを保持
// (旧エントリには旧ASIDが付いており、誤ヒットしない)
//
// カーネルは、CPUごとのASIDビットマップを使用してASIDを割り当ておよび回収する
遅延TLB (Lazy TLB)
// カーネルスレッド (mm == NULL) の切り替え時:
// enter_lazy_tlb(prev->active_mm, next)
// → next->active_mm = prev->active_mm
// → CR3への書き込みを行わない (prevのページテーブルを引き続き使用)
// → カーネルアドレス空間は共有されているため、切り替え不要
//
// これにより、カーネルスレッドの切り替えが非常に高速になる → TLBフラッシュのオーバーヘッドがない
ARM64 の同等実装
// ARM64 は PCID の代わりに ASID (8/16ビット) を使用
// TTBR0_EL1: ユーザー空間ページテーブル (プロセスごとに異なる)
// TTBR1_EL1: カーネル空間ページテーブル (全プロセスで共有)
//
// switch_mm(): TTBR0_EL1 + ASID を書き込み
// → TLBフラッシュ用の追加命令は不要 (ASIDでハードウェアが区別)
//
// VMID (Virtual Machine ID):
// 仮想化拡張機能。ハイパーバイザによって管理
// stage-2 ページテーブルのTLBタグとして使用
switch_to: レジスタコンテキスト
// arch/x86/entry/entry_64.S
// switch_to() はマクロであり、以下に展開される:
//
// 1. prevの汎用レジスタをカーネルスタックに保存
// 2. prevのRIP/RSPをtask_struct.threadに保存
// 3. nextのRIP/RSPを復元
// 4. nextの汎用レジスタを復元
// 5. nextのRIPにジャンプ
thread_struct — 保存されるハードウェアコンテキスト
// arch/x86/include/asm/processor.h
;
FPUの遅延切り替え
// arch/x86/kernel/fpu/core.c
// 最新のx86 (XSAVE/XSAVES) におけるFPUの切り替え戦略:
// 1. 各プロセスは独立したfpstate (XSAVE領域、AMXの場合最大約10KB) を持つ
// 2. 切り替え時に「遅延」処理を行う:
// - switch_to() 内でFPU状態の自動保存/復元は行わない
// - TS (Task Switched) ビットまたはXFDビットを設定
// - 新プロセスが初めてFPUを使用 → #NM例外発生 → その時点でカーネルがXRSTORを実行
//
// 3. なぜか? XSAVE領域は大きい (AMXの場合数KB)。多くのプロセスはFPUを使用しない
// 遅延保存により、意味のないデータ転送を回避できる
// 遅延保存の例外:
// - 次プロセスがFPUを明示的に必要とする場合 (__fpu_state_activeチェックで確認)
// - かつFPUがダーティである場合 → switch_to内でXSAVE/XSAVESを必ず実行する必要がある
アーキテクチャ間の差異
| x86-64 | ARM64 | RISC-V | |
|---|---|---|---|
| 切り替え関数 | __switch_to_asm() (アセンブリ) | cpu_switch_to() (アセンブリ) | __switch_to() (アセンブリ) |
| FPU | XSAVE/XRSTOR (~10KB) | FPSIMD/SVE (遅延) | FPU (遅延) |
| アドレス空間 | CR3書き込み + PCID | TTBR0書き込み + ASID | SATP書き込み + ASID |
| バリア | membar 明示的 | TTBR書き込み後に isb | sfence.vma |
TLB Shootdown
グローバルなページテーブルエントリを変更した場合(例: munmap)、他の全CPUにTLBフラッシュを通知する必要があります:
// kernel/smp.c → arch/x86/mm/tlb.c
void
マルチコアでのTLB shootdownはオーバーヘッドが大きい(IPI + 待機)ため、カーネルは複数の最適化を行っています:
- 遅延TLBモード: 対象CPUがカーネルスレッド上で実行中(ユーザー空間を実行していない)の場合、フラッシュを遅延できる
- バッチ処理: 複数のTLB操作を1つのIPIに結合
- セルフスヌープ: 一部のハードウェアではページテーブル変更を自動検知でき、ソフトウェアIPIが不要
切り替えオーバーヘッド分析
典型的な数値 (x86-64, ~3GHz):
| シナリオ | レイテンシ | 理由 |
|---|---|---|
| スレッド切り替え (同一プロセス) | ~0.5-1 μs | CR3書き込みなし、FPUは通常遅延 |
| プロセス切り替え (PCIDあり) | ~1-3 μs | CR3書き込みあり、ただしTLBは保持 |
| プロセス切り替え (PCIDなし) | ~3-5 μs | CR3書き込み + 全non-global TLBの冷間起動 |
| NUMA間 | +1-3 μs | 新CPUのキャッシュが冷えているため、リモートからのデータ取得が必要 |
測定方法
# 1. perf: レイテンシヒストグラム
# 2. bpftrace: schedule() レイテンシを直接測定
# 3. /proc/<pid>/sched
|
# nr_voluntary_switches: プロセスが自発的に譲渡 (block/sleep/yield)
# nr_involuntary_switches: タイムスライス終了によりプリエンプト
# involuntaryが高い → CPU集約型;voluntaryが高い → IO集約型
参考と拡張
- カーネルドキュメント:
Documentation/x86/tlb.rst,Documentation/arch/x86/topology.rst - LWN:
- "PCID and the TLB" (lwn.net/Articles/729754/)
- "Lazy FPU state restoration" (lwn.net/Articles/675957/)
- ソースファイル:
kernel/sched/core.c— context_switch()arch/x86/mm/tlb.c— switch_mm_irqs_off()arch/x86/kernel/process_64.c— __switch_to()arch/x86/entry/entry_64.S— アセンブリレベルのswitch_toarch/x86/kernel/fpu/core.c— FPUの遅延切り替え
キーワード: context_switch, switch_mm, switch_to, PCID, ASID, TLB shootdown, lazy TLB, FPU lazy restore, XSAVE