概要
優先度逆転(Priority Inversion)は、優先度の低いタスクが共有資源を占有していることで、優先度の高いタスクが実行を妨げられる現象です。本来「高優先度タスクが低優先度タスクより先に実行される」はずのシステムにおいて、論理的に逆転した状態が生じます。
典型的な優先度逆転のシナリオ:
- 低優先度タスクL がミューテックスMを取得して資源を使用中
- 高優先度タスクH が起床し、同じミューテックスMを要求 → ブロック(LがMを解放するまで待機)
- 中優先度タスクMが実行可能になり、プリエンプションでLを中断
- LがMを走れない間、HはMの解放を待てない → HはMがLを、LはMに邪魔されるという逆転
この状態が続くと、ハードリアルタイムシステムではデッドラインを超過する危険があります。最悪の場合、優先度逆転は無期限に続く「無界の優先度逆転(Unbounded Priority Inversion)」となります。
歴史・背景
優先度逆転という問題はRTOS理論の黎明期から知られていましたが、1997年にNASAのMars Pathfinder探査機で実際の宇宙ミッションに影響を与えた事例として有名になりました。
Mars Pathfinderでは、気象情報バスのタスクが高優先度、バスの管理タスクが低優先度でセマフォを共有していました。中優先度の通信タスクが低優先度タスクをプリエンプションし続けたため、高優先度タスクのデッドラインが超過。ウォッチドッグタイマがシステムリセットを繰り返すという問題が発生しました。
この問題はNASAが事前に知っていた優先度継承プロトコルを有効化することで解決されましたが、この事例が「優先度逆転は実際の危機を引き起こす」という認識を業界に広めました。
優先度逆転への解決策として、1990年にL.ShaらがPriority Inheritance Protocol(PIP)とPriority Ceiling Protocol(PCP)を論文で発表しました。これらは現在の多くのRTOSに実装されています。
技術仕様
優先度逆転の解決策
| 手法 | 説明 | メリット | デメリット |
|---|---|---|---|
| 優先度継承(Priority Inheritance) | ミューテックス保持中の低優先タスクを、ブロック中の最高優先タスクの優先度まで一時的に引き上げる | 実装が比較的容易・多くのRTOSに実装済み | ネストしたロックで複雑化 |
| 優先度上限(Priority Ceiling) | ミューテックスに「利用可能な最高優先度」を事前設定し、取得時にその優先度へ引き上げる | デッドロックも防止・解析が容易 | 最高優先度の事前把握が必要 |
| ロックフリー設計 | ミューテックス自体を使わず、アトミック命令・キュー・ダブルバッファで設計 | 優先度逆転自体が発生しない | 設計が複雑 |
| 割り込み禁止 | クリティカルセクションで割り込みを禁止 | シンプル | 禁止中に全割り込みが止まる(リアルタイム性低下) |
FreeRTOSでのサポート状況
| 関数 | 優先度継承 | 説明 |
|---|---|---|
xSemaphoreCreateBinary() | なし | バイナリセマフォ(優先度継承なし) |
xSemaphoreCreateMutex() | あり | ミューテックス(優先度継承付き) |
xSemaphoreCreateRecursiveMutex() | あり | 再帰ミューテックス(同タスクから複数回取得可) |
FreeRTOSではxSemaphoreCreateMutex()が優先度継承を自動的に実装しています。バイナリセマフォ(xSemaphoreCreateBinary())は優先度継承がないため、タスク間の同期・通知に使い、資源の排他にはミューテックスを使うのが正しい使い方です。
動作原理
優先度逆転の発生メカニズム
タスク構成:
TaskH(優先度3・高): センサ制御
TaskM(優先度2・中): データ処理
TaskL(優先度1・低): ログ記録(SPI Flashへの書き込みにミューテックスを使用)
時系列:
t=0: TaskL実行 → ミューテックス取得 → SPI Flash書き込み開始
t=1: TaskH起床 → ミューテックス要求 → ブロック(Lがリリースするまで待機)
t=2: TaskM起床 → プリエンプションでTaskLを中断 → TaskM実行中
この間、TaskHはミューテックスを待ちながら、TaskMに実行を奪われ続ける
t=50: TaskM完了 → TaskLが再開 → ミューテックス解放
t=51: TaskH実行開始 ← 49ms遅延! デッドライン超過の可能性
優先度継承プロトコルの動作
優先度継承(Priority Inheritance Protocol)の場合:
t=0: TaskL実行 → ミューテックス取得
t=1: TaskH起床 → ミューテックス要求 → ブロック
[RTOSが優先度継承を適用:TaskLの優先度を3(TaskHと同じ)へ一時引き上げ]
t=2: TaskM起床(優先度2)→ TaskL(現在優先度3)をプリエンプションできない!
TaskLが継続実行
t=5: TaskL ミューテックス解放 → TaskLの優先度を1に戻す
TaskH がミューテックスを取得 → 実行開始(遅延最小化)
コード例:優先度継承の有無による違い
/* 問題のあるコード:バイナリセマフォ使用(優先度継承なし) */
SemaphoreHandle_t badSem = xSemaphoreCreateBinary();
xSemaphoreGive(badSem); /* 初期値1(解放済み) */
/* 優先度継承のある正しいコード:ミューテックス使用 */
SemaphoreHandle_t goodMutex = xSemaphoreCreateMutex();
void lowPriorityTask(void *pvParam) {
for (;;) {
/* ミューテックス取得(このタスクが保持中に高優先タスクが来ると、
RTOSがこのタスクの優先度を引き上げる) */
xSemaphoreTake(goodMutex, portMAX_DELAY);
write_to_flash(); /* 時間のかかる処理 */
xSemaphoreGive(goodMutex);
vTaskDelay(1000);
}
}
void highPriorityTask(void *pvParam) {
for (;;) {
/* lowPriorityTaskが保持中でも、優先度継承により最小時間で取得できる */
xSemaphoreTake(goodMutex, portMAX_DELAY);
read_flash();
xSemaphoreGive(goodMutex);
vTaskDelay(10);
}
}
優先度上限プロトコル(Priority Ceiling Protocol)
PCP(Priority Ceiling Protocol)の動作:
各ミューテックスMにCeiling(そのミューテックスにアクセスするタスクの最高優先度)を設定
例:flashMutexへアクセスするタスクの最高優先度 = 3(TaskH)
flashMutex.ceiling = 3
→ 誰かがflashMutexを取得した瞬間、その保持者の優先度が3に引き上げられる
→ 中優先度タスク(優先度2)は常にプリエンプションできない
→ デッドロックも防止できる
用途・ユースケース
優先度逆転が問題になるシステム
- 自動車制御ECU: 低優先タスクのNVMアクセス中に高優先制御ループがブロックされ、デッドライン超過するリスク
- 医療機器: 緊急アラームタスクが通常データ記録タスクに遅延させられるリスク
- 航空宇宙(Mars Pathfinderの教訓): 完全な解析と優先度継承の適用が必須
優先度逆転を防ぐ設計指針
- 資源の排他にはミューテックスを使う(バイナリセマフォではなく)
- 共有資源を保持する時間を最小化する
- タスク数と優先度の設計を事前に文書化し、WCETを含めて分析する
- ロックフリー設計(キュー・リングバッファ・アトミック変数)を検討する
実装・開発のポイント
ロックフリーで優先度逆転を回避
最もシンプルな解決策は、ミューテックス自体を使わない設計です。タスク間の通信にキュー(FIFO)やリングバッファを使うことで、優先度逆転のリスクを根本的に排除できます。
/* ロックフリーな設計例:キューによるデータ渡し */
QueueHandle_t dataQueue = xQueueCreate(10, sizeof(SensorData_t));
/* 低優先度タスク(センサ読み取り) */
void sensorTask(void *pvParam) {
SensorData_t data;
for (;;) {
data.value = readSensor();
data.timestamp = xTaskGetTickCount();
/* ミューテックス不要:キューは内部で安全に管理 */
xQueueSend(dataQueue, &data, 0);
vTaskDelay(10);
}
}
/* 高優先度タスク(データ処理) */
void processTask(void *pvParam) {
SensorData_t data;
for (;;) {
xQueueReceive(dataQueue, &data, portMAX_DELAY);
processData(&data); /* ブロッキングなし */
}
}
優先度逆転の検出
FreeRTOS標準には優先度逆転の自動検出機能はありませんが、以下の方法で手動検出できます。
/* タスクの応答時間をタイムスタンプで監視 */
void highPriorityTask(void *pvParam) {
TickType_t deadline = pdMS_TO_TICKS(10); /* 10msデッドライン */
for (;;) {
TickType_t wakeTime = xTaskGetTickCount();
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
TickType_t elapsed = xTaskGetTickCount() - wakeTime;
if (elapsed > deadline) {
/* デッドライン超過ログ → 優先度逆転の可能性 */
log_deadline_miss(elapsed);
}
processEvent();
}
}
他技術との比較
| 解決策 | 実装の難易度 | デッドライン保証 | デッドロック防止 |
|---|---|---|---|
| 優先度継承(FreeRTOS Mutex) | 低(RTOS自動) | 有界の逆転のみ防止 | なし |
| 優先度上限(PCP) | 中(天井値の事前設計) | 有界かつ最小化 | あり |
| ロックフリー設計 | 高(設計変更が必要) | 逆転なし | あり |
| 割り込み禁止 | 低 | 期間中の割り込みも停止 | あり |
優先度逆転はリアルタイムシステムの安全性を脅かす重大な問題です。Mars Pathfinderの教訓を活かし、設計段階でのリスク分析とミューテックスの適切な使用により確実に防止することが求められます。