(第4回)タイマを実装してみる

2007/10/21

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

前回は主にスレッド間でのメッセージ通信を実装した. 今回は,タイマを実装してみよう.

タイマは以下のように実装してみる.

つまり,一定時間待ちたいような場合には, kz_timer()で待ち時間を指定して,直後に kz_recv() でメッセージ待ちに入ればよい. kz_timer() だけではタイマをかけるだけでメッセージ待ちは行わないので, スレッドはそのまま走り続けてしまうことになる. kz_timer() は即時終了する,ということだ.

実はタイマの実装方法についてはちょっといろいろ考えたところがあって, KOZOSにシグナルのような機構を導入しタイマ満了時にはシグナルが 送信されるようにする(kz_recv()はシグナルによって終了する)とか, kz_timer()が即終了するのでなくタイマ満了時に kz_timer() が終了する (つまり,kz_timer()はタイマ設定と同時にメッセージ待ちに入る)とかいった 実装も考えたのだけど,まあメッセージの送受信をすでに実装しているので, その作りを流用するのが楽でいいかな,と.

で,作成したのが以下.

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

以下は前回からの差分.

システムコールの追加方法に関しては前回と同様なのでとくに説明しない. タイマの実装は thread.c にまとまっているので,そこだけ説明しよう.

タイマは複数設定されることがあり得るので, タイマ構造体によってリンクリスト管理する. このためには thread.c の先頭付近で

diff -ruN -U 10 kozos03/thread.c kozos04/thread.c
--- kozos03/thread.c	Sun Oct 21 22:32:45 2007
+++ kozos04/thread.c	Sun Oct 21 22:32:45 2007
@@ -5,23 +5,30 @@
 #include 
 #include 
 
 #include "kozos.h"
 #include "syscall.h"
 #include "thread.h"
 
 #define SIG_NUM 32
 #define STACK_SIZE 0x8000
 
+typedef struct _kz_timebuf {
+  struct _kz_timebuf *next;
+  int msec;
+  kz_thread *thp;
+} kz_timebuf;
+
 kz_thread threads[THREAD_NUM];
 kz_thread *readyque[PRI_NUM];
 static jmp_buf intr_env;
+static kz_timebuf *timers;
 
 kz_thread *current;
 
 static void getcurrent()
 {
   readyque[current->pri] = current->next;
   current->next = NULL;
 }
 
 static int putcurrent()
のようにして構造体 kz_timebuf と,そのリンクリスト timers を定義してある. 構造体 kz_timebuf には,タイマの起動時間(ミリ秒)と タイマをかけたスレッドのID(タイマ満了時にメッセージを送信する先) を保持している.

kz_timer()によるタイマ設定時には,thread_timer() が呼ばれる. タイマでちょっと難しいのは,たとえば

という点だ.たいていハードウエアはいくつかのタイマ資源 (起動時間を設定しておくと,その時間が経過したときに割り込みが入る) を持っていたりするものだが,タイマをひとつしか使わないとすると, このように時間の減算やソート,タイマ設定の上書きなどが必要になってくる. そういう心づもりで,以下の thread_timer() を見てほしい.
static int thread_timer(int msec)
{
  kz_timebuf **tmpp;
  kz_timebuf *tmp;

  tmp = malloc(sizeof(*tmp));
  tmp->next = NULL;
  tmp->thp = current;

  for (tmpp = &timers; *tmpp; tmpp = &((*tmpp)->next)) {
    if (msec < (*tmpp)->msec) {
      (*tmpp)->msec -= msec;
      break;
    }
    msec -= (*tmpp)->msec;
  }

  if (msec == 0) msec++;
  tmp->msec = msec;
  tmp->next = *tmpp;
  *tmpp = tmp;

  ualarm(timers->msec * 1000, 0);

  putcurrent();
  return 0;
}
タイマ設定時には,現在のタイマキューを検索し,常にソートされた状態で キューに挿入される.

さらに kz_timer() に引数に渡されるのは 「現在から何ミリ秒後にタイマが起動するか?」という 値だが,キューへの挿入時には 「前回のタイマの満了時から何ミリ秒後にタイマ起動するか?」 という値にして保存する (キューの next ポインタをたどるたびに,タイマ時間で msec を減算している).

キューへの接続が終ったら, タイマキューの先頭にあるタイマ情報を見て ualarm() を起動する. これにより,一番最初に満了すべきタイマ値で ualarm() が行われ, そのタイマ時間が経過したときに SIGALRM が発生することになる. タイマが既にかかっている状態で新しい(もっと短い時間の)タイマが設定された 場合には,新しいタイマ値でタイマの設定を上書きする必要があるが, ここで無条件で ualarm() による設定を行っているので, 必要ならばタイマの上書きが行われることになる.

