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

KOZOSの改良なのだけど,まずは割り込みまわりを大幅に改造したい.

現状でKOZOSは,割り込み処理用に子プロセスを起動して,ソケットの select() を子プロセスにまかせている.ただしソケットは fork() 時に引き継がれるので,新しくソケットを生成した場合には,そのソケットは子プロセスから見れない.この対策として,ソケットを開くたびに子プロセスを終了,再起動している.

また,ソケットの生成や操作は基本的に各サービススレッド(telnetdとか)が勝手に行っており,子プロセスは select() 待ちと受信時の通知のみ行っている.accept()に関しても,子プロセスは通知をするだけで,実際の accept() はデーモンが各自行っている.まあ疑似OSという考えで,ソケットまわりはあまり深く考えずに実装してしまったのでこんなことになっているのだが,ちょっとあんまりな作りだ.

そもそもextintrや子プロセスは,ハードウエアの割り込み処理を疑似的に実現するのが本来の目的なので,ソケット周りの処理はすべて子プロセスにまかせて,各サービススレッドは文字列の受信割り込みと実際の送受信くらいが行えればよい.つまり,子プロセスが単なるシリアルコンソールのように見えてくれると,それっぽくていい感じだ.

ということでソケットのオープンやaccept(),select()類はすべて子プロセスにまかせるように改造する.各サービススレッドは,子プロセスから受信文字を受け取ったり,子プロセスに送信処理を依頼することで,文字データの送受信を行う.(実際には子プロセスとのやりとりのために,extintrスレッドが中継動作をしている)

ソケット関連の処理はすべて子プロセスに閉じて行われるので,各サービススレッドは,外部とのやりとりがソケットで行われていることは意識しない.つまり,ソケットでなくシリアルを利用するように子プロセスを書き換えれば,サービススレッド側は何の変更もなく,telnet接続からシリアル接続に変更することができる.

子プロセスは文字の送受信処理と割り込み発行を行うハードウエアを模している.通常の組み込みOSで,一番手軽な入出力デバイスはシリアルコンソールだ.ソケット処理をすべて子プロセスにすべて任せ,ソケットを意識させないことで,シリアルデバイスを模しているわけだ.

で,子プロセスとどうやって通信するかなのだが,これは専用のソケットを開いてしまうのがいいだろう.(外部からの接続のためのソケットと,内部通信用のソケットの違いに注意.ここで言っているのは内部通信用のソケットのこと)

通常の組み込みOSだと,これは(おそらくメモリ上にマッピングされた)ハードウエア操作用のレジスタを読み書きすることになるのだが,KOZOSでは親プロセスと子プロセスの間でソケット通信することで,子プロセス(疑似シリアルデバイス)を操作する.

子プロセスは外部に対してソケットをオープンし,待ち合わせる.外部からTCP接続要求を受けた際(つまり,kozosに対してtelnetした際)には子プロセスは accept() する.また文字データを受信した際には,受信データの通知用ソケットを通じて親プロセスにデータ送信し,親プロセスに対して SIGHUP を通知する.親プロセスはSIGHUPを受けたら,なんらかのデータを外部から受信したとして,受信処理を行う.

つまりKOZOSでは,以下の表のように組み込みOSの動作を模していることになる.
組み込みOSKOZOS
シリアルケーブルによる接続TCP/IPによる子プロセスへの接続(telnet接続)
シリアルからの受信telnet接続からの受信
シリアルへの送信telnet接続への送信
受信割り込み子プロセスから親プロセスへのSIGHUP
レジスタリードによる受信データの読み込み内部通信用ソケットからの受信データ読み込み
レジスタライトによるデータ送信内部通信用ソケットへの送信データ書き込み


で,改造したソースは以下のような感じ.まず親プロセス側(KOZOS本体側)ではソケットは一切扱わないので,telnetd スレッドは command スレッドに変更.httpdは廃止.command スレッドでは,ソケット処理は行わずに extintr から通知された受信データを扱うように修正している.

さらに extintr.c は,上述した作りにごっそり書き換えている.扱うソケット数は決めうちで4個,ポート番号はこれも決めうちで12345~12348としている.外部からはこのポートに(telnetなどで)TCP/IP接続できる.

