このページの目次

Linux コンテキストスイッチ

スケジューラは「次に誰を実行するか」を決定し、コンテキストスイッチはCPUをprevからnextへ引き渡す役割を担います。これは単なる汎用レジスタの保存だけでなく、以下のような処理も含まれる可能性があります。

  • ユーザーアドレス空間とCR3/PCID
  • カーネルスタックとcallee-savedレジスタ
  • FPU/SIMD/拡張状態
  • perf、ptrace、I/Oビットマップ、TLSなどのアーキテクチャ状態
  • RCU、membarrier、rseq、スケジューラロックの後処理

「コンテキストスイッチの所要時間」「タスクがウェイクアップしてから実行されるまでのレイテンシ」「キャッシュ/TLBの冷却によるその後のパフォーマンス低下」は、3つの異なる指標です。これらを固定のマイクロ秒数で一概に概括することはできません。

1. 呼び出しパス

典型的なパスは以下の通りです。

schedule()
  └─ __schedule()
       ├─ pick_next_task()
       └─ context_switch(rq, prev, next, rf)
            ├─ prepare_task_switch()
            ├─ mmの切り替えまたは借用
            ├─ prepare_lock_switch()
            ├─ switch_to(prev, next, prev)
            └─ finish_task_switch(prev)

context_switch()kernel/sched/core.c で定義されています。以下のコードは制御フローのみを残したものであり、⁠任意のカーネルバージョンのソースコードと逐一行対応するものではなく、コメント付きの簡略化版です⁠:

prepare_task_switch(rq, prev, next);
arch_start_context_switch(prev);

if (!next->mm) {                 // カーネルスレッドへ切り替え
    enter_lazy_tlb(prev->active_mm, next);
    next->active_mm = prev->active_mm;
    // prevがユーザータスクかどうかに応じてactive_mmの参照を維持
} else {                         // ユーザータスクへ切り替え
    membarrier_switch_mm(rq, prev->active_mm, next->mm);
    switch_mm_irqs_off(prev->active_mm, next->mm, next);
    // カーネルスレッドからユーザータスクへ切り替える際、借用したactive_mmの解放を遅延
}

prepare_lock_switch(rq, next, rf);
switch_to(prev, next, prev);
return finish_task_switch(prev);

mmの参照管理や同期ステップは変更される可能性があるため、常にターゲットバージョンの現在の context_switch() ソースコードと照らし合わせるべきです。

2. mmactive_mm

ユーザータスクは mm を持ちます。純粋なカーネルスレッドはユーザーアドレス空間を持たないため mm == NULL です。しかしCPUは依然としてページテーブルのセットをロードする必要があり、そのためカーネルスレッドは直前のユーザータスクの active_mm を一時的に借用します。

prev → nextアドレス空間の動作
ユーザー → ユーザーswitch_mm_irqs_off() を呼び出し、必要に応じてCR3/ASIDを切り替え
ユーザー → カーネルスレッドレイジーTLBに入り、カーネルスレッドが prev->active_mm を借用
カーネルスレッド → カーネルスレッドレイジーTLBを維持し、借用した active_mm を移行
カーネルスレッド → ユーザーnext->mm へ切り替え、その後古い借用した active_mm を解放

「カーネルスレッドがユーザーページテーブルを借用する」からといって、任意のユーザーアドレスにアクセスできるわけではありません。カーネルモードでの実行は、依然としてページテーブル、KPTI、SMEP/SMAP、明示的なユーザーアクセスAPIなどの制約を受けます。

3. switch_mm_irqs_off()、CR3、PCID

x86-64において、アドレス空間の切り替えは最終的にCR3とTLBの状態を中心に展開されます。PCIDはTLBエントリにアドレス空間のタグを付与し、CR3の切り替え時にグローバルエントリ以外を無条件に破棄する必要をなくします。

しかし、「PCIDがあればフラッシュは永遠に不要」というわけではありません。以下の状況では依然として無効化が必要になる場合があります。

  • ASID/PCID が回収され、再割り当てされる
  • ページテーブルの内容が変更される
  • KPTI においてユーザー/カーネルのCR3ペアを維持する必要がある
  • mm のTLB生成番号が、現在のCPUで遅れていることを示している
  • セキュリティ修正やアーキテクチャの制約により、より強力な無効化操作が要求される

Linuxは、各CPUの cpu_tlbstate において、現在のmm、ASID、TLB生成番号を追跡しています。具体的な実装は arch/x86/mm/tlb.c にあります。

4. switch_to() が実際に保存するもの

x86-64の switch_to マクロは __switch_to_asm(prev, next) を呼び出します。アセンブリのエントリポイントでは主に以下の3つのことが行われます。

  1. rbprbxr12r15 などのcallee-savedレジスタを prev のカーネルスタックにプッシュする。
  2. 現在の rspprev->thread.sp に書き込み、次に next->thread.sp をロードする。
  3. next のカーネルスタックからレジスタをポップし、アーキテクチャ状態の切り替えを完了するためにC関数 __switch_to() へジャンプする。
# arch/x86/entry/entry_64.S(簡略化)
pushq %rbp
pushq %rbx
pushq %r12
pushq %r13
pushq %r14
pushq %r15

movq %rsp, TASK_threadsp(%rdi)
movq TASK_threadsp(%rsi), %rsp

popq %r15
popq %r14
popq %r13
popq %r12
popq %rbx
popq %rbp
jmp __switch_to

汎用レジスタがすべて thread_struct 配列にコピーされるわけではありません。callee-savedの状態は主にカーネルスタック内の inactive_task_frame に保存され、thread.sp がそのフレームを見つけるためのキーとなります。

「switch_to は誰に返るか」

