リアルタイムOSの内部構造を見てみよう!

第03回 ASPカーネルの構成

前回は仕様を策定する上での方針について解説してきました.今回は実装にあたってのもう少し具体的な方針を解説します.概念的なお話は今回までになります.次回からは,具体的なソースコードが出てきます.具体的なソースコードを理解するために必要なデータ構造なども,今回解説しました.
実装するにあたって,次の5つの基本方針が立てられました.

1.ソースコードの読みやすさ,改造しやすさを重視
オープンソースの良い点として,たくさんの人の目に触れて,品質を高めていくことができる.という点があります.そのためASPカーネルのソースコードは,ソースコードの読みやすさを重視しています.また,ASPカーネルが搭載される組込みシステムは,大きさも特徴もさまざまなものが想定されます.よって,システムの要求に合わせて,機能をちょっと追加するといった,ユーザーがカーネルをチューニングする.という使い方も重要だと考えていますので,改造しやすいというのも重視してソースコードが書かれています.

2.新しいターゲットシステムへのポーティングが行いやすい構造
ターゲット依存部,非依存部を明確にしています.詳細は後ほど解説します.

3.検証が容易な構造
  あまり凝った作りにすると,検証パターンが増えてしまうという点が懸念されます.検証はできるだけ容易に.という方針で作られています.詳細は後ほど解説します.

4.実行性能とメモリ使用量に配慮
RAM使用量を小さくしています.詳細は後ほど解説します.

5.スケーラビリティに配慮
大小さまざまなシステムで使えるように.サービスコールを少ししか使わないのなら,使わないサービスコールのために,メモリ使用量が増えない作りとしています.

上記,5つの基本方針をもとに,ASPカーネルでは次の5つを採用しています.
1リンクモデル
カーネルのライブラリ化
ターゲット依存部と,非依存部との明確な分離
サービスコール実行中に割込みを許可しない
RAM使用量を抑える

それぞれ1つずつ解説していきます.

1リンクモデル
ASPカーネルの大きな特徴として,1リンクモデルを採用している.ということがあります.システム全体(アプリケーション+システムサービス+カーネル)を1つのロードモジュールにリンクして,実行形式にしています.システムサービスとはミドルウェアつまり,デバイスドライバーなどを含みます.
この1つになっている実行形式を,ROM(またはRAM)において実行します.
このような仕組みにしていることによって,アプリケーションからカーネルの機能を呼び出すときには,サブルーチンコール,つまり関数呼び出しで簡単に呼び出すことができるわけです.これを実現するためには,アプリケーションもカーネルも特権モードで実行しなくてはいけません.しかし,ASPカーネルはメモリ保護機能を持っていないので,両方とも特権モードで動かしても差し支えありません.

▲ーネルのライブラリ化
カーネルのコードをライブラリ化して,リンカのライブラリリンク構造によって必要なコードのみをリンクしています.つまり,ライブラリリンク機構を使用して,自分が呼び出したサービスコールのライブラリしかリンクされない仕組みにしています.たとえば,メールボックスを使わなければ,そのコードはリンクされないので,使わないメールボックス機能のためにメモリサイズが増えることはありません.特別な設定をしなくても,不要なコードを省くことができるので,スケーラビリティの確保の観点からもメリットが大きいといえます.
他のRTOSが採用している方法に,コンフィギュレーションするときに,#ifdefを埋め込んで,使用する機能を選択する.というものがあります.細かく設定できて便利な半面,テストしなくてはいけないコンフィギュレーションの数が膨大になる.という面もあるため,「3.検証を容易にする」という基本方針のもと,ASPカーネルではカーネルのライブラリ化を採用しています.

ターゲット依存部と非依存部との明確な分離
多種類のターゲットに対応するためには必須の考え方です.

ASPカーネルのターゲット依存部について解説します.
\target\ms7727cp01_gcc
ターゲット依存部は,\targetというディレクトリにあります.さらに,\targetディレクトリの下に\ms7727cp01_gccというディレクトリがあります.今回使用するボードは,SH3のms7727cp01というボードで,コンパイラはgccを使用します.開発環境によってアセンブラが違うこともありますので,このように,今回使用するボードとコンパイラに依存するものがここに入っています.
次に,ターゲット依存部の共通部は,\archに入っています.つまり,SH3,SH4に共通な部分です.
\arch\sh34_gcc
SH3SH4をつかったボードは他にもたくさんあります.タスク切り替えのロジックなど,SH3,SH4で共通なソースコードは,ここに入っています.