あとGDB利用を考えるとまたややこしいので,stubdはとりあえず廃止.

今回の修正はほとんど extintr に対するものとなっている.処理を簡単に説明すると,まず子プロセスの fork() 時には親プロセスとの間に連絡用のソケットを開く.ソケットはコマンドによる子プロセスの操作,受信データの通知,送信データの通知用に3種類開いている.これを子プロセスが接続管理する4ポートぶん開いているので,それだけでも12本のソケットをオープンしている.(さらに外部との接続待ちや接続,全体処理用などのためにソケットを開いている)

command スレッドは,使用したい外部ソケットの番号を指定してuコマンドをextintrスレッドに発行する(このコマンド発行の実体は,KOZOSのメッセージである).extintrスレッドはuコマンドを受信すると,コマンドの発行もとのスレッドをおぼえておいて,子プロセスからデータ受信した際に,受信ソケットに対応するスレッドに対して(KOZOSのメッセージで)データ送信を行う.

図にするとこんな感じ.

(初期化時)

サービススレッド extintr 子プロセス 外部からの接続
(commandスレッドなど) (外部からのtelnet接続など)
| | | |
| | socket() |
| | | |
| uコマンド | listen() |
kz_send()=========>| | |
| kz_recv() select()待ち |
| | |<-----------connect()
kz_recv()待ち kz_recv()待ち accept() |
| | |<------------>|
| | | TCP/IP接続確立 |
| | | |
| | select()待ち |
| | | |

---> は,ソケットによる接続や通信
===> は,KOZOSのメッセージ送信

(文字データ受信時)

サービススレッド extintr 子プロセス 外部からの接続
(commandスレッドなど) (外部からのtelnet接続など)
| | | |
kz_recv()待ち kz_recv()待ち select()待ち |
| | | |
| | |<------------send()
| | read() データ送信 |
| | | |
| |<------------write() |
| |<======------SIGHUP |
| kz_recv()(※1) | |
| read() 割り込み無効化(※2) |
| | | |
|<===========kz_send() select()待ち |
kz_recv() | | |
| kz_recv()待ち | |
| | | |
| eコマンド | | |
kz_send()=========>| | |
| kz_recv() eコマンド | |
| write()----------->| |
| | read() |
| | 割り込み有効化 |
| | | |
| | select()待ち |
| | | |

※1 kz_setsig() システムコールにより,シグナル受信は
メッセージによりextintrに通知される.
※2 データ受信の際に当該の外部ソケットの割り込み無効化し
(具体的には,select() 用の fd_set のビットを落とす),
eコマンドによる通知で再度割り込みを有効にする.

(文字データ送信時)

サービススレッド extintr 子プロセス 外部からの接続
(commandスレッドなど) (外部からのtelnet接続など)
| | | |
| kz_recv()待ち select()待ち |
| wコマンド | | |
kz_send()=========>| | |
| kz_recv() | |
| write()---------->| |
| | read() |
| | write()---------->|
| | | read()
| | | |

データの受信時には,受信した外部ソケットを select() 時の fd_set から落とすことで,一時的に受信検出できないようにする.これは実際のシリアルデバイスで,データの受信時に割り込みを一時的に無効化することを模しているつもり.処理スレッド(commandスレッド)側では受信データをリードしたらeコマンドを発行することで,受信割り込みの蓋開けをする.

で,extintr.c では以下の関数がそれぞれの処理を行っている.
  • intr_controller() ... 子プロセスでの処理(select()待ち,accept()処理,外部からのデータ受信処理とSIGHUP発行処理,外部へのデータ送信処理,e/dコマンドによる割り込み有効化/無効化処理)
  • extintr_handler() ... extintr の子プロセスからの割り込み受信処理(SIGHUP受けて子プロセスからのデータ受信してスレッドに転送する)
  • extintr_mainloop() ... extintr のサービススレッドからのコマンド受け付け処理(uコマンド(ソケット利用)の受け付け処理,e/dコマンド(割り込み有効化/無効化)の受け付け処理,wコマンド(データ送信)の受け付け処理)
  • extintr_main() ... 子プロセスを fork() して,ソケット生成する処理
