(第14回)カーネル用コンテキストを改良する

2007/11/11

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

今日はなんかノッているので2本目.

前回はコンテキスト管理を getcontext()/setcontext() の枠組に置き換えた. で,従来はカーネル用コンテキストは intr_env という変数に保存していて, 割り込み発生時には intr_env に setcontext() することで, カーネル用コンテキストに切替えていた.これは前回も書いたように, ユーザスレッドのコンテキストでカーネルの(システムコールなどの)処理を 行うのはまずいというのが理由なのだが,実はシグナルハンドラの先頭から setcontext() されるまでの間はやはりユーザスレッドのコンテキストが利用されて しまうので,根本的な解決になっていない. しかしシグナル発生時にスタックが利用されてしまうのは避けられず, これはある意味どーしようもない.

しかし,前にも書いたように,シグナルに関しては現在は signal() よりも sigaction() を使うのが現代的(第12回参照)なのだけど,このあたりをいろいろ 調べていたら,実は sigaltstack() という,またこれがまさにこーいうときの ために使うためのシステムコールがあるんだな. うーん,ほんとにラッキーな,というか,まあ誰でも考えることは同じという ことだろうか.

なので,sigaltstack() を使うように変更すれば,intr_env による面倒な カーネル用コンテキストへの切替え処理は不要になるんだな. で,そーいうふうに書き直してみた.

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

以下に修正内容を説明しよう. まずメインの修正となるのは thread.c だ.

diff -ruN kozos13/thread.c kozos14/thread.c
--- kozos13/thread.c	Sun Nov 11 23:17:30 2007
+++ kozos14/thread.c	Mon Nov 12 00:21:49 2007
@@ -21,14 +21,13 @@
 
 kz_thread threads[THREAD_NUM];
 kz_thread *readyque[PRI_NUM];
-static ucontext_t intr_env;
 static kz_timebuf *timers;
 static kz_thread *sigcalls[SIG_NUM];
 static int debug_sockt = 0;
-static sigset_t block;
+sigset_t block;
+static int on_os_stack = 0;
 
 kz_thread *current;
-static int current_signo;
 
 static void getcurrent()
 {
まず intr_env が不要になるので削除している. シグナルのブロック情報である block は,実は extintr からも利用するように 修正したので static を外してある.on_os_stack については後で説明. あと current_signo はカーネル用コンテキストへの切替え後にシグナル番号を 参照するための変数だったのだけど,カーネル用コンテキストへの切替え処理が 無くなったのでこれも不要になったので削除.
 static void thread_init(kz_thread *thp, int argc, char *argv[])
 {
+  /*
+   * なぜかここで UNBLOCK にしないとシグナル受信できないので
+   * UNBLOCK にする.謎.
+   */
   sigprocmask(SIG_UNBLOCK, &block, NULL);
 
   thp->func(argc, argv);
thread_init()にはコメント追加.今回,KOZOS起動時のシグナルのブロック設定が 無くなったので,上記の UNBLOCK の設定処理は不要なはず...なのだが, なぜか削るとうまく動かないので,そのまま残してある.謎だ.

次に thread_intrvec() でのディスパッチ処理.

@@ -410,19 +413,39 @@
   }
   extintr_proc(signo);
   dispatch();
+
+  /*
+   * スタブでの read() 待ちブロック中に SIGALRM (および SIGHUP)が発生した
+   * 場合に,以下の setcontext() によるコンテキストスイッチで SIGALRM が
+   * 消えてしまう(これはスタブでのブロックに比べると発生頻度は低いが,OSの
+   * 処理中にシグナル発生した場合も同様).
+   * 対策として,シグナルのブロックを一瞬だけ開けて,SIGALRM が発生した
+   * 場合には,再度シグナル処理を行う.
+   * これはOSの割り込み処理の再入になるが,以下の位置に限定して再入が行われる
+   * ので問題は無い.
+   */
+  on_os_stack = 1;
+  sigprocmask(SIG_UNBLOCK, &block, NULL);
+  sigprocmask(SIG_BLOCK, &block, NULL);
+  on_os_stack = 0;
+
+  /* ここで SIGALRM が発生するとシグナルを取りこぼす...要検討 */
+
   setcontext(¤t->context.uap);
 }
まあコメントにも書いてある通りなのだけど, 「スタブ中の read() でのブロック中に SIGALRM が発生した場合に取りこぼすため, タイマが止まってしまう」という問題の対処として, 前回はシグナル処理中のシグナル発生を受け付けるようにした. これは前回も書いた通り,KOZOSの処理中に再入する可能性が発生するため 問題ありありだ.

