1999年‐2002年 佐々木 芳
始めに
本書は1994、1995年ころH16、V25プロセッサを対象にμITRON3.0を実装したときの経験を元に1999年に整理したものです。今回ホームページに公開するに当たって一部修正しました。
本書では、プログラマから見たμITRON仕様準拠OSを実装レベルから概観します。以下、このμITRON仕様準拠OSのことを単に「MyKernel」と呼びます。MyKernelにはどの製品でも持っていると思われる最低限の機能をアセンブラで実装しました。
参考文献
μITRON3.0標準ハンドブック 監修 坂村健、編集/発行 社団法人 トロン協会、制作/発売 パーソナルメディア
最初におさらいの意味でプロセッサがプログラムを実行するメカニズムを整理します。次にマルチタスクの仕組みを考えます。
一般に、プロセッサ(CPU)で実行中のプログラムはメモリ上の以下の領域から構成されています。
コード領域
データ領域
スタック領域
プログラムはプロセッサの以下の構成要素を使い実行されます。(呼び方はプロセッサ各製品より違いますが、だいたいどれもこのような機能を持っています)
プログラムカウンタ(PC)
スタックポインタ(SP)
ステータスレジスタ(フラグレジスタ)
汎用レジスタ
特殊レジスタ
図 1‑1 実行中のプログラム
通常、Cコンパイラが生成するオブジェクトコード(機械語)にはプロセッサの特殊レジスタを使用する命令は含まれていませんが、それ以外のプロセッサの構成要素は独占して使用することを前提にしたコードとなっています。本書では、「シングルタスクのプログラム」とはこのようにプロセッサの構成要素を独占するプログラムと理解します。
MyKernelの最も重要な役割はタスク管理です。「タスク」という言葉が何を意味するかはOSにより違うので、まずMyKernelにおけるタスクの説明をします。
タスクとは、ユーザプログラムの実行制御の単位です。異なるタスクのプログラムは並列処理されると考えてさしつかえありません。MyKernelはマルチタスクOSですが、このことは複数のプログラムを擬似的に並列実行させることを意味しています。いいかえると、MyKernel上の各タスクはプロセッサを独占して使用しているように実行されます。タスクは仮想的なプロセッサを持っていると考えることもできます。
この仮想的なプロセッサは、実際のプロセッサのすべての機能を持っている必要はなくユーザプログラムを実行するのに必要な機能のみを持っていれば十分です。もし高級言語でユーザプログラムを作成する場合には、言語コンパイラが出力するオブジェクトコード(機械語)を実行できる機能だけあればよいことになります。具体的には、プログラムカウンタ、汎用レジスタ、フラッグレジスタ、スタック領域などを操作する機能です。各タスクのプログラムが連続して正しく実行されるためにはこれらの情報がそれぞれのタスクのプログラムに固有で、他のタスクのプログラムの実行状況にかかわらず一貫性があることが必要となります。この各タスクのプログラムの実行に必要なこれらの情報をタスクの「コンテキスト(文脈、前後関係)」と呼びます。仮想的なプロセッサはこれら以外の機能、例えば割込み処理機能やOS構築用の機能を持つ必要がありません。割込み処理については後で述べることにします。
実際のプロセッサは1つのプログラムカウンタを持ち、それが指し示す番地に格納されている命令(機械語)を逐次実行します。したがって、ある時点でプロセッサが実行しているのは、1つのタスクのプログラムです。そこで、MyKernelはタスクごとにコンテキストを管理することによりタスクのプログラムの逐次処理と実行タスクの切り替えを行います。実行中のタスクのコンテキストをメモリに保存し、新しく実行するタスクのコンテキストをメモリから読みだしプロセッサに与えるとプロセッサは新しいタスクの実行を始めます。
MyKernelでは、複数のタスクプログラムの実行を制御するためにタスクごとに「タスク管理ブロック」をメモリ上に確保しています。タスク管理ブロックにでは、以下のようなタスクの実行に関する情報を保持しています。
開始アドレス
スタック開始アドレスとスタックサイズ
タスク優先度
タスク状態
コンテキスト
図 1‑2 タスク切り替えの様子(タスク1→タスク2)
このように、MyKernelはタスクという単位でプログラムを擬似並列実行します。並列実行する複数のタスクが協調して動作するためにはタスク間の同期・通信機能が必要になります。この機能はプロセッサにはないのでMyKernelによって提供されます。これらの機能は「システムコール」という形でタスクのプログラムから利用できます。
プロセッサの実行するタスクが切り替わるのは、タスクの状態を遷移させるシステムコールが発行された時や割込みが発生した時です。プロセッサの実行するタスクが切り替わることを「タスクスイッチ」と呼び、これを実現するMyKernel内のメカニズムをディスパッチャと呼びます。ディスパッチャは実行中のタスクのコンテキストを保存し、新しく実行するタスクのコンテキストを実際のプロセッサに戻して新しいタスクの実行を再開させます。MyKernelではコンテキストをタスクのスタックの最上位に格納します。(386のようにタスクの概念を持つプロセッサの場合はタスク切り替え専用の命令があるのでタスク管理ブロックの一部にコンテキストを保存することもあります。)
実行可能なタスクが複数存在した場合に、MyKernelによるタスクの実行順序(次にどのタスクを実行するか)を制御することをタスクの「スケジューリング」と呼びます。MyKernelでは、タスクに与えられる「タスク優先度」(符号付き整数)をもとにした優先度ベースのスケジューリング方式を採用しています。数の小さい方が高い優先度を表します。MyKernelでは必ず優先度の高いタスクから実行され、優先度の高いタスクが実行中である限り、それより低い優先度のタスクは全く実行されません。
個々のタスクを区別するために、MyKernelではID番号(符号付き整数)を用います。これを「タスクID」と呼びます。各タスクは「コンフィグレーション」時に生成を指定し、システム起動時に作られます。
MyKernelでは、タスクの実行状態を表すために「タスク状態」を定義し、このタスク状態を遷移させるシステムコールを明確にしているので、プログラマはどのタスクを実行させるかを必要に応じて制御できます。MyKernelの各タスクの状態は以下の状態のどれかになっています。
l
実行(RUN)状態
現在そのタスクは実行中であるという状態。そのタスクがプロセッサを占有している状態。別な言い方をすると、プロセッサがそのタスクのプログラムを実行している状態。プロセッサは1つを想定しているので1つのタスクしかこの状態になりません。
l
実行可能(READY)状態
タスク側の実行の準備は整っているけれど、そのタスクよりも優先度が高い(同じ場合もある)タスクが実行中であるため、そのタスクは実行できないという状態。実行可能状態のタスクが複数個あった場合、これらのタスクはプロセッサが空くのを待って待ち行列を作ります。これを「レディキュー(Ready Queue)」と呼びます。
l
待ち(WAIT)状態
そのタスクを実行できる条件が整わないために、実行ができない状態。すなわち、何等かの条件が満足されるのを待っている状態。待ち状態からの実行再開は中断した場所から行われるので、タスクのプログラムの連続性は保たれます。
MyKernelの起動時の動きは以下のようになります。
@
コンフィグレーションで指定された全タスクをレディキューに並べます。
A
そして、優先度の最も高いタスクを実行状態にします。
B
そのタスクが自ら待ち状態になるシステムコールを発行した時点で、次の優先度のタスクが実行状態になります。
C
以後優先度の高いタスクから順番に実行状態になります。
MyKernelで定義するタスク状態と、状態間の遷移を引き起こすシステムコールを次の図に示します。
図 1‑3 タスク状態遷移図
タスクのスケジューリングとは次に実行するタスクを決めることです。MyKernelでのタスクのスケジューリングは、タスクの優先度を使って行います。各優先度ごとの実行可能状態のタスク管理ブロックを双方向リンクでつないで管理します。新たに実行可能状態になったタスクはこのリンクの最後尾に並びます(First
Come First Service方式)。この様子を、図1−4、図1−5の例で説明します。
図1−4は、優先度1のタスクA,優先度3のタスクE,優先度2のタスクB,C,Dをこの順番に実行可能状態にした後のレディキューの状態です。現在実行可能状態にある最も優先度の高いタスクはタスクAなので、タスクAが実行されています(タスクAが実行状態)。この状態では、優先度の低いタスクB,C,D,Eは全く実行されません。
この状態で外部割込みが発生した場合、割込みハンドラから戻る時点で再度スケジューリングが行われます。しかし、割込みハンドラ内でタスクAよりも優先度の高いタスクが実行可能状態に移っていない限り、割込みハンドラから戻った後もやはりタスクAが実行されます。
この状態でタスクAが自ら待ち状態になるシステムコールを発行すると、タスクAはレディキューから外されます。そして、レディキューの中の先頭のタスクBが実行状態に移ります(タスクBがディスパッチされます)。この時のレディキューの状態を図1−5に示します。
一旦待ち状態に移行したタスクが、再度実行可能状態に移行した場合には、そのタスクは、その優先度のレディキューの最後尾に付きます。
MyKernel内のタスクをディスパッチするメカニズムを「ディスパッチャ」と呼びます。
図 1‑4 始めのレディキューの状態
図 1‑5 タスクAが待ち状態になった後のレディキューの状態
MyKernelでは、ユーザプログラムは以下に示すタスク操作が行えます。
タスク管理
・ディスパッチを禁止する(Disable
Dispatch)
・ディスパッチを許可する(Enable
Dispatch)
・タスク優先度を変更する(Change
Task Priority)
・タスクのレディキューを回転する(Rotate
Ready Queue)
・他タスクの待ち状態を解除する(Release
Wait)
・自タスクのIDを得る(Get
Task Identifier)
タスク付属同期
・自ら起床待ち状態に移行する(Sleep
Task)
・待ち状態のタスクを起床する(Wakeup
Task)
・タスクの起床要求を無効にする(Cancel
Wakeup Task)
MyKernelでは、ユーザプログラムはタスク間の同期・通信として以下の機能が使えます。
・セマフォ(Semaphore)
・イベントフラグ(EventFlag)
・メイルボックス(Mailbox)
MyKernelでは、計数型のセマフォが使用できます。資源獲得値や資源返却値は1に固定されています。計数の最大値は32767です。セマフォで待つタスクはそのセマフォの待ち行列に並びます。待ち行列での並び方はFIFOです。個々のセマフォはセマフォIDで区別します。
図 2‑1 セマフォの構造
MyKernelでは、ユーザプログラムは以下に示すセマフォ操作が行えます。
・セマフォに対する信号操作(Signal
Semaphore)
・セマフォに対する待ち操作(Wait
on Semaphore)
・セマフォ状態を参照する(Get
Semaphore Status)
MyKernelでは1ワード(16ビット)のイベントフラグが使用でき、指定ビットがセットされるのをOR条件やAND条件で待つことができます。イベントフラグで待つタスクはそのイベントフラグの待ち行列に並びます。複数のタスクが同じイベントフラグで待つことができます。待ち行列での並び方はFIFOです。個々のイベントフラグは、イベントフラグIDで区別します。
図 2‑2 イベントフラグの構造
MyKernelでは、ユーザプログラムは以下に示すイベントフラグ操作が行えます。
・イベントフラグをセットする(Set
Event Flag)
・イベントフラグをクリアする(Clear
Event Flag)
・イベントフラグを待つ(Wait
Event Flag)
・イベントフラグ状態を参照する(Get
Event Flag Status)
MyKernelではメイルボックスが使用できます。タスク間でメイルボックスを介してメッセージの交換ができます。タスクは指定したメイルボックスへメッセージを送り、指定したメイルボックスからメッセージを受け取ることができます。送信されたメッセージは受け取りタスクがいないとメッセージキューに挿入されます。メッセージキューでのメッセージの並び方はFIFOです。
メッセージを受取に行きメッセージがないとタスクはそのメイルボックスの待ち行列に並びます。待ち行列での並び方はFIFOです。個々のメイルボックスは、メイルボックスIDで区別します。
図 2‑3メイルボックスの構造
MyKernelでは、ユーザプログラムは以下に示すメイルボックス操作が行えます。
・メッセージを送信する(Send
Message to Mailbox)
・メッセージの受信を待つ(Receive
Message from Mailbox)
・メイルボックスの状態を参照する(Get
Mailbox Status)
これまで述べたように、MyKernelのタスクのプログラムを実行する仮想的なプロセッサには割込みを処理する機能は含まれていません。割込みプログラムの起動には、MyKernelは介在せず実際のプロセッサの割込み処理機能により行われます。したがって、割込み処理プログラムはタスクのプログラムとは独立したプログラムとして書く必要があります。割込みはタスクの実行と無関係に発生するので、割込み処理プログラムはタスクのコンテキストと無関係に起動されます。そこで割込み処理プログラムとタスクのプログラム間の同期・通信もシステムコールを使って行います。MyKernelでは、割込み処理プログラムを「割込みハンドラ」としてコンフィグレーション時に指定しておく必要があります。また、MyKernelでは、多重割り込みをサポートしていません。
MyKernelのもとで動くタスクのプログラムの動きは、タスクの状態遷移図を見て追っていくことができます。しかし、MyKernelを使用するプログラマは割込みハンドラもプログラミングします。この場合、非タスク部(タスク以外の部分)を実行している間のシステムの状態についても考慮しておかないと、正しいプログラミングができません。そこで、μITRON仕様では、システムの実行中の状態を以下のように分類します。
システム状態
1)非タスク部実行中
・システムコール実行中
・タスク独立部実行中(割込みハンドラ実行中)
2)タスク実行中
システムの実行状態はこのように分類されますが、重要なことは、プログラマにはシステムコール実行中の内部状態を考慮しなくてもよいということです。つまり、システムコールの内部で実行タスクが切り変わって別なタスクを実行した後再びそのタスクが実行状態になった場合でも、そのタスクのプログラムに注目するとその実行シーケンスはタスクの切り換えがない場合と変わりません。このためプログラマはシステムコールの内部処理に関係無くプログラミングできます。
タスク独立部(割り込みハンドラ)の特徴は、タスク独立部に入る直前に実行中だったタスクを特定することが無意味であり、「自タスク」の概念が存在しないことです。タイマなどの割込みは、タスクの実行とは全く無関係に発生するため、その時点での実行状態のタスクはいつも同じとは限りません。また、タスク独立部ではタスクの切り換え(タスクディスパッチ)は起きません。タスクディスパッチが必要になっても、それはタスク独立部を抜ける(タスクのコンテキストに戻る)まで遅らされます。μITRON仕様では、これを「遅延ディスパッチ(delayed dispatching)」の原則と呼びます。したがって、タスク独立部からは、待ち状態に入るシステムコールや暗黙で自タスクを指定するシステムコールは発行できません。
遅延ディスパッチは具体的にいうと、割込みハンドラ内でタスクスイッチが起こるようなシステムコールを発行しても、その時点ではタスクスイッチは起きずに割込みハンドラの最後に発行するret_intシステムコール内でタスクスイッチが発生すると言うことです。割込みハンドラ内と違いタスク内ではタスクスイッチが起きるようなシステムコールを発行するとそのシステムコールの内部でタスクスイッチが起こります。
図 3‑1 割り込みハンドラとタスクスイッチ
図3−1では、タスクAの優先度はタスクBより高いと仮定しています。
MyKernelでは、ユーザプログラムは以下に示す割込み管理機能が使えます。
・割込みハンドラから復帰、実行タスクを再スケジュール(Return
from Interrupt Handler)
・割込みとディスパッチを禁止する(Lock
CPU)
・ 割込みとディスパッチを許可する(Unlock CPU)
・割込みマスク(レベル、優先度)変更
・ 割込みマスク(レベル、優先度)参照
μITRON仕様ではタスクなどシステムコールの操作対象となるものを総称してオブジェクトと呼びます。MyKernelのオブジェクトには、タスクのほかにイベントフラグ、セマフォ、メイルボックスなどがあります。
各オブジェクトは、ID番号(タスクID、イベントフラグID、セマフォID、メイルボックスID)で区別されます。MyKernelでは、各オブジェクトはコンフィグレーション時に指定されたそのID番号と生成情報を基にMyKernelの起動時に作られます。オブジェクトのID番号として1〜[最大値]の正の整数の値が指定できます。
オブジェクト 識別子
タスク タスクID
イベントフラグ イベントフラグID
セマフォ セマフォID
メイルボックス メイルボックスID
オブジェクトの生成に必要な情報を以下に示します。
タスク生成情報
タスクスタートアドレス(task)
初期優先度(itskpri)
スタックサイズ(stksz)
セマフォ生成情報
セマフォの初期値(isemcnt)
イベントフラグ生成情報
なし
メイルボックス生成情報
なし
ユーザプログラムは、このID番号を使って各オブジェクトを区別します。オブジェクトを操作するシステムコールを発行する場合に、そのオブジェクトのID番号をパラメータとして指定します。
MyKernelでは、インタバルタイマの割込みを使ったシステムクロックの機能をサポートします。システムクロックは、MyKernel内部では48ビットのカウンタです。インタバルタイマの割込みが入るたびにカウンタをプラス1します。インタバルタイマの間隔とそれをシステムクロックとして使うかどうかはコンフィグレーション時に指定します。
MyKernelでは、システムクロックを組み込むとユーザプログラムは以下の機能が使えます。
・システムクロックの値を設定する
・システムクロックの値を読み出す
・時間を指定して待ち状態に入る
MyKernelでは、待ち状態に入るシステムコールにおいてタイムアウトの指定ができます。この機能はシステムクロックをコンフィグレーション時に組み込んだとき使用できます。タイムアウト指定でで待ち状態になったタスクは、タイムアウト管理ブロックをタイムアウトキューに追加します。システムタイマの割込みハンドラでこのタイムアウトキューを毎回調べてタイムアウトに達したタスクは待ち状態の解除が解除されます。
MyKernelの場合、タスクを起床するシステムコールwup_tsk(Wakeup Task)が既に発行されているタスクに対して、もう一度wup_tskを発行しても、エラーにはならず、その要求が保持されます。これを要求のキューイングと呼びます。この時、wup_tskの対象タスクは、待ち状態移行のシステムコールslp_tsk(Sleep
Task)を2回実行してもまだ待ち状態にはならず、3回目のslp_tskの実行でやっと待ち状態になります。(キューイングの最大値は32767)
待ち状態に入る可能性のあるシステムコールで条件が満たされなくても待ち状態に入りたくない場合には、タイムアウトの時間指定をTMO_POL(0)としてシステムコールを発行します。
MyKernelでは、タスク独立部(割込みハンドラ)から発行するシステムコールとタスク部から発行するシステムコールを分離していません。同じシステムコールをタスク部でもタスク独立部でも使えるほうがプログラミング上は便利です。
MyKernelでは、メモリプール機能はサポートしません。RAM領域のうちMyKernelが使用する領域(リンク時に決定)以外の部分は、ユーザが自由に使用できます。MyKernelが使用する領域は、コンフィグレーション時に指定した範囲で動的に変化しません。
実行中のμITRON仕様OSのスペックの概要、OSの形式番号、バージョン番号などを得るシステムコールがあります。