うーんちょっとややこしいけど,以前よりはすっきりした構造になったような.

ハードウエアが行うような処理は子プロセスのほうにまとめたので,子プロセスを疑似ハードウエアとして扱うことができるようになっている.KOZOS側では,(操作する先がメモリマップドレジスタかソケットかの違いはあるが)基本的にはいわゆる普通のシリアルコントローラを扱うような感じで子プロセスを操作できる.このため,KOZOSをそのままどこかのハードウエアに移植して,本当のシリアルデバイス利用するような場合にも,extintr.c をシリアルデバイス利用に書き換えるだけで対処できるはずだ.

動作確認についてはめんどうなのでここでは省略するが,とりあえず確認はとれている.次の課題は割り込みの優先度づけかなあ...
(注意)このブログは本家 のほうの文章部分のみの転載です.ソースコードの配布,画像などについては本家のほうを参照してください.文章中のリンク先は面倒なのですべて本家のほうに変換してしまっているのでご注意ください.

いやー,いろいろ別の作業があって,更新が1年以上も開いてしまった.

で,KOZOSなんだけど,1年間ちょっといろいろ考えたのだが,割り込みまわりをもっとOSっぽくするのと,あといくつか改善というか新機能のアイディアがあるので,まあ徐々に試していきたい.

まず「割り込み」と「リアルタイム性」について考えてみよう.

最初に考えなければならないのは,以下のことだ.
  1. ある(優先度の高い)スレッドの動作中に,別の割り込みを可能にするか?
  2. ある割り込みのハンドラ実行中に,別の割り込みを可能にするか?
  3. あるシステムコールの実行中に,別の割り込みを可能にするか?
以下に,それぞれについて考えてみよう.

「ある(優先度の高い)スレッドの動作中に,別の割り込みを可能にするか?」

まず1についてだが,リアルタイム性を確保するならばスレッドに優先度をつけるだけではダメで,割り込みにも優先度をつけて,優先度の高いスレッドの動作中には優先度の低い割り込みは入らないようにしたい.つまり,割り込みをマスクしたい.

なぜかというと,割り込み発生時にはまず割り込みハンドラが起動するが,割り込みハンドラの内部では時間のかかる処理は行わないというのが鉄則なので,ハンドラは簡単な処理(受信バッファからのデータの吸い上げと割り込みの刈り取り等)のみ行って,あとは複雑な処理は処理用のスレッドにまかせたい.つまり割り込みハンドラは処理スレッドに(メッセージなどで)処理データを通知して,終了してしまいたい.で,あとは処理スレッドが処理を行えばいいのだけど,ここで別の(もっと優先度の低い)割り込みが入ると,処理スレッドが割り込まれて,処理の完了までのリアルタイム性を確保できない.

これを避ける方法のひとつとして,リアルタイム性の必要な処理はすべて割り込みハンドラで行う,という方法もあるかもしれない.(高優先度の割り込みハンドラ実行中に低優先度の割り込みハンドラが起動されないように,割り込み処理には優先度が必要だろう)

しかしそれではあんまりなので,対処としてとりあえず思いつくのは,割り込みにレベルをつけて,スレッドの優先度に対応させるという方法だ.割り込みを受け付けて,あるスレッドがディスパッチされて動作を開始するときに,そのスレッドに対応する割り込みレベルよりも低レベルの割り込みをマスクしてしまう,というものだ.

マスク処理は割り込みを管理している extintr スレッドが行う.このため extintr スレッドは最優先にする必要がある.というのは,割り込み発生時にはハンドラから処理スレッドに対して(メッセージによって)データが渡され,それによって処理スレッドが動作開始するが,このスレッドの動作開始(ディスパッチ)より前にマスク処理が行われないとまずいからだ.さらに extintr のマスク処理中にも割り込みを受け付けないように,extintr 実行中はSIGHUP を受け付けないようにシグナルの設定をする必要があるかもしれない.

こう考えると,extintr は割り込み管理スレッドともいうべきもので,実際に(汎用OS上でなく)実CPU上でKOZOSを動かすとしても,必須のスレッドだということになる.