割り込み処理中はシグナルはブロックされている(sa.sa_mask をそーいうふうに 設定している)ため,setcontext() でユーザスレッドに切り替わった直後に SIGALRM が発生することを期待したいのだが,どうも setcontext() をすると,ブロック中の 割り込みが消えてしまうらしく(このへんはOSに依存する現象でもあるだろう.きっと) ,やはり取りこぼしてしまう.

なので,OSの割り込み処理が終ってコンテキストスイッチによりユーザスレッドの 動作に切り替わる直前で,sigprocmask()により一瞬だけ割り込みを開けて, SIGALRMがブロックされているならばここで受け付ける. これはKOZOSの内部処理中に再度割り込みによりKOZOSの処理が行われることに なるのだが,この一瞬の部分だけなので,再入の問題は無い (KOZOS全体で割り込みを開けてしまうと,リンクリストの操作中に再入されたりすると リスト構造に矛盾が発生する,などの問題が考えられる). ただしKOZOSの内部処理中に再度割り込みによりKOZOSの処理が行われた場合には, シグナルハンドラでカレントスレッドに対するコンテキストの保存を行うとまずい. KOZOS動作用のコンテキストで,カレントスレッドのコンテキストを上書きして しまうからだ.対策として,シグナル受け付け時には on_os_stack を立てて, シグナルハンドラ内では on_os_stack を見ることで,通常のシグナル発生か, KOZOS内部処理中の(上記割り込み開け位置での)シグナル発生かを判断できるように してある.

まあコメントにもあるように,実は割り込みを閉じて setcontext() が行われる 短い期間に SIGALRM が発生してしまうと,やはりシグナルを取りこぼす. これについてはまたそのうち考えよう.

次はシグナルハンドラ.

 static void thread_intr(int signo, siginfo_t *info, ucontext_t *uap)
 {
-  memcpy(¤t->context.uap, uap, sizeof(ucontext_t));
-  current_signo = signo;
-  setcontext(&intr_env);
+  if (!on_os_stack) {
+    memcpy(¤t->context.uap, uap, sizeof(ucontext_t));
+  }
+  thread_intrvec(signo);
 }
さっきも書いたように,on_os_stack を見てコンテキストの保存を行う. on_os_stack がゼロでない場合は,KOZOS処理中の割り込みなので,コンテキストの 保存を行う必要は無い. さらに,従来は setcontext() でカーネル用コンテキストにスイッチしてから 割り込みベクタ処理を行っていたが,今回の修正でシグナル発生時に自動的に カーネル用コンテキスト(ていうか,スタック)に切り替わるようになったので, 割り込みベクタ処理を直接呼んでいる.(このため current_signo という変数が 不要となっている)

次に,KOZOSの起動処理だ.

 static void thread_start(kz_func func, char *name, int pri, int argc, char *argv[])
 {
   struct sigaction sa;
+  stack_t sigstack;
 
   memset(threads, 0, sizeof(threads));
   memset(readyque, 0, sizeof(readyque));
@@ -430,20 +453,15 @@
 
   timers = NULL;
 
+  sigstack.ss_sp = malloc(SIGSTKSZ);
+  sigstack.ss_size = SIGSTKSZ;
+  sigstack.ss_flags = 0;
+  sigaltstack(&sigstack, NULL);
+
   memset(&sa, 0, sizeof(sa));
   sa.sa_sigaction = (void (*)(int, siginfo_t *, void *))thread_intr;
-  sa.sa_flags |= SA_SIGINFO;
-#if 0
+  sa.sa_flags = SA_SIGINFO | SA_ONSTACK;
   sa.sa_mask = block;
-#else
-  /*
-   * シグナル処理中のシグナル発生を受け付ける.
-   * これを有効にするとKOZOSの処理中に再入する可能性が発生するため問題有り
-   * なのだが,スタブ中での read() ブロック中に SIGALRM が発生した場合の
-   * 暫定対処とする.
-   */
-  sigemptyset(&sa.sa_mask);
-#endif
 
   sigaction(SIGSYS , &sa, NULL);
   sigaction(SIGHUP , &sa, NULL);
@@ -460,30 +478,11 @@
   current = NULL;
   current = (kz_thread *)thread_run(func, name, pri, argc, argv);
 
-  swapcontext(&intr_env, ¤t->context.uap);
-  {
-    /* なぜか getcontext() しなおさないと正常動作しない...謎 */
-    static int f;
-    do {
-      f = 0;
-      getcontext(&intr_env);
-    } while (f);
-    f = 1;
-  }
-
-  thread_intrvec(current_signo);
+  setcontext(¤t->context.uap);
 }
