(注意)このブログは本家のほうの文章部分のみの転載です.ソースコードの配布,画像などについては本家のほうを参照してください.文章中のリンク先は面倒なのですべて本家のほうに変換してしまっているのでご注意ください.

前回までで,これでリアルタイムOSとそれなりに言えるのでは...?というところまでKOZOSを持っていったが,実はまだちょっと改良点がある.なのだけど,今回はその改良の前準備として,「サービスコール」というものを追加したい.

もともとKOZOSのサービスを呼び出すためには kz_send() や kz_recv() といった名前の「システムコール」を利用するのだが,これの実体は SIGSYS シグナルだ.で,これは実ハードウエアのシステムコール割り込みなどのソフトウエア割り込みを模している.

KOZOSの設計方針として,カーネルの内部処理(割り込み処理やシステムコール処理など)の最中には割り込み禁止にしてある.この理由は第32回で説明しているのだが,ある割り込みが入って割り込み処理をしている最中に別の(もっと優先度の高い)割り込みが入り,さらにその優先度の高い割り込みが(処理用スレッドにメッセージを投げて)スレッドに後続の処理を依頼した場合,つまり割り込みがネストした場合(この場合,割り込み処理はカーネル用スタックを(さらに積んで)利用することになる)に,処理用スレッドは,もともと処理していた優先度の低い割り込みの処理が完了しないと動作できない.なのでたとえスレッドの優先度を高くしたとしても,割り込み処理が残っている限りは応答時間の見積もりができないため,そのリアルタイム性が確保できない.

これの解決策として,ほんとうにリアルタイム性が必要な処理はすべて割り込みの延長で行う(KOZOSならば,kz_sethandler()で設定した割り込みハンドラですべて行う)という解決案もあるにはある.ただこの場合には,スレッドの処理はリアルタイム性をまったく保証できない(上で説明した割り込みのネストが発生した場合に,スレッドの処理が開始されるまでの時間が見積もれないから)ということになる(このへんのことは,第32回も参照してほしい).まあ実際にはすべての割り込みがかかった場合の最悪値として見積もることはできるにはできるが,それじゃあちょっとあんまりだ.

これの根本的原因は,割り込み処理はスレッドではなくシグナルハンドラの延長で行われるため,スレッドのようなコンテキストを持たないためだ.このような状態を「非コンテキスト状態」と呼ぶ...のだと思う.KOZOSの場合は非コンテキスト状態というのは,その処理を実行するためのkz_thread 構造体が存在しない,ということだ.なので非コンテキストの処理は,スレッドのように状態を退避/復元することができない.つまり,処理をスリープさせてあとで再開したりといったことはできない.割り込みハンドラの処理などは,非コンテキスト処理の代表的なものだ.

非コンテキスト処理はスリープさせることができないため,プリエンプティブという動作とは相性が悪い(カーネル用スタックもネストさせることで処理をネストさせることはできるが,スリープさせることはできない).ということで,KOZOSでは割り込み処理中の割り込み(割り込みのネスト)は禁止してある.つまり,割り込みハンドラなどの非コンテキスト処理中は,割り込み禁止になっているということだ.ていうか,OSの内部処理はすべて割り込み禁止になっている.こーいうのを「OS内部は割り込み禁止で走っている」というふうに言うことが多いようだ.

このため割り込み応答性能は若干落ちることになる(前の割り込み処理が終らないと,次の割り込みを(例え優先度が高くても)受け付けられない).まあスレッドのリアルタイム性を確保できないよりはマシかなーと思うので,現在はそーいう設計になっている.

具体的にいうと,thread_start()の以下の部分で割り込み禁止にしてある.

sa.sa_mask = block;

sigaction(SIGSYS , &sa, NULL);
sigaction(SIGHUP , &sa, NULL);
sigaction(SIGALRM, &sa, NULL);
sigaction(SIGBUS , &sa, NULL);
sigaction(SIGSEGV, &sa, NULL);
sigaction(SIGTRAP, &sa, NULL);
sigaction(SIGILL , &sa, NULL);

上記処理によって,(システムコールを含む)シグナル発生時には block で指定したシグナルがマスクされる.この結果シグナルを受け付けなくなるため,割り込み禁止状態になる.つまり割り込み処理中にかかわらず,システムコールなどのOS内部の処理をしている間は,割り込み禁止状態になっている.(システムコールの実体は SIGSYS で割り込みの一種なので,これは当然のことである)

ちなみにこの「OSの内部処理中は割り込み禁止」というつくりは,TOPPERSでもそのようになっているという話を聞いた.まあ詳しくは知らないのだけれど.(TOPPERSではまたべつの理由があるのかもしれない.詳しくはしらない)

ただ現状のKOZOSでは,ちょっと例外がある.というのは,kz_sethandler() で設定した割り込みハンドラの処理だ.本来割り込みハンドラでは,割り込みの受け付けを行い,当該の処理スレッドにメッセージなどで通知する,という動作を行う.難しい処理は割り込みハンドラでは行わずに,処理スレッドが行うべきだ.というのは割り込みハンドラが重い処理を行うと,割り込みの応答性の悪化に直結するからだ.リアルタイムOSではそれどころか,リアルタイム性の保証ができるか,という問題にもなってしまう.というのは,KOZOSのように割り込み処理中の割り込みを許していない場合,その割り込み処理が終らないと次の割り込みを受け付けられないため,割り込み処理はその処理時間を見積もれなければならないからだ.(このためキュー検索などは,割り込み処理では*してはいけない*)