ちなみに割り込みのマスクには,extintr の fd_set を利用すればよい.マスクしたいソケットのビットを fd_set から落として select() することで,そのソケットからの入力はマスクされる.つまり select() 用の fd_set が,割り込みマスクレジスタ&割り込みステータスレジスタのような役割になる.

「ある割り込みのハンドラ実行中に,別の割り込みを可能にするか?」

次に2についてだが,割り込み応答性能を向上させるには,割り込み処理中に,もっと優先度の高い別の割り込みを受けることを可能にしたい.つまり割り込みのネストを可能にしたい.

しかしこれにはちょっと問題がある.というのは,たとえばある割り込みを受けて,そのデータ処理はスレッドで行うとすると,スレッドのディスパッチが行われるまでデータ処理が行えないので,広い意味での割り込み処理が完了しないことになる.(割り込みハンドラはデータ送信して完了しているが,データ処理は完了できない)

ここでたとえば低優先度の割り込みの処理中に,高優先度の割り込みがネストして発生したとしよう.この場合,低優先度の割り込みハンドラは処理を中断して,高優先度の割り込みハンドラが実行される.で,データをメッセージによって処理用スレッドに送信するのだが,この後は中断された低優先度の割り込みハンドラの処理が(まだOSのスタックに残っているので)継続して実行される.で,この低優先度の割り込みハンドラの処理が終らないと,スレッドはディスパッチされない.ということはリアルタイム性を確保できない.

この根本対策としては,OSのカーネル内処理自体をスレッド化して,割り込みハンドラはOSのコンテキストで実行するのではなく,スレッド化してやる,という方法が考えられる(キュー操作などで排他したい箇所は,一時的に割り込み禁止にする.またシステムコールを呼び出したいときには,OSのシステムコール処理用関数を直で呼び出して構わない).考えられるのだけれど,それじゃあハンドラから処理用スレッドにメッセージ投げてあとはスレッドにまかせるという当初の作りとあまり変わらない気もする(割り込み禁止区間を短くすることで,応答性能を向上する効果はあるだろうが).

ということで,割り込みハンドラ実行中の割り込みは受け付けない,という作りがいいだろう.このため割り込み応答性能はちょっと落ちるが,まあいいか.

ちなみにリアルタイム性についてだが,割り込みハンドラ実行中には割り込み禁止になる.これは優先度の低い割り込みハンドラの実行中でも,割り込み禁止になる(優先度の高い割り込みであっても,待たされる).よって優先度にかかわらず,割り込みが発生してハンドリングされるまでの間に,割り込みハンドラの処理の最悪時間がかかる(待たされる)可能性がある.これは割り込みハンドラの処理を見ることで見積もることができるので,まあリアルタイム性はあるといえるのではなかろうか.ただし割り込みハンドラの内部で,キューの検索とかのような処理時間を見積もれない処理をしてはならないことになる.

また,割り込みハンドラの処理が終って割り込み可能にする際に,別の割り込みがすでに発生して待たされている可能性がある.これは複数の割り込みが発生しているかもしれないので,優先度に応じてハンドリングする必要があるだろう.

「あるシステムコールの実行中に,別の割り込みを可能にするか?」

で,最後に3の話なのだけど,システムコールも割り込みの一種であると考えると,OSがシステムコールの処理をしている最中には割り込み禁止にしていいように思える.この場合,上で説明した「割り込みハンドラの処理の最悪時間」の見積もりに,システムコール処理も含めなければならないことになる.

うーん,なんかここまでくると,やっぱ割り込みハンドラをスレッド化して,割り込み処理中の割り込みを可能にするといいのかもしんない.でもまあとりあえずいいや.

ちなみに割り込み中の割り込みが禁止だとすると,割り込みハンドラからシステムコールを呼ぶことも禁止になってしまうが,これは許可したい.というのは,これができないとハンドラからスレッドへのメッセージ送信ができないからだ.ちなみにシステムコールの処理中は割り込み禁止なので,「システムコールの処理中に割り込みが発生し,ハンドラが起動して,その中からさらにシステムコールが呼ばれる」というような「システムコールのネスト」は発生しない.