まず sigaltstack() でシグナルスタックを設定しておく. さらに sa.sa_flags に SA_ONSTACK を指定することで, シグナルスタックが利用されるようにする. シグナル処理中のシグナルは前回は暫定対処として受け付けたが, 今回は受け付けないでブロックする. 上でも書いたように,ディスパッチ処理の部分で一時的にシグナルを 受け付けることでブロックされているシグナルを処理するように修正したからだ.

さらに,前回は swapcontext() で最初のスレッドをスタートしていた. このためシグナルハンドラ内部でカーネル用コンテキストに切替えた際に swapcontext() の直後に飛んできていたのだが,シグナルハンドラからは 割り込みベクタ処理を直接呼ぶように修正したので, swapcontext() は利用せずに setcontext() で最初のスレッドをスタートするように 修正する.

次に,kz_start()の修正.

 void kz_start(kz_func func, char *name, int pri, int argc, char *argv[])
 {
-  /*
-   * setjmp()/longjmp()はシグナルマスクを保存/復旧するので,
-   * intr_env の setjmp() 前にシグナルマスクを設定することでシグナル処理中の
-   * シグナルの発生をマスクし,割り込みハンドラ内でのシグナルを無効とする.
-   * (でないと割り込みハンドラの実行中に intr_env に longjmp() した後に,
-   *  SIGALRM や SIGHUP の発生をハンドリングしてスレッドのディスパッチが
-   *  行われてしまい,誤動作する)
-   */
   sigemptyset(&block);
   sigaddset(&block, SIGSYS);
   sigaddset(&block, SIGHUP);
@@ -492,7 +491,6 @@
   sigaddset(&block, SIGSEGV);
   sigaddset(&block, SIGTRAP);
   sigaddset(&block, SIGILL);
-  sigprocmask(SIG_BLOCK, &block, NULL);
 
   thread_start(func, name, pri, argc, argv);
従来はカーネル用コンテキストへの切替え後にシグナルがブロックされるように kz_start() で SIG_BLOCK に設定していたが, 今回の修正でシグナル発生時には sa.sa_mask の設定により自動的にシグナルが ブロックされる設定に移行するので,不要になったので削除.

あとついでに,extintr の修正.

@@ -42,6 +44,7 @@
 
   FD_SET(cnt_fd, &readfds);
   if (maxfd < cnt_fd) maxfd = cnt_fd;
+  readfds_save = readfds;
 
   while (1) {
     fds = readfds;
@@ -53,8 +56,8 @@
 	  for (p = buf; p < buf + size; p += strlen(p) + 1) {
 	    if (!strcmp(p, "exit"))
 	      goto end;
-	    fd = atoi(p);
-	    FD_SET(fd, &readfds);
+	    if (!strcmp(p, "refresh"))
+	      readfds = readfds_save;
 	  }
 	}
       } else {
@@ -126,7 +129,6 @@
   struct timeval tm = {0, 0};
   intrbuf *ibp;
   int fildes[2];
-  char buf[32];
   pid_t chld_pid = 0;
 
   FD_ZERO(&readfds);
@@ -157,16 +159,14 @@
 		kz_send(ibp->id, size, buffer);
 	      break;
 	    }
-	    /*
-	     * GDBスタブ利用の場合はスタブ側で read() されてしまい
-	     * 上の select() で検知できないので,子プロセスへの
-	     * 割り込み再開通知を別の方法で毎回行う必要があるだろう.
-	     */
-	    sprintf(buf, "%d", ibp->fd);
-	    write(cnt_fd, buf, strlen(buf) + 1);
 	  }
 	}
       }
+      /*
+       * GDBスタブ利用の場合はスタブ側で read() されてしまい上の select() で
+       * 検知できないので,子プロセスへの割り込み再開通知を毎回行う.
+       */
+      write(cnt_fd, "refresh", 8);
     } else if (fd) { /* from thread */
       setintrbuf(fd, id, (struct sockaddr *)p);
       /*
従来は,子プロセス側はソケットが読み取り可になると監視対象から外し, 親プロセスのほうでソケットを読み込んだ後にソケット番号を通知し, 子プロセス側で再度監視対象に加えるという処理を行っていた. しかしGDBスタブ用のソケットが読み取り可になった場合, スタブの内部で read() によって読み込まれてしまうため 親プロセス側ではソケットを(すでに read() 済なので)読み取れず, ソケット番号を返さないために子プロセス側でずっと監視対象から外したままになる, という問題があった.まあスタブ用ソケットに関してはべつに子プロセスで監視して もらう必要性が薄いために問題はあまり無いのだけど, 実は Ctrl-C の受け付け処理を stubd.c が行っていて, このままだとこれがうまく動作できないのだなたしか.(スタブ動作中も子プロセスは ソケット監視して SIGHUP を送るので,監視対象から外れてしまう)

なので対策として,子プロセスが SIGHUP を発行した後(親プロセスがKOZOSから メッセージを受け取ったとき)にはソケットを読んだかどうかにかかわらず refresh というコマンドを送信して,すべてのソケットを監視対処に戻すようにする.

最後に,シグナルの設定を変更.

       }
       pipe(fildes);
       if ((chld_pid = fork()) == 0) { /* 子プロセス */
-#if 0
-	signal(SIGALRM, SIG_IGN);
-#endif
+	/*
+	 * 子プロセスはシグナル設定を引き継ぐらしいので,シグナル無効にする.
+	 * (man sigaction に以下の記述あり)
+	 * After a fork(2) or vfork(2) all signals, the signal mask, the signal
+	 * stack, and the restart/interrupt flags are inherited by the child.
+	 */
+	sigprocmask(SIG_BLOCK, &block, NULL);
         close(fildes[1]);
         intr_controller(fildes[0]);
       }