このディレクトリ構成は,JSPカーネルからずいぶん変更されました.

ぅ機璽咼好魁璽觴孫埣罎乏箙みを許可しない
これはOS設計において,大きなデシジョンポイントになります.この方針のため,カーネルの中はほぼ割込み禁止で動いています.コードがシンプルになりますし,カーネルの処理時間は短くなりますが,割込みの応答性は犠牲になっています.
長い処理時間が必要なサービスコール(たとえば,優先度順にキューをつなぐシステムコールですと,キューが長くなると少し時間がかかります.)も若干ありますが,これで,差支えないだろうという判断で作成しています.これも,アプリケーションがどのくらいの性能を要求するか.に大きく依存します.たとえば,「割込み応答は,100μ以内に.」という要求であれば,この方針で十分だと考えられます.条件に合わないアプリケーションの場合は,改造が必要です.
この方針も,「3.カーネルの検証を容易にする」ため,「1.改造を容易にする」ために,採用しています.

RAM使用量を抑える
こちらの方針は,性能とのトレードオフもあり,極限は追及していません.ROMではなく,RAMの使用量を減らすことに重点を置いています.RAM制約のほうが厳しい場合が多い,と判断したからです.(たとえば,1チップマイコンの場合は,RAMが4Kなど小さいことが多い.)外にDRAMをつけ,RAM使用量を気にする必要のない場合もありますが,ASPカーネルはRAMの使用量を抑える方針をとっています.
実装上では,スタックにおけるものはスタックに置き,固定エリアをできるだけ減らす方針で作成しています.たとえば,タスクを切り替えるときに,実行中のタスクのレジスタを退避させる必要があります.その際,すべてのレジスタをタスクの状態を管理するTCBという領域に保存するという方法もありますが,ASPカーネルでは,固定エリアをできるだけ減らすために,タスクのレジスタは,スタックに退避します.また,タスクの状態を管理するデータも,定数のものと変数のものを格納するデータ構造を分けることで,ROMに置くことができるものはROMに置く方針にしています.
ASPカーネルの構成を,【図3-1】で解説します.


【図3-1 ASPカーネル構成図】

カーネルの上でユーザーが定義したタスクや,カーネル生成割込みハンドラ,ユーザ定義割込みハンドラが動作します.ユーザISRを作成すると,コンフィギュレータによって,カーネル生成割込みハンドラが生成されます.
前述しましたように,カーネルの中には,ターゲット依存部と非依存部があります.

システムログタスクについて
たとえば,printf関数で100文字出力するには時間がかかります.そのため,出力を依頼したタスクが,待ち状態になる可能性があります.その間,優先順位の低い他のタスクが動いてしまって,振る舞いが変わってしまう可能性があります.そういった不都合な点があるので,システムログタスク機能を用意しています.
最低限の情報(32バイト位のデータ)をシステムログ機能の中のバッファにいったんコピーします.そのあと時間があるときに,システムログタスクがバッファからデータを吸い上げて,シリアルドライバ経由でログを出力するという仕組みです.ですから,出力を依頼してから,実際にシリアルドライバから出力されるまでに時間がかかる場合があります.しかしこの方法ですと,システムそのもののリアルタイム性を阻害せずにログを出力することができます.
一方,システムログタスクはOSの上で動いているので,システムがダウンしたら出力することができません.システムがダウンしても最後のメッセージを出力する方法として,低レベル出力機能を経由して文字を出力する方法も用意されています.

デイレクトリ構成
ASPカーネルのディレクトリ構造を以下に示します.
\include アプリケーション向けヘッダファイル
\kernel カーネルのソースファイル
\syssvc シリアルドライバ,システムログタスクなどの,システムサービスソースファイル
\library サポートライブラリソースファイル
\target ターゲット依存部
\arch ターゲット依存部の共通部分
\pdic デバイスドライバのターゲット依存部
\cfg カーネルのコンフィギュレータ
\utils ユーティリティプログラム
\sample サンプルプログラムとmakefile
\doc ドキュメント
\test テストプログラム

