FreeRTOS 04 · タスクの状態機械 — Running / Ready / Blocked / Suspended

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

遷移の一覧

#遷移きっかけカーネルがやること
1Running → Ready高優先度タスクが Ready に/タイムスライス切れ/taskYIELD()Ready リストに戻す。pxCurrentTCB を更新
2Ready → Runningスケジューラが選んだReady リストから外し、pxCurrentTCB を指す
3Running → BlockedvTaskDelay / xQueueReceive など待つ APIReady リストから外し、遅延リスト(+イベントリスト)へ
4Blocked → Ready条件成立(データ到着など)/タイムアウト遅延リストとイベントリストから外し、Ready リストへ
5任意 → SuspendedvTaskSuspend()いまいるリストから外し、xSuspendedTaskList へ
6Suspended → ReadyvTaskResume()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 つである。

  1. アイドルタスクが走っていない。優先度の高いタスクがビジーウェイトしていると、 アイドルタスクに順番が回らず、永久に後片付けされない。
  2. 削除されたタスクが確保したリソースは自動で解放されない。 malloc した領域、取ったミューテックス、開いたファイル——全部残る。

FreeRTOS にはリソースの自動回収がない。 Linux のプロセス終了とはまったく違う。 だから実務では、タスクを動的に生成・削除しない設計が推奨される。 起動時に必要なだけ作り、あとは寝かせておく方が安全である。

7. 状態を調べる API

eTaskState eTaskGetState(TaskHandle_t xTask);
戻り値意味
eRunning実行中(自分自身を問い合わせた場合)
eReadyReady
eBlockedBlocked
eSuspendedSuspended(無期限 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    5

Stack は残りスタックの最小値(ワード単位)である。17 章で詳しく扱う。

vTaskList() は本番で使ってはいけない. 実行中にスケジューラを止めて全タスクを走査するので、リアルタイム性が壊れる。 開発時のデバッグ用と割り切ること。

8. この章のまとめ

ポイント内容
4 つの状態Running / Ready / Blocked / Suspended
状態の実体どのリストに入っているか
Running常にちょうど 1 つ。走るものがなければアイドルタスク
Ready優先度ごとの配列 pxReadyTasksLists[]
Blocked時間待ち(遅延リスト)とイベント待ち(2 つのリストに同時登録)
無期限待ち遅延リストではなく xSuspendedTaskList に入る(最適化)
Suspendedネストしない。他タスクへの使用は避けるべき
削除自分の削除はアイドルタスクが後片付け。リソースは自動回収されない

次章では、この状態遷移の中で最も重い操作—— Running が入れ替わる瞬間、コンテキストスイッチを見る。