(第8回)外部割り込みっぽくしてみる

2007/11/03

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

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

で,前回までの話なのだけど,前回にも書いたが現状の実装では以下の欠点がある.

ちなみに現在の extintr の役割は,外からの入力を受け付けて当該のスレッドに メッセージを投げることだ.で,そのためには select() で入力を確認する 必要があり,ついでにふだんはその select() で待つことで,アイドル状態での CPU負荷を下げる,という設計になっている.

で,この解決策なのだけど,まず現状の設計のように select() で外からの入力を 待つ場合には,スレッド優先度を(extintrの31みたいに)下げざるを得ない (でないと他のスレッドが動けなくなってしまうので). しかしそれだと,今回のような問題が起きる.なので,

という設計が考えられる.つまり現状で extintr がやっている, 外部入力の処理とCPU負荷を下げるための select() 待ちを,別々に分離するわけだ.

問題は,たとえ extintr のスレッド優先度を上げたところで, 外部入力があったときに extintr に処理を渡すようななんらかのきっかけが無いと, やはり無限ループを救うことができない.で,どうするかなのだが, 2つの実装が考えられる.

まあどちらにしろ,シグナルを使うことになる (タイマ割り込みは要するに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 が追加されている.

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

以下は前回からの差分.

今回の修正のキモは,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からメッセージがとんでくることになる. つまり,子プロセスは以下のように動作すればいい.

で,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);
}
子プロセスは以下の動作をする. 上記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対応をぜひやってみたい.


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