カーネル本体の概略構造図
カーネルと外部がどのようなやり取りをするか.を表現したのが,【図3-2】になります.【図3-1 ASPカーネル構成図】の,リアルタイムカーネル部分を拡大したものです.

<br /><br /><br /><br /><br />
【図3-2 カーネル本体の構成図】

ASPカーネルには,100以上のサービスコールがありますが,それぞれに対応するサービスコール処理ルーチンがあります.そのサービスコール処理ルーチンから,必要に応じてユーティリティ関数(タスク管理,待ち状態管理,タイムイベント管理)が呼ばれて処理が行われます.

ここで2つの例を用いて,処理の流れをカーネル本体の構成図を使って解説します.

1,タスクの処理
システムが起動するとタスクBが起動して,タスクBが自分より優先度の高いタスクAに対して,act_tskサービスコールを発行します.その後,タスクBからタスクAにディスパッチするまでの流れを【図3-3】を用いて説明します.

<br /><br />
【図3-3 カーネル本体の構成図<タスク処理>】

,泙此ぅ瓮ぅ鶸愎堯覆冒蠹する処理)の中で,カーネルの初期化が行われて最後にディスパッチャが呼ばれる.
▲妊スパッチャが呼ばれると,タスクの切り替えが行われ,最初に動くべきタスクBを実行状態にしてタスクBが実行.
タスクBが動いて,その中でact_tsk(タスクA)サービスコールを発行し,カーネル内に入る.
きイ泙此ぢ弍するサービスコール処理ルーチンに入り,そこからタスク管理ユーティリティを使いながら処理をすすめる.
Ε妊スパッチが必要か判断し,必要であればディスパッチャが起動.
Д織好Bから一番優先順位の高いタスクAへ切り替わる.


2.割込み処理の場合
割込み処理が起動した場合について,【図3-4】を用いて解説します.

<br /><br />
【図3-4 カーネル本体の構成図<割込み処理>】

割込みが入るとカーネルの中にある,割込みハンドラ出入り口処理に行く.ここでは必要最低限の処理を行う.たとえば,レジスタ保存,スタックエリアの切り替え,多重割込み受付可能にする.など.
対応する割込みハンドラへ.
3箙みハンドラからサービスコールが呼ばれると,カーネル内に入っていく.
こ箙み処理中に呼ばれたサービスコールによって,タスク切り替えが必要になっても,サービスコール処理ルーチンから直接タスクディスパッチャは呼ばれない.なぜなら,割込みハンドラは,ディスパッチャより優先度が高いため,いったん割込みハンドラに処理が戻る.
キΤ箙み処理が終了後,タスクディスパッチャが起動.

3-1.オブジェクト毎のデータ構造
カーネルオブジェクト毎に,それを管理するための,データ構造.これをコントロールブロックと呼びます.
例)タスクコントロールブロック(TCB),セマフォコントロールブロック(SEMCB)
管理すべきデータの中には,定数のデータと変数のデータの両方があります.たとえば,タスクの管理情報のうち,タスクが使用するスタックの場所,初期優先度などが定数データです.それに対して,現在の優先度,タスクの状態などは,実行中に変化する変数データです.これら両方をTCBの中に入れておいてもいいのですが,なるべくRAM使用量を抑えるという観点から,定数値と変数値を分離します.そして定数値だけ,初期化ブロックと呼ぶデータ構造に格納し,こちらをROMに置けるようにして,RAM使用量を抑える仕組みとしています.
(例)タスク初期化ブロック(TINIB),セマフォ初期化ブロック(SEMINIB)

3-2.待ち状態を管理するためのデータ構造(WINFO)
タスクの待ち状態管理のためのデータ構造.
ある待ち状態のタスクが,どういう条件で待っているのか.を管理します.たとえば,セマフォを待っている,イベントフラグをAという条件で待っている.といった情報です.
ASPカーネルでは,このデータ構造をスタック上に確保することとしています.これも,固定的に使用するメモリ領域を減らすという方針からきている実装方法です.

3-3.タイムイベントを管理するためのデータ構造(TMEVTB)
発生時刻順にタイムイベントを管理するためのデータ構造. 
どこに確保するかは,タイムイベントの種類によって異なります.
待ち状態のタイムアウト・・・スタック上
周期ハンドラの起動・・・周期ハンドラコントロールブロック内
アラームハンドラの起動・・・アラームハンドラコントロールブロック内

