Chapter 22
設計指針と他の OS との比較
この章がなぜ必要なのか——ここまでの知識を「使える形」にまとめるため.
各章で個別に述べた指針を、設計・実装・レビューの順に並べ直す。 そして最後に、FreeRTOS で学んだ考え方が 他の OS にどこまで通用するのかを確認する。
OS の芯は共通である。 それを確かめて、このシリーズを終える。
この章で使う既出の用語(定義は各リンク先). コンテキスト(01 章 6 節)、TCB(02 章 2 節)、タスク(02 章 2 節)、ビジーウェイト(02 章 3 節)、ハードウェア(03 章 2 節)、Blocked(04 章 1 節)、Ready(04 章 1 節)、Running(04 章 1 節)、Suspended(04 章 1 節)、CFS(06 章 1 節)、FreeRTOS(06 章 4 節)、RAM(06 章 4 節)、固定優先度(06 章 1 節)、バイナリセマフォ(12 章 5 節)、ブロック(13 章 1 節)、メッセージ(14 章 7 節)、ソフトウェアタイマ(15 章 1 節)、ティック(15 章 1 節)、不可(16 章 4 節)、MPU(17 章 5 節)、正確(17 章 5 節)、循環待ち(19 章 4 節)、デッドライン(20 章 2 節)、周期(20 章 2 節)、最高(20 章 4 節)、スタックオーバーフロー(21 章 9 節)、デッドロック(21 章 8 節)
1. 設計段階で決めること
タスク分割
| 指針 | 理由 | 章 |
|---|---|---|
| 「独立して待つもの」を 1 タスクに | 待ちの単位がタスクの単位 | 02 |
| 10〜15 個以内に収める | 1 タスクあたり数百バイト〜数 KB | 02 |
| 分ける理由がないなら分けない | タスク間通信がバグを生む | 02 |
| タスクを動的に生成・削除しない | リソースが自動回収されない | 04 |
優先度設計
| 指針 | 理由 | 章 |
|---|---|---|
| 周期が短いタスクほど高優先度(RM) | 固定優先度では最適 | 20 |
| 実行時間で優先度を決めない | 重い処理を上げると短周期が破綻する | 20 |
| 優先度は 5〜8 段で足りる | 段数が多いと迷う | 06 |
| 同じ優先度に複数タスクは正常 | むしろ推奨される | 06 |
| 優先度 0 を使わない | アイドルタスク専用 | 09 |
configTIMER_TASK_PRIORITY は高め | ただしコールバックを軽くする | 15 |
通信路の選択
| やりたいこと | 道具 | 章 |
|---|---|---|
| ISR → タスクの起床 | タスク通知 | 14 |
| 構造化データの受け渡し | キュー | 11 |
| 大きな可変長データ | メッセージバッファ | 14 |
| UART の連続受信 | ストリームバッファ | 14 |
| 共有資源の保護 | ミューテックス | 12 |
| 複数条件の AND 待ち | イベントグループ | 14 |
| 資源の個数管理 | カウンティングセマフォ | 12 |
メモリ方針
| 指針 | 理由 | 章 |
|---|---|---|
| 静的確保を既定にする | 失敗しない・ビルド時に分かる | 16 |
| 動的が要るなら heap_4 | 結合するので断片化に強い | 16 |
| スタックはヒープの中にあることを意識する | configTOTAL_HEAP_SIZE の見積り | 16 |
printf はログタスクに集約 | 単独で 1〜2 KB 使う | 17 |
ネットワーク/ハードウェアの選択
| 指針 | 理由 | 章 |
|---|---|---|
| 正確なタイミングはハードウェアタイマ | ソフトウェアタイマは精度が粗い | 15 |
| キャッシュがあると WCET が難しくなる | 予測可能性のために無効化も検討 | 20 |
| 共有ペリフェラルはミューテックスで守る | 切り替えでレジスタは保存されない | 05 |
2. 実装段階の掟
タスク関数
void vTask( void *pv )
{
/* 初期化(ここは 1 回だけ) */
for( ;; )
{
/* ★ 必ずブロックする API を含める */
xQueueReceive( q, &item, portMAX_DELAY );
/* 処理 */
}
/* ★ ここには到達しない。到達するなら vTaskDelete(NULL) を書く */
}| 掟 | 理由 | 章 |
|---|---|---|
| 関数から return しない | 戻る先がない | 02 |
| 必ずブロックする API を入れる | ビジーウェイトは低優先度を飢えさせる | 02, 06 |
| 大きな配列をローカルに置かない | スタックオーバーフロー | 17 |
再帰・VLA・alloca を使わない | スタック深さが計算できなくなる | 17 |
周期処理は vTaskDelayUntil | vTaskDelay は誤差が累積する | 08 |
排他制御
| 掟 | 理由 | 章 |
|---|---|---|
| 共有変数を作らない。キューで渡す | 最も強力な対策 | 19 |
| ロックにはミューテックス(バイナリセマフォ不可) | 優先度継承 | 12, 13 |
| ミューテックスを持ったままブロックしない | 最重要 | 13 |
| ミューテックスをネストしない | デッドロック・連鎖継承の限界 | 13, 19 |
| ネストするなら 順序を決めて文書化 | 循環待ちを防ぐ | 19 |
| クリティカルセクションは数 µs 以下 | 割り込みレイテンシに直結 | 18, 19 |
| 確認と実行を分けない | TOCTOU | 19 |
_locked 命名規約を使う | 自己デッドロックを防ぐ | 19 |
割り込み
| 掟 | 理由 | 章 |
|---|---|---|
| ISR は 10 µs 以下 | 全タスクの応答時間に効く | 18, 20 |
| ISR からは FromISR 版のみ | ブロックできない | 18 |
portYIELD_FROM_ISR を最後に 1 回 | 忘れると 1 ティック遅れる | 18 |
xHigherPriorityTaskWoken を pdFALSE で初期化 | 未初期化だと誤動作 | 11, 18 |
割り込み優先度を configMAX_SYSCALL 以下に | API を呼ぶなら必須 | 18 |
configPRIO_BITS をチップに合わせる | 設定ミスの温床 | 18 |
| 本処理は遅延タスクへ | 割り込み禁止時間を短く | 18 |
戻り値
/* ★ すべての生成 API・送信 API の戻り値を確認する */
if( xTaskCreate( vTask, "T", 256, NULL, 2, &h ) != pdPASS ) { fail(); }
if( xQueueSend( q, &item, 0 ) != pdPASS ) { handle_full(); }
if( xTimerStart( t, 0 ) != pdPASS ) { fail(); }
if( ( q = xQueueCreate( 10, sizeof(int) ) ) == NULL ) { fail(); }「たぶん成功する」で書くと、ヒープ不足やキュー満杯が静かに無視される。
3. レビュー用チェックリスト
設計レビュー
- ☐ タスク数は 15 個以内か
- ☐ 各タスクは「独立して待つもの」になっているか
- ☐ 優先度はレートモノトニックに従っているか
- ☐ 共有データを一覧にし、保護方法を決めたか
- ☐ ミューテックスに順序番号を振ったか
- ☐ CPU 利用率の見積りは 70 % 以下か
- ☐ スタックとヒープの合計は RAM に収まるか
- ☐ 動的確保が必要な箇所を洗い出したか
コードレビュー
- ☐ タスク関数のループにブロックする API があるか
- ☐ ミューテックスを持ったままブロックしていないか
- ☐ ミューテックスをネストしていないか
- ☐ クリティカルセクション内でブロックする API を呼んでいないか
- ☐ ISR から通常版 API を呼んでいないか
- ☐
portYIELD_FROM_ISRを書いたか - ☐
xHigherPriorityTaskWokenを初期化したか - ☐ タイマコールバックでブロックしていないか
- ☐ アイドルフックでブロックしていないか
- ☐
static変数を持つ関数を複数タスクから呼んでいないか - ☐ 生成 API・送信 API の戻り値を確認しているか
- ☐
printfを高優先度タスクから直接呼んでいないか
検証
- ☐
configASSERTを有効にして全機能を通したか - ☐
configCHECK_FOR_STACK_OVERFLOW = 2で長時間動かしたか - ☐ 全タスクの High Water Mark を確認したか
- ☐
xPortGetMinimumEverFreeHeapSize()を確認したか - ☐ エラー処理経路も含めてスタックを測ったか
- ☐ デッドラインミスをカウントしているか
- ☐ 49.7 日(ティック折り返し)を跨ぐ試験をしたか(またはシミュレートしたか)
設計レビュー用チェックリスト
上のチェックリストを、分野別にたどれる形で確認できる。
4. よくある症状と原因
| 症状 | 疑うべき原因 | 章 |
|---|---|---|
| たまに再起動する | スタックオーバーフロー | 17 |
| 数時間〜数日後にハングする | ヒープ断片化/ティック折り返し/割り込み優先度 | 16, 10, 18 |
| タスクを削除してもヒープが減ったまま | アイドルタスクの飢餓 | 09 |
| 応答がたまに 1 ms 遅れる | portYIELD_FROM_ISR の書き忘れ | 18 |
printf を足したら落ちた | スタック不足 | 17 |
| 高優先度タスクが遅い | 優先度逆転/ISR が重い/クリティカルセクションが長い | 13, 18 |
| タイマが全部止まった | タイマコールバックでブロックした | 15 |
| 低優先度タスクが動かない | 高優先度のビジーウェイト(正常な動作) | 06 |
vTaskStartScheduler() から戻ってくる | ヒープ不足 | 03, 16 |
vTaskDelay が待たない | pdMS_TO_TICKS が 0 になっている | 08 |
| 周期が少しずつずれる | vTaskDelay を使っている | 08 |
| デバッガを繋ぐと直る | レースコンディション | 19 |
5. 他の OS との比較
用語の対応
| FreeRTOS | Zephyr | POSIX / Linux | µITRON / TOPPERS |
|---|---|---|---|
| タスク | スレッド | スレッド (pthread) | タスク |
xTaskCreate | k_thread_create | pthread_create | cre_tsk / act_tsk |
vTaskDelay | k_sleep | nanosleep | dly_tsk |
| キュー | k_msgq | メッセージキュー (mq_*) | データキュー |
| セマフォ | k_sem | sem_t | セマフォ |
| ミューテックス | k_mutex | pthread_mutex_t | ミューテックス |
| イベントグループ | k_event | (条件変数で代替) | イベントフラグ |
| タスク通知 | (相当なし) | (相当なし) | (相当なし) |
| ソフトウェアタイマ | k_timer | timer_create | 周期ハンドラ |
設計思想の違い
| 観点 | FreeRTOS | Zephyr | Linux (PREEMPT_RT) |
|---|---|---|---|
| 規模 | 約 1 万行 | 約 100 万行 | 約 3000 万行 |
| 優先度 | 数値大が高 | 数値小が高(負値は協調的) | RT は大が高、nice は小が高 |
| スケジューラ | 固定優先度 | 固定優先度 + メタIRQ | CFS / SCHED_FIFO / SCHED_DEADLINE |
| メモリ保護 | なし(MPU 版はある) | MPU / MMU 対応 | MMU で完全分離 |
| プロセス | なし | なし | あり |
| デバイスドライバ | なし(自分で書く) | 豊富(統一 API) | 非常に豊富 |
| ネットワーク | 別製品(FreeRTOS+TCP) | 内蔵 | 内蔵 |
| ビルド | ソースを取り込む | West / CMake / Kconfig | Kconfig / make |
| 最小 RAM | 数 KB | 十数 KB | 数 MB〜 |
| 優先度継承 | 簡易版 | あり | あり(rt_mutex) |
| SMP | v11 で対応 | 対応 | 完全対応 |
どれが優れているか、ではない.
- RAM 8 KB のマイコンで動かす → FreeRTOS(Zephyr でも厳しい)
- ドライバとネットワークを自分で書きたくない → Zephyr
- ファイルシステム・プロセス分離・豊富なライブラリ → Linux
- 機能安全認証を取りたい → FreeRTOS(SafeRTOS)、あるいは商用 RTOS
規模が大きいほど機能が多く、RAM を食い、予測可能性が下がる。 トレードオフである。
共通している「OS の芯」
このシリーズで学んだことのうち、どの OS でも同じものを挙げる。
| 概念 | どこまで共通か |
|---|---|
| コンテキストスイッチ | 完全に同じ。レジスタとスタックポインタの保存・復元 |
| タスク/スレッドの状態機械 | ほぼ同じ。Running/Ready/Blocked/Suspended |
| Ready リストと優先度 | 同じ発想。Linux も優先度別リスト+ビットマップ(O(1) 時代) |
| ブロックと起床 | 同じ。待ちキューに繋いで、条件成立時に外す |
| 偽の目覚めへの対処 | 完全に同じ。while で条件を再確認する |
| 優先度逆転と継承 | 同じ問題、同じ解法 |
| デッドロックの 4 条件 | 完全に同じ。順序規約も同じ |
| 割り込みの上半分/下半分 | 同じ思想。名前が違うだけ |
| 遅延実行(ワークキュー) | 同じ。FreeRTOS の xTimerPendFunctionCall = Linux のワークキュー |
| レートモノトニック・応答時間解析 | 理論は共通。OS を問わない |
これがこのシリーズの結論である.
FreeRTOS は小さいが、OS が OS であるための要素はすべて持っている。 そして、その要素は他の OS でも同じ形で存在している。
Linux の
schedule()は 1000 行を超え、 CFS の赤黒木、CPU ごとのランキュー、負荷分散、 cgroup による制限——さまざまなものが載っている。だがその芯にあるのは、「Ready なタスクから 1 つ選んで、コンテキストを差し替える」 という、FreeRTOS の
vTaskSwitchContext()と同じ処理である。小さいものを完全に理解することは、大きいものを理解する最短経路である。
6. FreeRTOS の SMP 対応(補足)
v11 でマルチコア(SMP)が本体に統合された。単一コアとの違いを簡単に述べる。
#define configNUMBER_OF_CORES 2| 変化 | 内容 |
|---|---|
pxCurrentTCB | 配列になる(pxCurrentTCBs[coreID]) |
| 「最高優先度が走る」 | 「上位 N 個が走る」に変わる |
| 並行 → 並列 | 本当に同時に実行される(02 章の話が変わる) |
| クリティカルセクション | 割り込み禁止+スピンロックが必要 |
| タスク固定 | vTaskCoreAffinitySet() で特定コアに固定できる |
SMP になると、02 章で「単一コアでは並列は起きない」と述べた前提が崩れる.
- 「読む・足す・書く」の途中でなくても、本当に同時に同じ変数を触りうる
taskENTER_CRITICAL()は「自コアの割り込みを止める」だけでは不十分- 単一コアで「たまたま動いていた」コードが、確実に壊れる
単一コアから SMP への移行は、排他制御の全面的な見直しを要する。 安易に
configNUMBER_OF_CORESを増やしてはいけない。
7. これから読むもの
| 対象 | 内容 |
|---|---|
tasks.c | このシリーズの 04 章〜10 章の実物。まずこれを読む |
queue.c | 11 章〜14 章の実物。xQueueGenericSend を追う |
list.c | 250 行。30 分で読み切れる。最初に読んでもよい |
port.c(自分のCPU用) | このシリーズで省いた部分。契約の実装が見える |
| FreeRTOS 公式ドキュメント | freertos.org の API リファレンスとブック |
| *Hard Real-Time Computing Systems*(Buttazzo) | 20 章の理論的背景 |
| *Making Embedded Systems*(White) | 組み込み設計全般 |
Zephyr の kernel/sched.c | 同じ問題を別の解き方で。比較すると理解が深まる |
最後に、ソースを読むときの助言.
tasks.cは 5,500 行あるが、#ifの中身の大半は使わない機能である。 自分のFreeRTOSConfig.hで有効になっているものだけを追えば、 実質的に読むべきは 1,500 行程度である。そして、その 1,500 行の中身は——このシリーズで全部説明した。
地図は渡した。あとは歩くだけである。
8. このシリーズのまとめ
| 部 | 学んだこと |
|---|---|
| I. なぜ OS が要るのか | スーパーループの限界。「途中で待てる」ことが OS の価値。コンテキストという有限のデータ |
| II. スケジューラの中核 | 状態=リストの所属。切り替え=SP の差し替え。規則は「最高優先度が必ず走る」の一行。O(1) の Ready リスト |
| III. 待つ・起こす | ソート済み遅延リストと 2 本交換。キュー 1 本でセマフォもミューテックスも実装。優先度継承 |
| IV. 時間とメモリ | タイマタスクという専用スタッフ。5 つのヒープ。スタックが最大の事故原因 |
| V. 正しく動かす | 割り込みとの境界の規則。排他設計の原則。応答時間解析。トレース |
OS は魔法ではない。 レジスタをスタックに積み、ポインタを差し替え、リストの間でノードを動かす—— それだけのことを、正確に、漏れなく、速くやっているだけである。
その「それだけのこと」を最後まで追えたなら、 あなたはもう、どの OS のソースを開いても道に迷わない。