リアルタイム制御

コンテキストスイッチ

実行するタスクを切り替える処理。

概要

コンテキストスイッチ(Context Switch)は、CPUが実行中のタスク(スレッド)を別のタスクへ切り替える処理です。「タスクスイッチ」とも呼ばれます。

「コンテキスト(Context)」とは、あるタスクがCPUを使っていた状態(CPUレジスタ群の値・プログラムカウンタ・スタックポインタ)の総体です。コンテキストスイッチでは:

  1. 現在実行中のタスクのコンテキストをそのタスクのスタックまたはTCB(Task Control Block)へ保存
  2. 次に実行するタスクのコンテキストを復元
  3. 復元先タスクが中断した箇所から実行を再開

コンテキストスイッチはスケジューラが主導し、プリエンプション・タイムスライス・タスクのブロッキング等をトリガとして実行されます。コンテキストスイッチの所要時間(数μs〜数十μs)はシステムのレイテンシに直接影響するため、RTOSの重要な性能指標のひとつです。

歴史・背景

コンテキストスイッチの概念はタイムシェアリングシステム(1960年代)で誕生しました。複数のユーザが1台のコンピュータを共有するために、CPUを高速に切り替える仕組みが必要になったのが始まりです。

初期のマイコン向けRTOS(1980年代のVxWorks等)でも、コンテキストスイッチのコストを最小化することが重要課題でした。最初期の実装はソフトウェアのみで行われていましたが、ARM Cortex-Mアーキテクチャ(2004年〜)ではハードウェアが一部のレジスタ退避(R0-R3, R12, LR, PC, xPSR)を自動化し、コンテキストスイッチの高速化に貢献しています。

現代のRTOS(FreeRTOS, Zephyr等)では、コンテキストスイッチを数μs(Cortex-M4, 168MHzで約1〜3μs)で完了させるよう最適化されています。

技術仕様

コンテキストスイッチで保存・復元される情報

ARM Cortex-M(FreeRTOS使用時)の場合:

保存対象保存方法内容
R0, R1, R2, R3, R12, LR, PC, xPSRハードウェア自動(例外スタッキング)汎用レジスタ・復帰アドレス等
R4〜R11ソフトウェア(FreeRTOSのポートコード)カリー保存レジスタ
S0〜S15, FPSCR(FPU有効時)ハードウェア自動(遅延スタッキング)浮動小数点レジスタ
S16〜S31(FPU有効時)ソフトウェア追加浮動小数点レジスタ
スタックポインタ(PSP)TCBへ保存タスクスタックの先頭アドレス

FPUを使用するタスクがある場合、保存レジスタが増えるためコンテキストスイッチコストが増大します。FreeRTOSではconfigUSE_TASK_FPU_SUPPORTで制御できます。

コンテキストスイッチのコスト実測値

プロセッサクロックコンテキストスイッチ時間
ARM Cortex-M0(FreeRTOS)48MHz約5〜8μs
ARM Cortex-M4(FreeRTOS)168MHz約1〜3μs
ARM Cortex-M4F(FPU付き)168MHz約2〜5μs(FPU遅延スタッキング含む)
ARM Cortex-M7(FreeRTOS)400MHz約0.5〜2μs
Xtensa LX7(ESP32-S3)240MHz約3〜5μs

TCB(タスクコントロールブロック)

各タスクの管理情報はTCB(Task Control Block)に格納されます。FreeRTOSのTCBには以下が含まれます。

/* FreeRTOSのTCB(簡略版) */
typedef struct tskTaskControlBlock {
    volatile StackType_t *pxTopOfStack; /* スタックポインタ(コンテキストスイッチで使用) */
    ListItem_t xStateListItem;          /* Readyリスト等のリンクリストノード */
    ListItem_t xEventListItem;          /* イベント待ちリスト用ノード */
    UBaseType_t uxPriority;             /* タスク優先度 */
    StackType_t *pxStack;               /* スタック先頭アドレス */
    char pcTaskName[configMAX_TASK_NAME_LEN]; /* タスク名 */
    /* ... FPUコンテキスト、スタティスティクス等 ... */
} TCB_t;

動作原理

コンテキストスイッチの詳細手順(ARM Cortex-M + FreeRTOS)

[タスクAが実行中]

[PendSV例外(コンテキストスイッチ用割り込み)が発生]
  ← Tick割り込み内でスケジューラが次タスクを決定しPendSVをPend

[ハードウェアが自動でスタックへ退避]
  スタック: {xPSR, PC, LR, R12, R3, R2, R1, R0}

[PendSVハンドラ(FreeRTOS port.c)実行]

  [TaskAのR4〜R11をスタックへPUSH命令で退避]

  [TaskAのスタックポインタ(PSP)をTCBへ保存]

  [スケジューラがTaskBのTCBを選択]

  [TaskBのスタックポインタをTCBから復元]

  [TaskBのR4〜R11をスタックからPOP命令で復元]

  [PendSVハンドラからリターン(BX LR)]

[ハードウェアがスタックから自動復元]
  スタック: {xPSR, PC, LR, R12, R3, R2, R1, R0} ← TaskBの値

[TaskBが中断していた箇所から実行再開]

