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

さいきんいろいろ忙しくて前回からちょっと間が開いてしまったが,文化の日なのでちょっと文化的(工学的,か?)なことをしたい.ということで連載再開.

で,前回までの話なのだけど,前回にも書いたが現状の実装では以下の欠点がある.
  • スレッド内で無限ループに入ると,extintr の select() が呼ばれなくなってしまうので,OS全体が固まる.
  • 無限ループ中になんとか抜けるには現状でタイマ割り込みしか無いが,たとえタイマ割り込みがかかったとしても,extintr のスレッド優先度が31と最低(これは,select()待ちをするため)なので,他スレッドが無限ループしている限りは extintr が動けないので,外からの入力を処理できない.
ちなみに現在の extintr の役割は,外からの入力を受け付けて当該のスレッドにメッセージを投げることだ.で,そのためには select() で入力を確認する必要があり,ついでにふだんはその select() で待つことで,アイドル状態でのCPU負荷を下げる,という設計になっている.

で,この解決策なのだけど,まず現状の設計のように select() で外からの入力を待つ場合には,スレッド優先度を(extintrの31みたいに)下げざるを得ない(でないと他のスレッドが動けなくなってしまうので).しかしそれだと,今回のような問題が起きる.なので,
  • extintr のスレッド優先度を最優先にする.これにより,他スレッドが無限ループに入っても,なにかきっかけがあってOSに処理が渡れば,extintrが動くことができる.で,extintrが動ければ,extintrから各スレッドにメッセージが投げられるので,各スレッドも受信処理を行える.
  • extintr のスレッド優先度が最優先の場合,extintr 内で select() で待つとOS全体が固まってしまうのでダメ.よってCPU負荷を下げるためのスレッドを,idleスレッドとして別途作成する.
という設計が考えられる.つまり現状で extintr がやっている,外部入力の処理とCPU負荷を下げるための select() 待ちを,別々に分離するわけだ.

問題は,たとえ extintr のスレッド優先度を上げたところで,外部入力があったときに extintr に処理を渡すようななんらかのきっかけが無いと,やはり無限ループを救うことができない.で,どうするかなのだが,2つの実装が考えられる.
  • タイマ割り込みにより,定期的に extintr を動かす.
  • 外部からシグナルを発行する.(このシグナルが,extintr に処理を渡すきっかけになる)
まあどちらにしろ,シグナルを使うことになる(タイマ割り込みは要するにSIGALRMなので).1番目は,kz_timer() によって定期的に extintr にメッセージ発行するようなスレッドを別途作成し,extintr はメッセージ受けたら待ち時間無し(ゼロ秒でタイムアウトする)の select() で外部入力の有無を調べ,入力があれば処理する,という実装だ.

2番目はちょっとわかりにくいのだけど,外部入力の有無を他人に調べてもらい,外部入力があるのならばシグナルを発行してもらうというものだ.後述するけど,これだと非常に外部割り込みっぽくなってそれっぽくていいなーと思う.で,問題はその他人をどうするかなのだが,ちょっとどうしようか考えたのだけど,fork()で専用の子プロセスを作成し,select()で見張っていてもらうというのはどうだろうか.上述したように,idleスレッドを作成したとしても,外部入力があったときにextintr に処理を渡すようななんらかのきっかけが無いと,idleスレッド内のスリープや他スレッド内の無限ループから抜けられない.このためのきっかけとして,子プロセスからシグナルを発行してもらうわけだ.問題は子プロセス側で select() するために,親プロセス側のソケットを引き継ぐ必要があるのだが,ふつうに fork() してソケットって引き継げるものなのだろうか?と思ったのだけど,考えてみれば標準入出力とかパイプとかは引き継げるし,ためしてみたら引き継げるみたいだったので,まあとりあえずやってみてから考えればいいだろう.

1番目はデバイスドライバがポーリングするイメージ.2番目はCPU外部の割り込みコントローラから割り込みを上げてもらうようなイメージに近いだろうか.ちなみに1番目の実装だと,kz_timer()使って専用のスレッドをひとつ上げるだけなので,現状の作りから簡単に修正できる.ただポーリングになるので応答が鈍そうだし,ちょっとカッコ悪いな~~~,なんだかな~~~,って気がする.で,2番目の実装なのだけど,子プロセスが入力を監視してシグナルを上げてくるってのは,まるで割り込みコントローラがCPUとは独立して動いて割り込み線をアサートしてくるみたいでそれっぽい.応答も鈍くなることは無いし,まあもともとKOZOSは勉強用に実際の組み込みOSを模擬する,という目的があるので,今回は2番目の実装を採用.

で,書いてみた.今回はアイドル用のスレッドとして,idle.c が追加されている.以下は前回からの差分.今回の修正のキモは,extintr.cの改造だ.以下に説明していく.

まずメインループだが,