で,手始めの改造.従来は extintr は kz_setsig() システムコールを使って割り込み(SIGHUP)を受け付けていたのだけど,kz_sethandler() を利用して割り込みハンドラからメッセージを投げるように改造してみた. 実は改造したのが去年の正月でもう1年近く前で,なんでこんなふうに改造したのかあんまりおぼえていないので,ちょっと読んでわかる範囲で説明すると,もともと extintr の SIGHUP 受信時に行っていた処理を extintr_handler() という関数にして,kz_sethandler() でそっちが呼ばれるようにしただけだ.さらにハンドラ内部からシステムコールが呼べるように,extintr_proc() でハンドラ実行前に専用のコンテキストを用意するような処理を追加してある.(さらにそのシステムコールの延長で,用意したコンテキストがレディーキューに対して処理されないように,getcurrent() とかに対処を入れている...のだと思う)

システムコールは割り込みハンドラから呼ばれる場合もあるので,システムコールのパラメータをカレントスレッドからでなく引数から取得するようにsyscall_proc() や thread_intr() を修正.thread_intrvec() も同様の理由で修正.

あと,割り込みハンドラからシステムコールが呼び出せるように,extintr_proc() 内部で block_sys によってSIGSYSを有効化する処理を追加.

あとついでに,割り込み禁止/有効化のサービス関数としてkz_block()/kz_unblock() を追加.

ってとこかしら.

えー、本家のページ でも書いたのだけど、このたび本を出しました。


C言語 デバッグ完全解説


内容は。。。まあ、見てみてください。


ここんとこKOZOSがぜんぜん触れていなかったけど、これの校正とかなんやらがあったためなのだな。


ちょっとまた別のこともしたいので、KOZOSはちょっとばかしお休みかなあ。


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

もう年末ですね! でも仕事が終らん!

で,前回からちょっと間が開いてしまっているのだが,リアルタイム性をどうやって確保するか,実はちょっと思いあぐねている.

リアルタイム性を確保するためには,OSの処理中の割り込み,つまり割り込みの再入を可能にしたい.前回は割り込みハンドラを登録可能にしたが,とくに割り込みハンドラの実行中の割り込みを可能にしたい.というのは,割り込みハンドラの実行中は割り込み禁止にしてしまうと,ハンドラでなにか重い処理をしたときに,リアルタイム性が確保できないからだ.

しかし,ある重要な割り込み処理をしている最中に,どーでもいいような割り込みが上がってくるのも困る.こーいうような場合には割り込みを待たせたい.でないと重要な処理が中断されて,どーでもいい割り込みの処理に切り替わってしまうからだ.

つまり,割り込みにも優先度を持たせたいということになる.たとえば現状のKOZOSでは外部割り込みは SIGHUP の1種類のみだが,複数種類にして優先度をつけるとか,SIGALRM はもっと優先度を高くするとかだ.割り込みに優先度をつけるのは,シグナルマスクを利用することで段階的にマスクをかけることで実現可能だ.

しかし,割り込みハンドラでの処理は極力短くするのがOSの鉄則だ.ということで割り込みハンドラでは,割り込みを上げてきたハードウエアのバッファを見て(KOZOSならば,extintr が read() することに相当する),それを処理するスレッドにメッセージとして投げてやる,という設計が良い.

しかし,例えば重要な割り込みが入った際に,ハンドラがそれを受けて処理用スレッドにメッセージとして投げたとしよう.スレッドはそのメッセージを受けて処理を開始するが,この際にどーでもいいような優先度の低い割り込みが入ると,やっぱりそのスレッドは待たされてしまうことになる.つまり,優先度の高い割り込みの実際の処理をスレッドに任せることは不可能(他の優先度の低い割り込みに割り込まれてしまうので)ということになる.

これを防止するためには,スレッドの優先度と割り込みの優先度を関連づけて,たとえば優先度の高いスレッドの実行中には,それに応じた割り込みマスクをかけて,優先度の低い割り込みは受け付けないようにする,という方法がある.

