(第30回)リアルタイム性について考える(その5:割り込みハンドラ登録の準備)

2007/12/17

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

今回は,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では,割り込み処理は割り込みハンドラが行うものだ.ということで今回は, 割り込み発生時に登録した関数を呼び出してくれるような,割り込みハンドラの 登録用のシステムコールを追加してみた.

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

修正点について,以下に説明する. まあ今回はシステムコールを追加しただけなのであまり説明するようなことは無い. 本来ならば割り込みハンドラを利用するように 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などがひととおり従来通り 動くことは確認した.

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


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