まあコメントにもあるのだけど,どうも子プロセスはシグナルの設定を 引き継ぐらしい.なので子プロセス側で SIGALRM とかが発生するとKOZOSの処理が 行われることになり,ちょっとまずい(スレッドのディスパッチが発生して スレッドが動いてソケット操作とかしてしまうとすごくまずい). そもそも子プロセスはソケットを監視するだけでKOZOSのサービスは一切不要なので, シグナルをブロックしてしまう処理を追加する.

説明はここまで.動作は前回と同じなので説明は省略. まあとりあえず,前回と同じような操作をして同じようにgdbから操作できたことは 確認した.

で,今回まででKOZOSの枠組というか,おおまかな部分が大体できあがって, 必要なサービス(システムコール)もそれなりに揃っているとおもうのだけど, いったんファイル一覧を見てみよう.

% ls -l
total 118
-rw-r--r--  1 hiroaki  user    460 11 12 00:30 Makefile
-rw-r--r--  1 hiroaki  user    346 11 12 00:30 clock.c
-rw-r--r--  1 hiroaki  user    104 11 12 00:30 configure.h
-rw-r--r--  1 hiroaki  user   6465 11 12 00:31 diff.txt
-rw-r--r--  1 hiroaki  user   4441 11 12 00:30 extintr.c
-rw-r--r--  1 hiroaki  user   2073 11 12 00:30 httpd.c
-rw-r--r--  1 hiroaki  user  25110 11 12 00:30 i386-stub.c
-rw-r--r--  1 hiroaki  user  24834 11 12 00:30 i386-stub.c.orig
-rw-r--r--  1 hiroaki  user    193 11 12 00:30 idle.c
-rw-r--r--  1 hiroaki  user   1133 11 12 00:30 kozos.h
-rw-r--r--  1 hiroaki  user    657 11 12 00:30 main.c
-rw-r--r--  1 hiroaki  user    221 11 12 00:30 outlog.c
-rw-r--r--  1 hiroaki  user   1987 11 12 00:30 stubd.c
-rw-r--r--  1 hiroaki  user   3823 11 12 00:30 stublib.c
-rw-r--r--  1 hiroaki  user    252 11 12 00:30 stublib.h
-rw-r--r--  1 hiroaki  user   2739 11 12 00:30 syscall.c
-rw-r--r--  1 hiroaki  user   1538 11 12 00:30 syscall.h
-rw-r--r--  1 hiroaki  user    692 11 12 00:30 t2w.c
-rw-r--r--  1 hiroaki  user   2707 11 12 00:30 telnetd.c
-rw-r--r--  1 hiroaki  user  10715 11 12 00:30 thread.c
-rw-r--r--  1 hiroaki  user    764 11 12 00:30 thread.h
% 
KOZOSのコア部分は thread.c なのだけど,たったの 764 バイト, 行数は 508 行となる.まあ他にも動作に必須のものとして, 外部割り込み関連が extintr.c, システムコールが syscall.c, アイドルスレッドが idle.c, スタブ処理が stubd.c と stublib.c にあるが, これらを全部合わせても行数は1113行,ヘッダファイル (configure.h, kozos.h, stublib.h, syscall.h, thread.h) を合わせても1316行なのだな.2000行にも満たないのよ.まあエラー処理とかが ほとんど無いし,問題点や怪しい部分もまだまだありありなのだけど, こんなもんでOSって動いてしまうんだねえ...うーん,面白いもんだなあ.

あとは,kz_break() によるブレーク処理の修正, Ctrl-C の動作(stubd.cの動作)の説明, スレッド対応とかが必要かなあ.まあもう眠いので次回にしよう.


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