もう一点,OSの処理中の割り込みを受け付けるかどうかなのだけど,以下のいずれかの対処をすることでこれは可能だ.
  • KOZOSの内部処理を再入可能にする.
  • 再入不可の箇所は,部分的に割り込み禁止にする.(割り込み禁止区間を設ける)
  • 割り込みの優先度をうまく設計することで,再入不可な関数への再入が実際には起こらないようにする.
で,KOZOSの処理中の割り込み,つまり割り込み中の割り込みを可能にしたとしよう.割り込み中にもっと優先度の高い割り込みが入り,そっちのハンドラが新たに呼ばれたとする.優先度の高い割り込みのハンドラはとりあえず処理用のスレッドにメッセージを投げて,あとはスレッドに処理を任せるとしよう.しかし,ハンドラの終了後にすぐにスレッドをディスパッチすることはできない.もともと行っていた(優先度の低い)割り込み処理にいったん戻り,処理を完了させてからディスパッチしなければならないのだ.でないともともとも割り込み処理は途中までの中途半端な位置まで実行されただけで,宙に浮いてしまう.

割り込みを階層的にした場合には,何十にもネストして発生する可能性もある.ということは,優先度の高い割り込みのハンドラがメッセージを投げて,さらにその処理用スレッドがディスパッチされるまでの時間が保証できなくなってしまう.どれくらいの割り込みがネストしていて,それらの処理にどれくらいの時間がかかるのか見積もることができないからだ(いや正確に言うと最悪時間を見積もることはできるのだけど,きっととんでもなく長い時間になってしまう).

ということで,結局のところリアルタイム性を確保するには割り込みハンドラ内ですべての処理を行うか,もしくはKOZOSの内部処理中の割り込みは禁止にして,現状のように内部処理が完了した時点で割り込み許可するかのいずれかになってしまう.前者は融通が効かなそう.後者は応答が悪そう(あと割り込み禁止区間が長くなるので,リアルタイム性を保証するのが難しくなりそう).

うーん,困った.ということでいまちょっと本を読んで勉強中なのだな.なんかうまいやりかたはないだろーか.

きれいな解決策としては,ハンドラごとにコンテキストを持たせるというか,ハンドラをスレッド化するという方法がある.実はKOZOSはもともとそんなようなつくりになっているのだけど,割り込みハンドラを単なる関数呼び出しにするのではなく,kz_setsig() によりメッセージを投げさせて,完全にスレッドとして処理させるというものだ.これならきちんと優先度をつけて処理することが可能だ.可能なのだが,うーん,でも割り込みのたびにディスパッチされるというのもねえ...きれいだけど,なんか性能悪そうでやなのよね.

あーでも,KOZOSの内部処理中に割り込み入ったらすぐにスレッドをディスパッチできないから同じことか.やっぱダメだね.
(注意)このブログは本家 のほうの文章部分のみの転載です.ソースコードの配布,画像などについては本家のほうを参照してください.文章中のリンク先は面倒なのですべて本家のほうに変換してしまっているのでご注意ください.

今回は,KOZOSの割り込み応答性能についてちょっと考えてみよう.とはいっても(もう,なんべんも書いていることだが,いちおう言っておくと)KOZOSは汎用OS上で1プロセスとして動作するユーザランドOSなので,そーいうことまで考慮したKOZOS自身の本当の割り込み応答性能を論ずるのは,OSに依存してしまうのでちょっとナンセンスだ.なのでまあ勉強と言う意味で,仕組み上というかKOZOS単体としての理論上の割り込み性能,という話になる.

で,まあはっきりいってしまうと,現状のKOZOSは,割り込み応答性能がちょっとイマイチのつくりになってしまっている.

まずKOZOSの欠点として,システムコールも含めたOSの処理中は割り込み禁止になっている.これは KOZOS 起動時に呼ばれる初期化用関数である 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);

のようにして,シグナルハンドラ内部でのシグナル発生をブロックしているためだ.これはOSっぽい言いかたをすると,「割り込み中は割り込み禁止にしている」ということになる.これはKOZOSの処理中のKOZOSへの再入を防止するのが目的で,安全のためなのだけど,性能的にはよろしくない.

