組込み機器のCPUは1個でも、やることは「センサを読む」「モータを回す」「表示を更新する」と複数あります。リアルタイムOS(RTOS)は、その一つひとつをタスクという単位に分け、いま誰にCPUを渡すかを決め続けます。決め方の軸は優先度、そして外の出来事を知る手段が割込み。この2つが分かると、組込みの設問はほぼ同じ骨組みで読めます。
CPUは同時に1つのタスクしか実行できません。それでも複数の処理が並行して見えるのは、RTOSがごく短い間隔でCPUを渡す相手を切り替えているから。切り替えの単位がタスクで、切り替えの判断材料が状態(いま動けるのか、何かを待っているのか)と優先度です。だから設問は「その瞬間、どのタスクが実行中か」を追わせる形になります。
タスクの3状態
RTOSは各タスクを次の3つの状態のどれかで管理します。実行できるのは常に1つだけという一点が、すべての土台です。
実行(RUN)
- いまCPUを使っているタスク
- CPUが1個なら常に1つだけ
- 実行可能なタスクのうち最も優先度が高いものが選ばれる
実行可能(READY)
- いつでも動けるが、CPUが空いていないので待機
- 待っているのはCPUだけ
- 優先度順に並ぶ(レディキュー)
待ち(WAIT)
- 事象が起きるまで動けない(入力・時間経過・資源の空き)
- CPUが空いても動けない
- 事象が起きて初めて実行可能へ
状態遷移図
矢印の上がその遷移を起こす事象
自分より高優先度がいない
↗
待ち要因の発生
入力待ち・時間待ち・資源待ち
待っていた事象の発生
(待ち解除)
より高優先度のタスクが実行可能になり、CPUを取り上げられた(自ら手放す場合は横取りではない)
遷移表
| 遷移 | 呼び名 | きっかけとなる事象 |
|---|---|---|
| 実行可能 → 実行 | ディスパッチ | CPUが空き、そのタスクが実行可能な中で最高優先度だった |
| 実行 → 実行可能 | プリエンプション | より高優先度のタスクが実行可能になった(時分割方式なら持ち時間切れも) |
| 実行 → 待ち | 待ち状態への移行 | 自分から待つ操作をした(入力待ち・一定時間の待機・資源やメッセージの取得待ち) |
| 待ち → 実行可能 | 待ち解除 | 待っていた事象が起きた(データ到着・時間経過・資源が空いた) |
| 待ち → 実行 | 起きない | 待ちが解けても、CPUを取れるとは限らない。いったん実行可能に入って順番を待つ |
「実行可能」と「待ち」の区別が最初の壁です。どちらも動いていないので同じに見えますが、実行可能=CPUさえ空けば今すぐ動ける/待ち=CPUが空いても動けない。設問で「CPUが空いたのに動かないタスク」が出てきたら、それは待ちであり、必要なのはCPUではなく事象(データ・時間・資源)です。
動きで見る
1つのタスクが3状態を巡る様子を、番号順に追ってみます。横取り(プリエンプション)と待ちからの復帰が実行可能止まりであることが要点です。(図は横スクロールできます)
優先度とプリエンプション
RTOSが「リアルタイム」でいられるのは、締切の厳しい処理に高い優先度を付けておけば、いつでも即座に割り込ませられるからです。実行中のタスクを途中で止めてCPUを高優先度側に移すことをプリエンプション(横取り)、CPUを実際に割り当てる動作をディスパッチといいます。
設問は「この瞬間、どのタスクが実行中か」を問います。判定の型は①実行可能なタスクを全部挙げる → ②その中で最高優先度を選ぶ → ③それだけが実行、他は実行可能。待ちのタスクは優先度が高くても選ばれないのが引っかけどころです。順番に処理されるのではなく、高優先度が来たらその場で切り替わると考えます。
割込みとポーリング
外の出来事(ボタンが押された、データが届いた)を知る方法は2つあります。割込み=起きたら向こうから知らせてもらう/ポーリング=こちらから定期的に見に行く。どちらもプログラムは書けますが、応答性とCPUの無駄が大きく違います。
| 観点 | 割込み | ポーリング |
|---|---|---|
| 知り方 | 事象が起きた側がCPUに知らせる | CPUが定期的に状態を見に行く |
| 応答の速さ | 速い(発生した時点で処理へ移れる) | 遅れる(最悪で確認周期1回分の遅れ) |
| CPUの使い方 | 起きていない間は他の処理に使える | 何も起きていなくても確認処理で消費 |
| 短所 | 処理の流れが中断される。多発すると本処理が進まない | 周期を短くすると負荷増、長くすると応答悪化(取りこぼしも) |
| 向く場面 | めったに起きないが起きたらすぐ対応したい事象 | 常に頻繁に変化し、周期処理で足りるもの |
割込みハンドラ(割込み処理ルーチン)は短く済ませるのが鉄則です。割込み中は他の割込みや通常のタスクが待たされるので、ここで時間のかかる処理を書くと、別の割込みを取りこぼしたり高優先度タスクの締切を壊したりします。定石はハンドラでは「受け取って記録し、担当タスクに知らせる」だけにして、重い処理はタスク側で行うこと。設問で「ハンドラの処理時間を短くする理由」を問われたら、この一点です。
排他制御 ― 共有資源を同時に触らせない
複数のタスクが同じメモリや同じ装置を使うとき、途中で横取りされると中途半端な状態を別のタスクが見てデータが壊れます。そこで、共有資源を使う区間(クリティカルセクション)に入れるのを1つのタスクだけに限る仕組みが必要です。
セマフォ
- 使える資源の個数を数える仕組み
- 取得で減り、返却で増える。0なら待ちに入る
- 個数1で使えば「1つだけ通す」鍵になる
- 資源の空きをタスク間で知らせる用途にも使える
ミューテックス
- 1つだけ通すことに特化した鍵
- ロックした本人しか解除できない(持ち主の概念がある)
- 持ち主が分かるので優先度逆転への対策を組み込める
優先度逆転:低優先度タスクが資源をロックしている最中に、同じ資源を要る高優先度タスクが来ると、高優先度側がロック解除を待たされます。さらに悪いのは、その間に中くらいの優先度のタスクが実行可能になった場合。中優先度は資源と無関係なのでどんどん実行され、低優先度はCPUをもらえずロックを解除できない。結果、高優先度が中優先度に事実上追い越される——これが優先度逆転です。対策は、資源を持つ間だけ低優先度側の優先度を一時的に引き上げること(優先度継承)。
タスク間通信
タスクどうしは共有変数を直接触るのではなく、RTOSの用意した通信手段でやり取りします。
メッセージ(メールボックス/キュー)
- データそのものを渡す
- 受け手はキューが空なら待ちに入り、届くと待ち解除
- 送った順に取り出せるので取りこぼしにくい
イベントフラグ
- 「起きた/起きていない」のビットの集まり
- データは運ばず合図だけを伝える
- 複数条件がそろったら動くという待ち方ができる
リングバッファと追い越し
割込みで受け取ったデータを溜め、タスク側でゆっくり取り出す——この受け渡しに使う定番がリングバッファです。配列の末尾まで書いたら先頭に戻り、書込み位置と読出し位置を別々に進めます。
書込みが速すぎて読出し位置を追い越した瞬間、未読データが上書きされる
▼
▼
正常時:書込みが先、読出しが後を追う(間にある=未読データ)
↓ 読出しが遅れ、書込みが一周して追いついた
▼
まだ読まれていないデータの上に新しいデータが書かれ、そのデータは永久に失われる
壊れ方は「書込みが読出しを追い越す」の一方向です(読出しが書込みを追い越すのは、まだ書かれていない古いデータを読むという別の不具合で、通常は「空なら読まない」判定で防ぎます)。追い越しが起きる原因は受信の速さに対して読出しが遅いこと。対策は、バッファを十分大きくする/読出し側タスクの優先度を上げる/満杯なら受信側を待たせるか到着を通知する、という向きで考えます。「なぜデータが欠落したか」=上書きと即答できるようにしておきます。
デッドラインと周期 ― CPU利用率で見積もる
リアルタイム性の判断は、感覚ではなく数え上げで行います。周期的に動くタスクが複数あるとき、各タスクについて「1周期あたりに使う時間 ÷ 周期」を求め、それを全部足したものがCPU利用率です。
CPU利用率 = Σ( 各タスクの処理時間 ÷ そのタスクの周期 )
例:10ms周期で2ms使うタスク(0.2)+50ms周期で10ms使うタスク(0.2)+100ms周期で30ms使うタスク(0.3)
→ 合計 0.7=70%。残り30%が余裕(他の処理・割込み・将来の追加分)
判断の型は「合計が100%を超えていないか」。超えていればどう優先度を付けても間に合わないので、処理時間を削るか周期を延ばすしかありません。100%未満でも、割込み処理の時間や切替えのオーバヘッド、そして高優先度に横取りされて待たされる時間があるため、実務では余裕を残します。設問では「処理時間の合計が周期内に収まるか」「周期を短くしたら成立するか」という形で、この計算をそのまま問われます。
状態の呼び名や数はRTOSによって異なり、実行可能を細分したり「休止(未起動)」「強制待ち」を加えて4〜5状態で説明することもあります。試験で土台になるのは実行/実行可能/待ちの3つと、その間の遷移を起こす事象なので、まずこの3状態で読み解けるようにしておけば足ります。