Chapter 04
タスクの状態機械 — Running / Ready / Blocked / Suspended
この章がなぜ必要なのか——OS のロジックの 9 割はこの図の上にある.
スケジューラが実際にやっていることは、突き詰めるとこうである。
「タスクを、あるリストから別のリストへ動かす。そして走らせるタスクを 1 つ選ぶ。」
状態とは、そのタスクがどのリストに入っているかのことである。 ここを掴むと、以降の章は「どういう理由でリストを移動するか」の各論になる。
この章で使う既出の用語(定義は各リンク先). コンテキスト(01 章 6 節)、TCB(02 章 2 節)、スタック(02 章 2 節)、タスク(02 章 2 節)、ビジーウェイト(02 章 3 節)、スケジューラ(03 章 2 節)
1. 4 つの状態
FreeRTOS のタスクは、常にこの 4 つのどれか 1 つの状態にある。
| 状態 | 意味 | CPU を使うか | 実体(どこにいるか) |
|---|---|---|---|
| Running | いま実行中 | 使う | pxCurrentTCB が指している |
| Ready | 実行可能。CPU の空きを待っている | 使わない | pxReadyTasksLists[優先度] |
| Blocked | 何かを待って寝ている | 使わない | 遅延リスト+イベントリスト |
| Suspended | 明示的に止められている | 使わない | xSuspendedTaskList |
単一コアでは Running はちょうど 1 つ. 走るタスクがなければ、アイドルタスクが Running になる(09 章)。 だから「Running が 0 個」という状態は存在しない。必ず何かが走っている。
状態機械を動かす
各遷移をクリックすると、何がきっかけで、カーネル内部で何が起きるかが表示される。
2. 状態遷移の全体像
┌─────────────────────────────────────┐
│ │
vTaskCreate │ (2) スケジューラが選ぶ │
│ │ ↓ │
↓ │ ┌──────────┐ │
┌────────┐ │ │ Running │ │
│ Ready │────┘ └────┬─────┘ │
└────────┘ ←──────────────┘ │
↑ ↑ (1) 優先度が高いタスクが Ready になった │
│ │ またはタイムスライス切れ │
│ │ │
│ │ (3) 待つ API を呼んだ │
│ │ ↓ │
│ └────────────┌───────────┐ │
│ (4) 条件成立 │ Blocked │ │
│ or タイムアウト└─────┬─────┘ │
│ │ │
│ (5) vTaskSuspend │
│ ↓ │
│ ┌────────────┐ │
└───────────────────│ Suspended │←───────────────┘
(6) vTaskResume└────────────┘ (5) vTaskSuspend遷移の一覧
| # | 遷移 | きっかけ | カーネルがやること |
|---|---|---|---|
| 1 | Running → Ready | 高優先度タスクが Ready に/タイムスライス切れ/taskYIELD() | Ready リストに戻す。pxCurrentTCB を更新 |
| 2 | Ready → Running | スケジューラが選んだ | Ready リストから外し、pxCurrentTCB を指す |
| 3 | Running → Blocked | vTaskDelay / xQueueReceive など待つ API | Ready リストから外し、遅延リスト(+イベントリスト)へ |
| 4 | Blocked → Ready | 条件成立(データ到着など)/タイムアウト | 遅延リストとイベントリストから外し、Ready リストへ |
| 5 | 任意 → Suspended | vTaskSuspend() | いまいるリストから外し、xSuspendedTaskList へ |
| 6 | Suspended → Ready | vTaskResume() | xSuspendedTaskList から外し、Ready リストへ |
Suspended → Running の直行はない. 必ず Ready を経由する。「実行可能になった」と「実行される」は別のことだからである。 同じ理由で、Blocked → Running もない。
3. Ready — 「走れるが、順番待ち」
Ready のタスクは、優先度ごとの配列に入っている。
static List_t pxReadyTasksLists[ configMAX_PRIORITIES ];pxReadyTasksLists[4] : (空)
pxReadyTasksLists[3] : ── TaskA ──┐ ← 優先度 3 の Ready リスト
└─────────┘
pxReadyTasksLists[2] : ── TaskB ── TaskC ──┐
└──────────────────┘ ← 同じ優先度が複数いてもよい
pxReadyTasksLists[1] : (空)
pxReadyTasksLists[0] : ── Idle ──┐ ← アイドルタスクは常にここ
└────────┘スケジューラの仕事は、「空でない最高優先度のリストの先頭を選ぶ」——それだけである。 どうやって「空でない最高優先度」を O(1) で求めるかは 07 章で扱う。
同じ優先度に複数のタスクがいるとき、どちらが走るか. リストの先頭が走る。そして走った後は末尾に回される。 これがラウンドロビンである(08 章)。
4. Blocked — 「条件が整うまで寝る」
Blocked は 4 つの中で最も重要で、最も込み入っている。
Blocked に入る API
| API | 何を待つか |
|---|---|
vTaskDelay(n) | いまから n ティック |
vTaskDelayUntil(&last, n) | 前回起床から n ティック後(周期実行用) |
xQueueReceive(q, &buf, timeout) | キューにデータが入るまで |
xQueueSend(q, &item, timeout) | キューに空きができるまで |
xSemaphoreTake(s, timeout) | セマフォが空くまで |
ulTaskNotifyTake(clear, timeout) | 通知が来るまで |
xEventGroupWaitBits(...) | 指定のビットが立つまで |
2 つの待ち方
Blocked には、実は性格の違う 2 種類がある。
| 種類 | 待つもの | 使うリスト | 起こされ方 |
|---|---|---|---|
| 時間待ち | 時間だけ | 遅延リストのみ | ティックが来たら |
| イベント待ち | イベント(+タイムアウト) | 遅延リスト + イベントリスト | イベント発生 or タイムアウト |
イベント待ちのタスクは、2 つのリストに同時に入っている。
TCB_t {
ListItem_t xStateListItem; ← 遅延リスト(または Ready リスト)に繋ぐ
ListItem_t xEventListItem; ← イベント待ちリストに繋ぐ
...
}1 つの構造体に 2 つのリストノードを持たせる.
これは C で多重リストを実現する定番の手法である。 Linux カーネルの
list_headを構造体に複数埋めるのと同じ発想である。なぜ 2 つ必要か:
xQueueReceive(q, &buf, pdMS_TO_TICKS(100))は 「データが来る or 100 ms 経つ、どちらか早い方」で起きる必要がある。 だから両方のリストに登録しておいて、先に来た方が起こす。 起こす側は、もう一方のリストからも外す責任を負う。
タイムアウトなし(portMAX_DELAY)で待つ場合は、遅延リストではなく xSuspendedTaskList に入る。 「永久に時間切れしない」=「時間の管理をしなくてよい」=「遅延リストに置く意味がない」からである。 これは実装上の最適化であって、状態としては Blocked である。
紛らわしい点:
vTaskSuspend()された Suspended タスクと、portMAX_DELAYで待っている Blocked タスクは、同じリストに同居している。eTaskGetState()はこの 2 つを区別できる(xEventListItemが繋がっているかで判定する)。
5. Suspended — 「明示的に止める」
vTaskSuspend(xHandle); /* 止める */
vTaskResume(xHandle); /* 再開する */
vTaskSuspend(NULL); /* 自分を止める(他人に起こしてもらう) */Suspended のタスクは、何があっても実行されない。
- タイムアウトしない
- キューにデータが来ても起きない
- 優先度が最高でも走らない
vTaskSuspendは「止まった瞬間」を選べない.他のタスクを
vTaskSuspend()すると、そのタスクはどこで止まるか分からない。 ミューテックスを持ったまま止まるかもしれない。malloc の途中かもしれない。 そうなると誰も先に進めなくなる。だから実務では、
vTaskSuspendを他タスクに対して使うのは避けるべきである。 代わりに「止まってほしい」と通知して、相手に安全な地点で自発的に止まってもらう。/* 止める側 */ xTaskNotify(worker, STOP_REQUEST, eSetBits); /* 止まる側(安全な地点で判定する) */ for (;;) { if (ulTaskNotifyTake(pdTRUE, 0) & STOP_REQUEST) { cleanup(); vTaskSuspend(NULL); /* 自分で止まる */ } do_work(); }
vTaskResume の非対称性
vTaskSuspend はネストしない。 3 回 Suspend しても、1 回の Resume で起きる。
これはカウンタではなく「リストにいるかどうか」で管理しているためである。 「3 回止めたから 3 回起こす」という設計にはなっていない。混乱の元なので覚えておくこと。
6. 削除 — 5 番目の「状態」
vTaskDelete(xHandle);
vTaskDelete(NULL); /* 自分自身 */削除は状態遷移というより「消滅」だが、すぐには消えない。
| 誰を消すか | どうなるか |
|---|---|
| 他のタスク | その場で TCB とスタックを解放できる |
| 自分自身 | 自分のスタックの上で走っているので解放できない |
自分を削除する場合、タスクは xTasksWaitingTermination リストに移され、 アイドルタスクが後で解放する(09 章)。
だから「削除したのにメモリが空かない」ことがある.
原因はほぼ 2 つである。
- アイドルタスクが走っていない。優先度の高いタスクがビジーウェイトしていると、 アイドルタスクに順番が回らず、永久に後片付けされない。
- 削除されたタスクが確保したリソースは自動で解放されない。
mallocした領域、取ったミューテックス、開いたファイル——全部残る。FreeRTOS にはリソースの自動回収がない。 Linux のプロセス終了とはまったく違う。 だから実務では、タスクを動的に生成・削除しない設計が推奨される。 起動時に必要なだけ作り、あとは寝かせておく方が安全である。
7. 状態を調べる API
eTaskState eTaskGetState(TaskHandle_t xTask);| 戻り値 | 意味 |
|---|---|
eRunning | 実行中(自分自身を問い合わせた場合) |
eReady | Ready |
eBlocked | Blocked |
eSuspended | Suspended(無期限 Blocked を含む) |
eDeleted | 削除済みで後片付け待ち |
eInvalid | ハンドルが無効 |
デバッグ時には、全タスクの状態を一覧できる。
/* configUSE_TRACE_FACILITY と configUSE_STATS_FORMATTING_FUNCTIONS が必要 */
char buf[512];
vTaskList(buf);
printf("%s", buf);Name State Priority Stack Num
Sensor B 3 142 1
Control R 4 88 2
Display B 2 210 3
IDLE R 0 112 4
Tmr Svc B 6 180 5Stack は残りスタックの最小値(ワード単位)である。17 章で詳しく扱う。
vTaskList()は本番で使ってはいけない. 実行中にスケジューラを止めて全タスクを走査するので、リアルタイム性が壊れる。 開発時のデバッグ用と割り切ること。
8. この章のまとめ
| ポイント | 内容 |
|---|---|
| 4 つの状態 | Running / Ready / Blocked / Suspended |
| 状態の実体 | どのリストに入っているか |
| Running | 常にちょうど 1 つ。走るものがなければアイドルタスク |
| Ready | 優先度ごとの配列 pxReadyTasksLists[] |
| Blocked | 時間待ち(遅延リスト)とイベント待ち(2 つのリストに同時登録) |
| 無期限待ち | 遅延リストではなく xSuspendedTaskList に入る(最適化) |
| Suspended | ネストしない。他タスクへの使用は避けるべき |
| 削除 | 自分の削除はアイドルタスクが後片付け。リソースは自動回収されない |
次章では、この状態遷移の中で最も重い操作—— Running が入れ替わる瞬間、コンテキストスイッチを見る。