このため,たとえば時間のかかるようなシステムコールの処理中に割り込みが入った場合にも,割り込み禁止になっているために割り込みは待たされてしまう.割り込み発生時にどんなシステムコールが実行されているかなんて予想がつかないから,これではリアルタイムOSとはちょっと言えないだろう.

現状のKOZOSでは,システムコールなどの処理中に割り込みが発生した場合には,割り込みはブロックされ,スレッドのディスパッチの直前に
  on_os_stack = 1;
  sigprocmask(SIG_UNBLOCK, &block, NULL);
  sigprocmask(SIG_BLOCK, &block, NULL);
  on_os_stack = 0;

  /* ここで SIGALRM が発生するとシグナルを取りこぼす...要検討 */

  setcontext(¤t->context.uap);

のようにして,一瞬だけ割り込み許可している部分で割り込みハンドラが起動する.上の部分は,setcontext() によるコンテキスト切替えを行うとハンドラ内部で発生したシグナルが失われてしまうという問題を回避するための対処だ.これに関しては第14回を参照.

また,割り込み発生時には kz_setsig() によってメッセージ送信を依頼したスレッドにメッセージが投げられる.つまり,実際の割り込み処理はスレッドのメッセージ受信を契機として,スレッドが行うことになる.extintr とかがまさにそのような作りになっているのだが,これも割り込み応答性能という点で見ると,ちょっとイマイチだ.割り込み処理のために当該のスレッドのディスパッチが必ず必要になってくるからだ.さらに,(extintrなどの)スレッドによる割り込みの処理中にも,他の割り込みが発生してKOZOSの割り込みハンドラが呼ばれてしまう(そして,当該スレッドへのメッセージの送信処理が行われる)ことが考えられる.これでは重要な割り込み処理が行われている最中にも,何らかの割り込みが発生すると優先度に関係なく割り込み処理が行われるということになるので,リアルタイム性という点でちょっとまずい.

現状のKOZOSで,割り込み処理をスレッドにお願いしているのは,単に簡略化のためだ.通常のOSでは,割り込み処理は割り込みハンドラが行うものだ.ということで今回は,割り込み発生時に登録した関数を呼び出してくれるような,割り込みハンドラの登録用のシステムコールを追加してみた. 修正点について,以下に説明する.まあ今回はシステムコールを追加しただけなのであまり説明するようなことは無い.本来ならば割り込みハンドラを利用するように extintr とかを書き換えるべきなのだけど,まああまり一度に説明しても重いので,今回はシステムコールの追加だけにとどめて,少しずつ説明していくつもり.(最近,長~い説明が多いので)

従来から kz_setsig() というシステムコールで,割り込み発生時にメッセージを送信するように設定しておくことができるが,これと似たようなシステムコールとしてkz_sethandler() というのを追加する.
diff -ruN kozos29/kozos.h kozos30/kozos.h
--- kozos29/kozos.h	Mon Dec 17 21:53:06 2007
+++ kozos30/kozos.h	Mon Dec 17 22:12:16 2007
@@ -4,6 +4,7 @@
 #include "configure.h"
 
 typedef int (*kz_func)(int argc, char *argv[]);
+typedef void (*kz_handler)(int signo);
 
 /* syscall */
 int kz_run(kz_func func, char *name, int pri, int argc, char *argv[]);
@@ -17,6 +18,7 @@
 int kz_recv(int *idp, char **pp);
 int kz_timer(int msec);
 int kz_pending();
+int kz_sethandler(int signo, kz_handler handler);
 int kz_setsig(int signo);
 int kz_debug(int sockt);
 void *kz_kmalloc(int size);

kz_sethandler() は,引数にシグナル番号と関数へのポインタをとる.シグナル(割り込み)発生時には,設定しておいた関数が呼ばれることになる.

syscall.[ch]については,まあいつもどおりのシステムコール追加なので,とくに説明しない.次に,thread.c で実際にハンドラを登録する部分.
diff -ruN kozos29/thread.c kozos30/thread.c
--- kozos29/thread.c	Mon Dec 17 21:53:06 2007
+++ kozos30/thread.c	Mon Dec 17 22:23:51 2007
@@ -28,9 +28,10 @@
 static unsigned int readyque_bitmap;
 static kz_timebuf *timers;
 static kz_thread *sigcalls[SIG_NUM];