switch_to(prev, next, prev) の実行後、CPUはすでに next のカーネルスタックとタスクコンテキストを使用しています。コードは、next が最後に切り出された際に残された呼び出しチェーンから実行を再開します。

したがって、より正確には、「switch_to返るが、その返却は切り替え先のタスクのコンテキスト内で発生する⁠」と言えます。3番目のパラメータは、finish_task_switch() によるクリーンアップのために、実際に切り出されたタスクを受け取ります。「Cの視点からは返らない」と単純に言うのは、この二重の返却セマンティクスを隠蔽してしまいます。

5. 現代のx86 FPU状態の切り替え

現代のLinuxでは、通常のFPUコンテキストを「毎回TSフラグを設定し、新しいタスクが最初に浮動小数点命令を実行した際に #NM 例外をトリガーして旧状態を保存する」という古典的なレイジーFPUモデルで記述することはありません。

現在のスケジューリングパスにおける switch_fpu() は、必要に応じて以下を行います。

  1. 旧タスクのFPUレジスタをfpstateに保存する
  2. TIF_NEED_FPU_LOAD を設定する
  3. ユーザーモードへの復帰時、またはカーネルがその状態を必要とする前にレジスタを復元する

タスクがレジスタ状態を保持したままの同じCPUに戻った場合、カーネルは不要な復元を回避できます。XFD/#NM は依然としてAMXなどの動的拡張状態をオンデマンドで有効化するために使用できますが、これをすべてのFPU/SIMD状態のスケジリング戦略として一般化することはできません。

現在の switch_fpu()x86 FPU ドキュメント を参照してください。

6. TLBシュートダウンは通常のコンテキストスイッチの固定ステップではない

munmap()、権限の変更、またはページテーブルの解放などによりある mm のページテーブルが変更されると、その mm を現在実行中、または以前実行していた他のCPUは、古いTLBエントリをキャッシュしたままになっている可能性があります。カーネルは関連するCPUに無効化操作を実行させ、必要に応じてIPIを介して調整します。

ページテーブルの変更
  ├─ 現在のCPUで対応するエントリ/範囲を無効化
  └─ 古いTLB状態を保持しているリモートCPUへシュートダウンを発行
       └─ 必要な完了確認を待機

これは「スケジューラがCPUをAからBへ切り替える」と関連していますが、等価ではありません。通常のコンテキストスイッチでは既存のASIDをロードするだけの場合もありますが、ページテーブルの更新のみでタスクの切り替えなしにシュートダウンをトリガーすることもあります。

7. 測定方法

まず、何を測定したいかを明確にします。

7.1 スイッチ数の統計

perf stat -e context-switches,cpu-migrations,page-faults -- ./workload

grep -E 'voluntary_ctxt_switches|nonvoluntary_ctxt_switches' \
  /proc/<pid>/status

数値が高いことが必ずしもパフォーマンスの低下を意味するわけではありません。I/O指向のプログラムは多数の自発的な切り替えを生成します。問題は、レイテンシの目標、CPU利用率、キャッシュへの影響に依存します。

7.2 スケジューリングイベントの観察

perf sched record -- ./workload
perf sched timehist
perf sched latency

perf sched latency は、単一の switch_to アセンブリ命令のサイクル数ではなく、タスクがスケジューリングを待機するレイテンシに近い値を計測します。

7.3 wakeup-to-run レイテンシの測定

bpftrace -e '
tracepoint:sched:sched_wakeup {
    @wake[args->pid] = nsecs;
}
tracepoint:sched:sched_switch /@wake[args->next_pid]/ {
    @wake_to_run_us = hist((nsecs - @wake[args->next_pid]) / 1000);
    delete(@wake[args->next_pid]);
}'

このヒストグラムは、「タスクがウェイクアップされて実際にCPUを獲得するまでの時間」を測定するもので、run queueでの待機時間も含まれます。純粋なコンテキストスイッチコストとして表示すべきではありません。

7.4 マイクロベンチマーク

perf bench sched pipe -l 100000

これは、パイプを介して2つのタスクが往復する際の総合コストを測定するもので、システムコール、ウェイクアップ、スケジューリング、切り替えが含まれます。結果を報告する際は、少なくとも以下の事項を明記してください:CPU、カーネルバージョン、mitigation(緩和策)、CPUアフィニティ、周波数ポリシー、NUMA跨ぎかどうか、スレッドかプロセスか。

8. 一般的な誤解

  • 「1回のコンテキストスイッチは固定で1〜5μs」:測定定義やハードウェアが異なるため、直接比較はできません。
  • 「スレッドの切り替えではアドレス空間に触れない」:同じ mm では通常CR3の切り替えを回避しますが、スケジューリング、スタック、レジスタ、およびFPUのコストは依然として存在します。
  • 「PCIDはTLBフラッシュを排除する」:無条件フラッシュを減らしますが、ページテーブルの無効化やASIDの回収を排除するわけではありません。
  • 「すべてのFPU状態は初回使用時まで待機して復元する」:これは時代遅れの一般化です。現代の実装では、eager saveとユーザー復帰時のrestoreを使用し、動的拡張状態は別途処理します。
  • 「TLBシュートダウンはコンテキストスイッチである」:前者はページテーブルの一貫性メカニズムであり、後者はタスクの実行権の移行です。
  • finish_task_switch のトレースポイント間の間隔が切り替え所要時間である」:これには通常、タスクの実行時間やキュー待ち時間が混入しているため、測定範囲を事前に定義する必要があります。

9. 上位ソース

カーネルの文章を読む際、最も信頼性の高い習慣は、「安定した概念」、「特定バージョンの実装」、「測定結果」をそれぞれ明確に区別することです。コンテキストスイッチ自体は長年存在していますが、その周辺のセキュリティ強化、FPUポリシー、TLB管理、スケジューラの後処理は静止していません。