なので割り込みハンドラからもメッセージ送信処理が呼べる必要がある.つまり,kz_send() 相当の処理が呼べる必要があるのだが,さっきまでの話では「OSの内部処理では割り込み禁止」ということになっている.これはソフトウエア割り込みも含むので,システムコールは発行できないということだ.割り込みハンドラはOSの内部処理(割り込み処理)の延長で呼ばれるので,当然,システムコールは発行できないということになる.

この対策として,これは第32回で行われた修正なのだが,現状のKOZOSでは外部割り込みの処理(extintr_proc())の内部で intrcontext というコンテキストと block_sys というシグナルマスクを利用することで,SIGSYSのマスクを一時的に解除して,システムコール発行を可能にしている.実際に第32回の extintr はkz_sethandler() で設定した割り込みハンドラ(extintr_handler())の内部でkz_send() を呼び出してメッセージ送信を行っている.

で,この作りにはちょっと問題がある.まず,システムコール発行可能にするために割り込みに(たとえ,SIGSYSのみであろうと)穴を開けるというのはなんだかな~という気がする.次に,システムコールの発行後はそのままOSがスケジューリング処理とディスパッチ処理に入ってしまい,システムコールの呼び出し元の割り込みハンドラには処理は戻ってこない.なので,システムコールは呼び出したらもう処理は戻らないものとして,割り込み処理の最後でなければ行えない(システムコール呼び出しの後になにか処理を書いておいても,実行されない).なので,例えばシステムコールを2回呼び出すようなことはできない.このため,たとえばメッセージ用のメモリを獲得してからメッセージ送信とか,2つのスレッドにメッセージ送信とかいったことはできないことになる.

あともうひとつ問題点がある.第36回でスレッドのディスパッチ前に呼ばれる関数の登録サービスとして kz_precall() というシステムコールを追加しているが,ここにはSIGSYS に穴を開ける処理が無い(ていうか気がつかなかった).なので,割り込みハンドラからはシステムコールは呼べるが,kz_precall() で設定したハンドラからはシステムコールは呼べないということになっている.どちらもOSの内部処理の延長で呼ばれるので非コンテキスト処理なのだが,なんか対称的でなくてよろしくない.

で,これらの解決策として,kz_send() などのシステムコールに相当する処理を呼び出すためのサービス関数を追加する.これをここではシステムコールに対して「サービスコール」と呼ぶことにしよう.

ソースは以下のような感じ.前回からの修正点は,いつもどおり diff.txt を参照してほしい.システムコールが kz_*() であるのに対して,サービスコールの書式は kx_*() とした(接頭語 kx にはとくに理由はない.なんとなく).とりあえず用意したのは,割り込みやディスパッチ前ハンドラとして必要かな~と思われる以下の4つ.
  • int kx_wakeup(int id);
  • int kx_send(int id, int size, char *p);
  • void *kx_kmalloc(int size);
  • int kx_kmfree(void *p);
これらはそれぞれ kz_wakeup(), kz_send(), kz_kmalloc(), kz_kmfree() に相当する.ただしこれらはOSのシステムコール内部処理を(関数呼び出しによって)直接コールするので,非コンテキスト処理(つまり,kz_sethandler() か kz_precall() によって登録されたハンドラ関数)からしか呼び出してはならない.もしも通常スレッドから上記サービスコールを呼び出してしまうと,サービスコールの処理中に割り込みなどが入った場合(スレッドの延長で呼ばれているので,これは十分に有り得る)に,排他の問題が発生する.

まあ実装としては kz_send() などの内部で非コンテキストかスレッドかを調べて適切に処理(非コンテキストならば関数を直で呼び出して,スレッドならばSIGSYSを発行する)することで,kz_send() と kx_send() に区別せずに kz_send() ひとつにまとめてしまうこともできるのだが,まあシステムコールとサービスコールの違いをはっきりとさせたほうが混乱しなくていいかなーと思い,別立てとすることにした.ちなみにμiTRONでも,非コンテキスト処理から呼び出すサービスと,有コンテキスト処理(つまり,スレッド)から呼び出すサービスははっきりと区別されているようだ.

ちなみに排他に関してだが,サービスコールは非コンテキスト処理から,ていうかOSの内部処理の延長で呼ばれるので,割り込み禁止になっている.よってサービスコールの処理中の排他などは考えなくてよい.

また,サービスコールは非コンテキスト処理から呼ばれるので,コンテキストが必要な処理,つまり kz_sleep() や kz_recv() などのような待ち合わせを行う処理は実装できない.まあ割り込みハンドラなどの内部でスリープとかメッセージ受信が必要なことは無い(そのようなことが必要な処理は,スレッドになっているはず)し,ハンドラの内部ではスレッドのwakeupと,スレッドの優先度変更(これはまだ未実装)と,メッセージ送信と,あとメッセージ送信にメモリが必要な場合には,そのメモリの獲得くらいができれば十分なので,これはこれで問題は無い.

ついでに,kz_precall()による関数呼び出し後に,再度 schedule() を呼び出して,再スケジューリングされるように処理を追加している.これをやらないと,kz_precall() で設定したディスパッチ前ハンドラから kx_send() によってなんらかのメッセージを投げたあとに,(メッセージを投げたことによって)もっと優先度の高いスレッドがレディー状態になったとしても,そっちが動作できない(kz_sethandler()により設定した割り込みハンドラの場合は,ハンドラ呼び出し後にschedule() が呼ばれるようにもともとなっていたので,この問題は無い).あと現状のKOZOSのつくりだと,サービスコールによっては,カレントスレッドを指す変数 current が書き換えられる場合がある.まあこれは直そうと思えば直せる気もするが,修正がめんどいので,schedule() を呼び出して current を再計算している.