int extintr_main(int argc, char *argv[])
{
...

pid = getpid();
kz_setsig(SIGHUP);

先頭で,kz_setsig()というシステムコールを呼び出している.これは今回新設したシステムコールなのだけど,以下の動作をする.
  • 指定したシグナルが発行されたときに,自スレッドにメッセージを送らせる.
まあこれについては,あとでまた説明する.

で,extintrのメインループなのだが,

while (1) {
fd = kz_recv(&id, &p);
if (id == 0) { /* from kozos */
fds = readfds;
ret = select(maxfd + 1, &fds, NULL, NULL, &tm);
if (ret > 0) {
for (ibp = interrupts; ibp; ibp = ibp->next) {
if (FD_ISSET(ibp->fd, &fds)) {
switch (ibp->type) {
case INTR_TYPE_ACCEPT:
...
case INTR_TYPE_READ:
...
}
...
}
}
}

ここで,メッセージを受けたときに select() で外部入力状態を確認し,入力があるなら当該スレッドにメッセージを投げる,という動作をしている.まあこのへんの処理は実は前回とほぼ同じなのだが,select()のタイムアウト値がゼロになっているので,即時抜けしている点に注意してほしい.

extintrのこのへんの動作はSIGHUPを見なければならないので本来はKOZOSの内部で行うべきなのかもしれないが,シグナル発生をメッセージで通知する kz_setsig()というシステムコールを実装し,基本的にはメッセージドリブンで専用スレッド(extintr)にやらせる,というのが,KOZOSの特徴的なポリシーではある.このためKOZOSのコア部分である thread.c は,非常にシンプルなもの(スレッド管理とシステムコールだけ)になっている.まあ将来的にはタイマの管理も専用スレッドに切り出してしまいたい(kz_setsig()が新設されたので,これも可能なはずだ).

あとextintrが動作するのはメッセージを受けたときなのだけど,じゃあ誰がいつメッセージを投げるのかというと,先ほど先頭で kz_setsig() でSIGHUP が発生したときにメッセージが投げられるように設定しているので,SIGHUP 発生時にはOSからメッセージがとんでくることになる.つまり,子プロセスは以下のように動作すればいい.
  • select()で入力を監視し,入力があれば親プロセスにSIGHUPを投げる


で,extintrの続き.

} else if (fd) { /* from thread */
setintrbuf(fd, id, (struct sockaddr *)p);
/*
* ソケットを子プロセスに引き継ぐ必要があるので,子プロセスを
* 毎回作りなおす.
*/
if (chld_pid) {
write(cnt_fd, "exit", 5);
wait(NULL);
close(cnt_fd);
}
pipe(fildes);
if ((chld_pid = fork()) == 0) { /* 子プロセス */
#if 0
signal(SIGALRM, SIG_IGN);
#endif
close(fildes[1]);
intr_controller(fildes[0]);
}
close(fildes[0]);
cnt_fd = fildes[1];
}
}
}

ここが今回の改造のキモになる部分だ.従来の extintr は,他スレッド(telnetdとか)からメッセージを受けたら,そのソケットを見張るように select() 用の readfds に追加する,という動作をしていた.今回は,select() で見張るための子プロセスを作成する,という動作をしている.

注意として,子プロセスには現在のソケットをすべて引き継がせなければならない(子プロセス側では select() でソケットを見張る必要があるので).なので,毎回子プロセスを終了させては起動させなおす,ということをやっている.(子プロセスにパイプ経由で exit コマンドを送ることで子プロセスを終了させ,fork()で再度起動している)

また子プロセスとの同期のために,通信をするためのパイプを作成している.まあこのへんはネットとかで fork とか pipe とかプロセス間通信とかで検索すると山のように出てくるので,各自で調べてほしい.

実際の子プロセスは以下になる.

static int intr_controller(int cnt_fd)
{
int fd, ret, size;
fd_set fds;
char buf[32];
char *p;

/*
* 子プロセスなので,この中で kz_ システムコールを利用してはいけない.
*/

FD_SET(cnt_fd, &readfds);
if (maxfd < cnt_fd) maxfd = cnt_fd;

while (1) {
fds = readfds;
ret = select(maxfd + 1, &fds, NULL, NULL, NULL);
if (ret > 0) {
if (FD_ISSET(cnt_fd, &fds)) {
size = read(cnt_fd, buf, sizeof(buf));
if (size > 0) {
for (p = buf; p < buf + size; p += strlen(p) + 1) {
if (!strcmp(p, "exit"))
goto end;
fd = atoi(p);
FD_SET(fd, &readfds);
}
}
} else {
kill(pid, SIGHUP);
for (fd = 0; fd <= maxfd; fd++) {
if ((fd != cnt_fd) && FD_ISSET(fd, &fds)) {
FD_CLR(fd, &readfds);
}
}
}
#if 0
/*
* 繰り返しシグナルが発生することの防止.
* 本来なら割込み処理側からメッセージを送ってもらって,
* ソケットごとに割込みの有効化/無効化を行う必要があるだろう.
* (シグナル送信したら無効化し,割込み処理が行われたら
* メッセージを送ってもらって割込みを有効化する)
*/
usleep(1000);
#endif
}
}

end:
close(cnt_fd);
exit(0);
}

子プロセスは以下の動作をする.
  • select()で待ち,外部入力があればSIGHUPを発行する.
  • SIGHUP発行時には,入力があったソケットをいったん select() 対象から外す.で,親プロセスからソケット追加の指示があったときには再び select() 対象に入れる.
  • 親プロセスから exit コマンドが来たときには終了する.