※ あー,っていうか今気がついたのだけど,これってタイマが既に起動している 状態で kz_timer() が呼ばれると,ualarm() の再設定が行われて タイマ時間が戻ってしまうってことだよね...(例えば100msのタイマを仕掛けて 50msたった状態で200msのタイマを設定すると,100msタイマが再設定されて またゼロからカウントされることになる.よって100msタイマなのに実際に タイマ起動するのは150ms後,ということになる) これを防止するには上書きを止めるだけではダメで,thread_timer()呼び出し時には gettimeofday() とかで現在時刻を取得して,前回のタイマ起動時間からの差分を 考慮しないといけないということだ... うーん面倒ではないけど,もうここまで文章書いちゃったので, まあそのうち対処しよう.

ということで,タイマ満了時には SIGALRM が発行される. これをハンドリングするために,KOZOSの起動部分で SIGSYS と同様にハンドラを 登録しておく.

@@ -301,21 +355,24 @@
 static void thread_start(kz_func func, char *name, int pri, int argc, char *argv[])
 {
   memset(threads, 0, sizeof(threads));
   memset(readyque, 0, sizeof(readyque));
 
+  timers = NULL;
+
   signal(SIGSYS, thread_intr);
+  signal(SIGALRM, thread_intr);
 
   /*
    * current 未定のためにシステムコール発行はできないので,
    * 直接関数を呼び出してスレッド作成する.
    */
   current = NULL;
   current = (kz_thread *)thread_run(func, name, pri, argc, argv);
 
   longjmp(current->context.env, 1);
 }
さらに SIGALRM 発生時には,タイマ処理のために alarm_handler() が呼ばれるように 割り込みベクタの疑似処理である thread_intrvec() に SIGALRM を追加しておく.
 static void thread_intrvec(int signo)
 {
   switch (signo) {
   case SIGSYS: /* システムコール */
     syscall_proc();
     break;
+  case SIGALRM: /* タイマ割込み発生 */
+    alarm_handler();
+    break;
   case SIGBUS: /* ダウン要因発生 */
   case SIGSEGV:
   case SIGTRAP:
   case SIGILL:
     {
       fprintf(stderr, "error %s\n", current->name);
       /* ダウン要因発生により継続不可能なので,スリープ状態にする*/
       getcurrent();
     }
     break;
最後にタイマ満了時のメッセージ送信処理だ. タイマ満了時には,タイマをかけたスレッドに対してメッセージが投げられる. これは alarm_handler() で行われる.
void alarm_handler()
{
  kz_timebuf *tmp;

  sendmsg(timers->thp, 0, 0, NULL);
  tmp = timers;
  timers = timers->next;
  free(tmp);
  if (timers) {
    ualarm(timers->msec * 1000, 0);
  }
}
sendmsg()によりメッセージ送信(当該スレッドのメッセージキューにメッセージが 積まれ,スレッドが kz_recv() による受信待ちならば wakeup 処理が行われる)し, 満了したタイマの資源を解放する.さらに次のタイマ設定が存在するならば, ualarm() によってタイマを再設定する. sendmsg() への引数はスレッドへのポインタの後に 0,0,NULL と続いているので, メッセージを受信するスレッドは, 送信元スレッドのIDはゼロ,サイズはゼロ,データへのポインタはNULLという メッセージを受信することになる. KOZOSではメッセージ主体の制御になるのだが (今回のタイマにしても,満了時にはシグナルが発生するのでなく, OSからメッセージが投げられてくる), OSから投げられるメッセージは送信元スレッドIDをゼロという設計になっている.

では,サンプルプログラムによって動作確認してみよう.

サンプルプログラムでは,2つのスレッドが
  while (1) {
    kz_timer(100);
    fprintf(stderr, "func1\n");
    kz_recv(NULL, NULL);
  }
のようにしてタイマをかけてから kz_recv() でタイマ満了を待つ, という処理になっている. kz_timer() によるタイマの設定値は,100msと150msだ. kz_recv() では引数に NULL,NULL を渡しているが, これはタイマ満了時にKOZOSから送信される空メッセージの受信待ちのため, 送信元スレッドIDとデータへのポインタを返してもらう必要が無いからだ.

実行結果は以下のようになる.

% ./koz 
func1
func2
func1
func2
func1
func2
func1
func1
func2
func1
func2
func1
^C
% 
2つのスレッドが交互にメッセージを受信している. func1 のほうが連続して受信している場合があるが,これはタイマ設定に 100ms,150msという歳があるためだ.だったらメッセージの出力数が 3:2 の 割合になってもいい気がするが,前述したように kz_timer() によるタイマ設定が 行われると,現在起動中のタイマまでリスタートしてしまうという問題があるため, 比率がずれてしまっているのだろう.
メールは kozos(アットマーク)kozos.jp まで