概要
プリエンプション(Preemption)とは、実行中のタスクを強制的に中断し、より優先度の高いタスクにCPUを譲る仕組みです。プリエンプティブスケジューリング(Preemptive Scheduling)とも呼ばれます。
プリエンプションがない(ノンプリエンプティブ/協調スケジューリング)場合、実行中のタスクが自ら制御を手放すまで他のタスクは実行されません。これでは「緊急処理が割り込めない」という問題が生じ、リアルタイム性が保証できません。
プリエンプションがある場合、割り込みやTick割り込みをきっかけに高優先度タスクが即座に実行を開始できます。ハードリアルタイムシステムでは、プリエンプティブスケジューリングはほぼ必須の機能です。
プリエンプション発生時にはコンテキストスイッチ(現在タスクのレジスタ等の保存と次タスクの復元)が実行されます。
歴史・背景
プリエンプティブスケジューリングはメインフレーム時代(1960年代)から存在しますが、マイコン向けにはリソースオーバーヘッドが大きく、初期の組み込みシステムでは協調スケジューリングが使われることも多かったです。
1980年代の商用RTOS(VxWorks等)が組み込み向けに優先度ベースのプリエンプティブスケジューラを実用化しました。Windowsも3.1まで協調スケジューリングだったのに対し、Windows 95/NT 4.0からプリエンプティブスケジューリングに移行しています。
組み込みLinuxのPREEMPT_RTパッチ(2004年頃〜)はLinuxカーネル自体をほぼ全域プリエンプト可能にし、RTOSに近い割り込みレイテンシを実現しました。Linux 6.12(2024年)でメインラインにマージされています。
技術仕様
プリエンプティブ vs. 協調スケジューリング
| 観点 | プリエンプティブ | 協調(ノンプリエンプティブ) |
|---|---|---|
| タスク切り替えタイミング | 任意(割り込みタイミング) | タスク自身が制御を手放す時 |
| 高優先度タスクの応答性 | 即座(数μs〜数十μs) | 低優先度タスク完了後 |
| デッドライン保証 | 可能(優先度設計による) | 困難 |
| コンテキストスイッチ負荷 | あり(Tick毎/割り込み毎) | タスク切り替え時のみ |
| 設計の複雑さ | 高(競合状態への対処必要) | 低 |
| 向いている用途 | リアルタイム制御・組み込みRTOS | シンプルな組み込み・協調マルチタスク |
FreeRTOSでのプリエンプション設定
/* FreeRTOSConfig.h */
#define configUSE_PREEMPTION 1 /* 1: プリエンプティブ(デフォルト) */
/* 0: 協調スケジューリング */
#define configUSE_TIME_SLICING 1 /* 同優先度タスク間のラウンドロビン */
#define configTICK_RATE_HZ 1000 /* 1kHz Tick = 1ms最小タイムスライス */
プリエンプションが発生するタイミング
| トリガ | 説明 |
|---|---|
| Tick割り込み | スケジューラが定期的に優先度チェック |
| ISRからの通知 | portYIELD_FROM_ISR()でISR終了後に即切り替え |
| 高優先度タスクの待機解除 | キュー受信・セマフォ獲得・タイマ完了 |
| 明示的な譲渡 | taskYIELD()の呼び出し |
動作原理
プリエンプション発生の詳細フロー
[TaskC(低優先度)実行中]
↓
[Tick割り込み発生(1ms毎)]
↓
[スケジューラのTick処理]
→ TaskAのタイマが満了 → Ready状態へ
→ TaskA(高優先度)がReadyに存在
↓
[プリエンプション決定]
→ TaskCのコンテキストを保存(スタックへレジスタ退避)
→ TaskAのコンテキストを復元
↓
[TaskA実行開始]
↓
[TaskAがvTaskDelay等でブロック]
↓
[スケジューラが次のReadyタスクを選択]
→ TaskCのコンテキストを復元
↓
[TaskC実行再開(中断した箇所から継続)]
ISRからのプリエンプション(即時切り替え)
/* ISR内でキューに送信 → 高優先度タスクが即座に起床 */
void USART1_IRQHandler(void) {
uint8_t byte = USART1->DR;
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xQueueSendFromISR(rxQueue, &byte, &xHigherPriorityTaskWoken);
/* xHigherPriorityTaskWokenがpdTRUEなら、ISR終了直後にコンテキストスイッチ */
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
/* portYIELD_FROM_ISRがない場合、次のTick割り込みまでスイッチが遅れる */
}
portYIELD_FROM_ISR()を使うことで、Tickを待たずにISR終了直後に高優先度タスクへ切り替わります。これによりレイテンシを最小化できます。
協調スケジューリングとの比較動作
協調スケジューリング(configUSE_PREEMPTION=0):
時刻0ms: TaskC(低)実行中
時刻5ms: TaskA(高)がReady になる ← プリエンプションなし
時刻50ms: TaskCが taskYIELD()を呼ぶ → ようやくTaskAへ切り替わり
プリエンプティブスケジューリング(configUSE_PREEMPTION=1):
時刻0ms: TaskC(低)実行中
時刻5ms: TaskA(高)がReady → 即座にTaskCを中断してTaskAへ切り替わり
(レイテンシ: 数μs以内)
用途・ユースケース
プリエンプションが必須の場面
-
緊急停止処理: 安全センサが危険状態を検知した際、最高優先度のフェイルセーフタスクが即座に実行される必要があります。
-
シリアル通信の受信バッファオーバーフロー防止: UARTの受信バッファが満杯になる前に高優先度の受信処理タスクが起動される必要があります。
-
モーター制御ループの周期保証: 1ms周期の制御タスクが、他のタスクの実行時間に関わらず正確に起動される必要があります。
プリエンプションが不要な場面
- 超シンプルな組み込み機器: 1〜2タスクのみで競合が発生しない場合
- 決定論的なシーケンス制御: 固定順序で処理を実行するだけでよい場合
- 省電力優先: コンテキストスイッチのオーバーヘッドを最小化したい超低消費電力設計
実装・開発のポイント
プリエンプションと競合状態(Race Condition)
プリエンプションが有効な場合、タスクがいつでも中断されうるため、共有データへのアクセスに注意が必要です。
/* 問題のあるコード(プリエンプションで破壊される可能性) */
volatile uint32_t counter = 0;
void taskA(void *pvParam) { /* 高優先度 */
for (;;) {
counter++; /* Read-Modify-Write: アトミックでない場合がある */
vTaskDelay(1);
}
}
void taskB(void *pvParam) { /* 低優先度 */
for (;;) {
counter++; /* TaskAにプリエンプションされると値が失われる可能性 */
vTaskDelay(10);
}
}
/* 修正:ミューテックスで保護 */
MutexHandle_t counterMutex;
void taskA_fixed(void *pvParam) {
for (;;) {
xSemaphoreTake(counterMutex, portMAX_DELAY);
counter++;
xSemaphoreGive(counterMutex);
vTaskDelay(1);
}
}
プリエンプション遅延の測定
/* GPIOトグルでプリエンプションのレイテンシを計測 */
/* ISRでGPIOをHigh → TaskAの最初の行でLow → ロジアナで計測 */
void gpio_isr(void *arg) {
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET); /* ISRでHigh */
BaseType_t xHPTW = pdFALSE;
vTaskNotifyGiveFromISR(highPriorityTaskHandle, &xHPTW);
portYIELD_FROM_ISR(xHPTW);
}
void highPriorityTask(void *pvParam) {
for (;;) {
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET); /* タスク起床でLow */
/* ロジアナでHigh→Lowの時間差 = プリエンプション遅延 */
}
}
スタックオーバーフローのリスク
プリエンプションによりタスクのスタックにコンテキストが積まれるため、スタックサイズは余裕を持って設定します。FreeRTOSのスタックオーバーフロー検出フックを有効にします。
/* FreeRTOSConfig.h */
#define configCHECK_FOR_STACK_OVERFLOW 2 /* スタックオーバーフロー検出 */
/* スタックオーバーフロー時に呼ばれるフック */
void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName) {
/* ここで緊急停止・ログ記録等 */
for (;;);
}
他技術との比較
| 観点 | プリエンプティブRTOS | 協調スケジューリング | ベアメタルスーパーループ |
|---|---|---|---|
| 高優先タスクの応答性 | 最良(数μs) | タスク完了依存 | ループ1周時間依存 |
| 競合状態のリスク | 高(要ミューテックス等) | 低(自発切り替えのみ) | なし(シングルタスク) |
| デッドライン保証 | 可能 | 困難 | ループ設計次第 |
| 実装の複雑さ | 高 | 低〜中 | 最低 |
| 適したシステム規模 | 中〜大規模 | 小〜中規模 | 小規模 |
プリエンプションはリアルタイム性確保の根幹ですが、設計の複雑さも増します。セマフォ・ミューテックス・キューを正しく使い、デッドロックや優先度逆転を防ぐ設計が不可欠です。