+static kz_handler handlers[SIG_NUM];
 static int debug_sockt = 0;
 sigset_t block;
-static int on_os_stack = 0;
+static stack_t intrstack;
 
 kz_thread *current;
 
@@ -315,8 +316,17 @@
   return 0;
 }
 
+static int thread_sethandler(int signo, kz_handler handler)
+{
+  handlers[signo] = handler;
+  putcurrent();
+  return 0;
+}

kz_sethandler() のシステムコール処理用関数として thread_sethandler() を追加してある.内容は,シグナル番号をインデックスとした配列に関数を登録するだけ.

次に,ハンドラの呼び出し部分.
 static void extintr_proc(int signo)
 {
+  if (handlers[signo])
+    handlers[signo](signo);
   if (sigcalls[signo])
     sendmsg(sigcalls[signo], 0, 0, NULL);
 }

従来は extintr_proc() では kz_setsig() で設定されたスレッドに対してsendmsg() を呼び出してメッセージを送信するだけだったが,さらにハンドラ関数を呼ぶ処理を追加する.

さらに,上でも説明したように,従来はスレッドのディスパッチの直前に割り込みを一瞬だけ許可していたのだが,この際に on_os_stack というフラグを立てて,割り込み禁止してから on_os_stack を落とし,スレッドのディスパッチを行っていた.これは割り込み発生時に,KOZOSの内部処理中に呼ばれたのか,通常のスレッド実行時に呼ばれたのかを検出し,後者の場合のみコンテキスト保存を行うようにするためだ.これを検出できないと,KOZOSの内部処理中に割り込み発生した場合に,スレッドのコンテキストを上書きして壊してしまうことになるからだ.ただこの処理は,スレッドのディスパッチの前に on_os_stack をゼロに戻さないとならないため,その後のディスパッチまでの短い一瞬で割り込みが発生するとやはり割り込みを取りこぼすというタイミング問題があった.(従来の thread_intrvec() の終端部分のコメント参照)

KOZOSの割り込み応答性能を改善するためには,KOZOSの処理中(主にシステムコールの処理中)の割り込みを許すようにしたい.つまり,KOZOSへの再入を可能にしたい.そのための準備として,どこからでも割り込みを受け付けられるように,従来の on_os_stack を用いた再入検出を改良する.
@@ -496,19 +510,16 @@
    * これはOSの割り込み処理の再入になるが,以下の位置に限定して再入が行われる
    * ので問題は無い.
    */
-  on_os_stack = 1;
   sigprocmask(SIG_UNBLOCK, &block, NULL);
-  sigprocmask(SIG_BLOCK, &block, NULL);
-  on_os_stack = 0;
-
-  /* ここで SIGALRM が発生するとシグナルを取りこぼす...要検討 */
 
   setcontext(¤t->context.uap);
 }
 
 static void thread_intr(int signo, siginfo_t *info, ucontext_t *uap)
 {
-  if (!on_os_stack) {
+  unsigned int esp = uap->uc_mcontext.mc_esp;
+  if ((esp <  (unsigned int)intrstack.ss_sp) ||
+      (esp >= (unsigned int)intrstack.ss_sp + intrstack.ss_size)) {
     memcpy(¤t->context.uap, uap, sizeof(ucontext_t));
   }
   thread_intrvec(signo);

まあ見ればわかるが,スタックポインタの値を見て,スレッド実行中の割り込みなのか,KOZOSの内部処理中の割り込みなのかを判断するように修正した.これだと Linux とかに移植する際には修正が必要になるのでちょっとうーむだが,まあしかたがない.

あとはまあ細かい修正がいくつかあるが,面倒なのでとくに説明はしない.あととりあえず今回の修正をして,gdbで繋いでtelnetなどがひととおり従来通り動くことは確認した.

これで割り込みハンドラを登録できるようになった.次回は実際に割り込みハンドラを利用するように改良することで,割り込み応答性能を改善してみたい.