FreeRTOSのPendSV使用理由

ARM Cortex-MでFreeRTOSがコンテキストスイッチにPendSV(Pendable Service Call)を使う理由:

  • PendSVを最低優先度の例外として設定することで、他のすべての割り込みハンドラが完了してからコンテキストスイッチが行われる
  • 割り込みハンドラ内でスイッチが起きることを防ぎ、安全性を確保
; FreeRTOSのxPortPendSVHandler(ARM Cortex-M、GCC向け、抜粋)
xPortPendSVHandler:
    mrs r0, psp                    ; プロセススタックポインタを取得
    ldr r3, pxCurrentTCBConst      ; 現在のTCBへのポインタを取得
    ldr r2, [r3]

    stmdb r0!, {r4-r11}            ; R4-R11をタスクスタックへ保存

    str r0, [r2]                   ; スタックポインタをTCBへ保存

    bl vTaskSwitchContext           ; 次のタスクを選択

    ldr r1, [r3]                   ; 新タスクのTCBを取得
    ldr r0, [r1]                   ; 新タスクのスタックポインタを取得

    ldmia r0!, {r4-r11}            ; 新タスクのR4-R11を復元

    msr psp, r0                    ; PSPを更新
    bx lr                          ; リターン(ハードウェアが残りを復元)

用途・ユースケース

コンテキストスイッチの頻度と影響

コンテキストスイッチの頻度はTick周波数とタスク数で決まります。

コンテキストスイッチオーバーヘッド率 =
  (コンテキストスイッチ時間) × (1秒あたりのスイッチ回数) / 1秒

例:
  スイッチ時間 = 2μs(Cortex-M4 168MHz)
  Tick = 1kHz、4タスクがラウンドロビン
  スイッチ回数 ≈ 4000回/秒
  オーバーヘッド = 2μs × 4000 = 8000μs/秒 = 0.8%

通常の1kHz Tick・数タスク構成では、コンテキストスイッチオーバーヘッドは1%未満に収まります。

FPUコンテキストの遅延保存(Lazy Stacking)

Cortex-MのFPUを使う場合、FPUレジスタの保存を「実際に必要になった時だけ」行う遅延スタッキング(Lazy Stacking)が有効です。すべてのタスクでFPUを使わない場合に効率化できます。

/* FreeRTOSでFPUを個別のタスクで有効化 */
/* portTASK_USES_FLOATING_POINT()を呼んだタスクのみFPUコンテキストを完全保存 */
void fpuTask(void *pvParam) {
    portTASK_USES_FLOATING_POINT(); /* このタスクがFPUを使うことを宣言 */
    float x = 1.0f;
    for (;;) {
        x = sqrtf(x + 1.0f);
        vTaskDelay(1);
    }
}

実装・開発のポイント

コンテキストスイッチの測定

/* GPIOを使ったコンテキストスイッチ時間の計測 */
/* TaskAでGPIO High → スリープ → TaskBでGPIO Low → ロジアナで計測 */

void taskA(void *pvParam) {
    for (;;) {
        HAL_GPIO_WritePin(GPIOC, GPIO_PIN_0, GPIO_PIN_SET);
        vTaskDelay(1); /* ← コンテキストスイッチが発生 */
    }
}

void taskB(void *pvParam) {  /* TaskAと同じ優先度、または高優先度 */
    for (;;) {
        HAL_GPIO_WritePin(GPIOC, GPIO_PIN_0, GPIO_PIN_RESET);
        vTaskDelay(1);
    }
}
/* ロジアナでGPIO波形を観測し、High→Lowの遅延がコンテキストスイッチ時間 */

コンテキストスイッチの最小化

過度なコンテキストスイッチはシステム効率を下げます。最適化のポイント:

  1. タスク数を適切に絞る: 機能をまとめられるタスクは統合
  2. Tick周波数の調整: 最大応答時間がms単位で許容できる場合、Tick周波数を下げる(例:100Hz)
  3. Tickレスモード(Tickless Idle): アイドル時はTick割り込みを止めて消費電力を削減
/* Tickless Idleの有効化(低消費電力向け) */
/* FreeRTOSConfig.h */
#define configUSE_TICKLESS_IDLE  1
/* アイドルタスクがスリープ時間を計算し、ハードウェアスリープモードへ */

スタックサイズとコンテキストスイッチ

コンテキストスイッチ時にスタックへ保存されるレジスタ分のスタック容量が必要です。最低限として(Cortex-M4、FPU無効):

  • ハードウェア自動退避: 8レジスタ × 4バイト = 32バイト
  • ソフトウェア退避(R4-R11): 8レジスタ × 4バイト = 32バイト
  • 合計: 最低64バイト(タスクスタックとは別に確保される)

他技術との比較

観点RTOSコンテキストスイッチLinuxコンテキストスイッチコルーチン切り替え
所要時間1〜10μs10〜100μs数ns〜数百ns
保存対象CPUレジスタ(ページテーブル不要)CPUレジスタ+ページテーブル最小限(スタックポインタ等)
MMU切り替えなし(同一アドレス空間)あり(プロセス間で必要)なし

関連用語

参考リンク