(第39回)非コンテキスト状態とは?

2009/02/01

あなたは 人目のお客様です.

前回までで,これでリアルタイム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() などのシステムコールに相当する処理を 呼び出すためのサービス関数を追加する.これをここではシステムコールに対して 「サービスコール」と呼ぶことにしよう.

ソースは以下のような感じ.

(2009/04/10 ライセンスに関する文書として,KL-01とLICENSEを追加. 詳しくは第43回を参照)

前回からの修正点は,いつもどおり diff.txt を参照してほしい. システムコールが kz_*() であるのに対して,サービスコールの書式は kx_*() と した(接頭語 kx にはとくに理由はない.なんとなく).とりあえず用意したのは, 割り込みやディスパッチ前ハンドラとして必要かな〜と思われる以下の4つ.

これらはそれぞれ 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 を再計算している.


メールは kozos(アットマーク)kozos.jp まで