上記2番目の動作は,親プロセスが入力の受信をもたついているときに子プロセスが毎回入力を感知して SIGHUP が上がりまくる(そして親プロセスは,SIGHUPの処理のためにさらにもたつく)という動作を防止するためだ.このため,入力があったらそのソケットを監視対象から除外し,親プロセス(extintr)側でソケットを読み取ったら再び監視対象に追加(このやりとりのためにパイプを利用している),という動作をしている.このへんの動作も,割り込み処理時に割り込みコントローラ側のレジスタをCPU側で刈り取っている(ちょっと動作は違うが,似てはいる)みたいで,ちょっとそれっぽいね.実はこの対策として,初期のころはusleep()でちょっと待つような暫定処理が入っていたのだけど(待っている間に親プロセスが動作してソケットをリードすることを期待している),この刈り取り処理が入ったので不要になったため,現在は #if 0 で無効になっている.

3番目のexitコマンドについてだが,上にも書いたがソケット情報を引き継ぐ必要があるので,監視対象のソケットが増えるたびに子プロセスは起動し直している.このため親側では子プロセスを終了させる必要があるのだが,まあこれは kill() してもいいのだけど,せっかくパイプを作成しているので,exit コマンドというので終了させるようにしてみた.

あと,アイドルスレッドについて.

int idle_main(int argc, char *argv[])
{
while (1) {
select(0, NULL, NULL, NULL, NULL);
}
}

アイドルスレッドは単に select() で無限待ちをするだけ.これが最低の優先度で起動するので,なにもないときにはプロセスはスリープしてCPU負荷を下げる,という動作をしている.で,外部入力があったときには子プロセスがSIGHUPを送ってくるし,タイマがかかったときにはSIGALRMが発行されて,KOZOSに処理が渡る,という動作になっている.SIGHUPが来ればextintrに対してメッセージが(これは,kz_setsig() で設定してたから)発行され,extintrが(idleよりも優先度高いから)動ける.で,extintrがソケットからデータをリードできれば,そのデータをメッセージで当該スレッドに送信するので,今度はそのスレッドが(これも,やはりidleよりも優先度高いから)動作できる.うーん,きれいな作りだ.

ついでに kz_setsig() についてちょっと説明しておこう.

static int thread_setsig(int signo)
{
sigcalls[signo] = current;
putcurrent();
return 0;
}

static void extintr_proc(int signo)
{
if (sigcalls[signo])
sendmsg(sigcalls[signo], 0, 0, NULL);
}

static void thread_intrvec(int signo)
{
switch (signo) {
...
case SIGHUP: /* 外部割込み */
break;
...
}
extintr_proc(signo);
dispatch();
longjmp(current->context.env, 1);
}

割り込みベクタにSIGHUPを追加し,さらに割り込み処理後に extintr_proc() を呼ぶように修正している.extintr_proc() では,シグナルの種類に応じてスレッドに空メッセージを投げている.

ちなみに telnetd.c,httpd.c に対してもちょっと修正が入っている.ソケットのクローズを close() でなく shutdown() にしないとなんかうまく telnet とかを終了できなくなっちゃった(子プロセス側でソケットを select() しているせい?前回は問題なかったのに...詳細不明)のでそうしたのと,あとついでに telnet での Ctrl-C,Ctrl-D に対応させてある.まあこのへんは実際のソースを見てほしい.

メインの関数は以下になる.実行してみよう.

% ./koz
Sat Nov 3 09:03:01 2007
Sat Nov 3 09:03:02 2007
Sat Nov 3 09:03:03 2007
Sat Nov 3 09:03:04 2007
Sat Nov 3 09:03:05 2007
Sat Nov 3 09:03:06 2007
Sat Nov 3 09:03:07 2007
Sat Nov 3 09:03:08 2007
Sat Nov 3 09:03:09 2007
Sat Nov 3 09:03:10 2007
Sat Nov 3 09:03:12 2007
Sat Nov 3 09:03:13 2007
Sat Nov 3 09:03:14 2007
Sat Nov 3 09:03:15 2007
...

おー,ちゃんと動いている.

% telnet 192.168.0.3 20001
Trying 192.168.0.3...
Connected to 192.168.0.3.
Escape character is '^]'.
> date
Sat Nov 3 09:03:12 2007
OK
> Connection closed by foreign host.
%

telnetも問題無しだ.

ちなみに今回は,外部入力を子プロセスで監視してSIGHUP割り込みを上げる,という実装にしてあるので,while(1)で無限ループするようなスレッドがいても,それより優先度の高いスレッドは正常に動作できる(もちろんそれより優先度の低いスレッドは動作できないが,優先度の高いビジーなスレッドが存在しているためなので,これはこれで正しい).このへんは組み込みOSの重要な要素なので,ぜひ,各自確認してみてほしい.

さて,今回でかなり組み込みOSっぽく動くようになった.無限ループのスレッドがあっても,きちんと動くようになった.次はKOZOS作成の目的のひとつである,gdb対応をぜひやってみたい.