タイムイベントを時刻順に管理するためのデータ構造が必要です.これについては,後ほど解説します.

3-4.カーネル共通のデータ構造

.轡好謄狆態を保持するためのデータ構造
システムの状態を何らかの方法で,保持する必要があります.

実行中のコンテキスト :タスクなのか非タスクか 
CPUロック状態 :CPUロックか解除されてるか,いないか
割りこみ優先度マスク

これら3つは,ターゲット依存です.CPUのステータスレジスタとして持っている場合は,新たに変数を設ける必要はありませんが,持っていない場合は変数で持つ必要があります.また,その持ち方はターゲット依存となります.

・ディスパッチ禁止状態<disdsp>
・ディスパッチ保留状態<dspflg>

この2つは,カーネルで決めている状態です.それぞれdisdsp dspflg という変数で管理します.

▲織好スケジューリングに用いるデータ構造 
これらはプログラム中で,タスクスケジューリング管理がしやすいように定義した補助変数です.

・実行状態のタスクを示す変数 <p_runtsk>
p_はポインタを意味し,p_runtskは,実行状態タスクのTCBへのポインタとなります.タスクの実態は,コントロールブロックになりますので,現在実行中のタスクは?という質問には,今実行中のタスクのコントロールブロックとなり,つまりp_runtskとなります.

・実行可能状態を管理するためのキュー <ready_queue>
実行状態のタスクも,実行可能状態のタスクと一緒に,このキューに入れています.

・レディキューの中で最高優先順位のタスクを示す変数<p_schedtsk>
言い換えれば,本来なら動くべきタスクです.
この変数もわざわざ変数として用意しなくても,レディキューから得られる値ですが,スケジューリングの際オーバーヘッドを減らすために,この変数を設けています.
本来ならp_schedtskと,p_runtskは一致するはずです.しかし,ディスパッチ保留状態の時は,p_schedtskのほうが優先度が高いのに,p_runtskが動いている.といったように両者が一致しないことがあります.ディスパッチ保留でない時,両者は一致しています.

システム時刻管理のためのデータ構造
現在システム時刻を管理する変数(current_time)

ぅ織ぅ爛ぅ戰鵐箸鮖刻順に管理するためのデータ構造
一番簡単な方法は,1列のキューでの管理です.この方法ですと,現時刻とキュー先頭のイベント時刻を比較して処理をすることになります.しかし,タイムイベントはタスクの数が増えてくると,どんどん増えてくる可能性があります.キューでの管理だと,タイムイベントの適切な挿入位置を探すのにかかる時間オーダーは,O(n)で,データ数が増えるとそれに比例して時間がかかってしまいます.そこでASPカーネルでは,ヒープ構造を採用しています.このデータ構造ですと,かかる時間オーダーはO(log n)となり,データ数が多くなるとこちらの方が有利になります.実際に,タイムイベントの数が,20−30くらいになると,ヒープ構造の方が有利です.
.織好コンテキストの場合
タスクごとにスタック領域を1つ持ちます.これはコンフィギュレータが静的に確保します.タスクが呼び出したサービスコールもタスク用のスタック領域を使用します.カーネル用のスタックは持っていません.もし,カーネルは特権モードで動き,アプリケーションはユーザーモードで動くという仕組みであれば,カーネル用とアプリケーション用のスタックを別々に持つ必要があります.ASPは両方とも特権モードで動くのでスタックも分けていません.

非タスクコンテキストの場合
割込みハンドラ用のスタック領域はシステム全体で1つ持つ仕様です.割込みが発生すると,タスク用のスタックから切り替えて使用します.
他の方法として,タスクのスタックの延長上に,割込みで使用するスタック領域を確保する方法も考えられます.タスクで使用しているスタックの下にそれぞれ割込みで使用するスタック(例えば100バイト)を用意すると仮定します.すると,10本タスクがあると,10×100バイトの領域が別に必要となります.必要領域を減らすという観点から,割込みハンドラに切り替わるとスタックも切り替える方針にしています.



いよいよ次回からは,カーネルのソースコードを見ていきます.お楽しみに・・・