(第1回)まずはてきとうに作ってみる

2007/10/18

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

とりあえず,独自OSといわれても,どんなものか実感がわかないよね... ホントは今回やることをきちんと説明してからソースコードの紹介, と順序を踏みたいのだけれど,まずはイメージがわかないと説明聞いても よくわからんと思うので, まずはスレッド切替えだけの単純なモノ(メッセージ通信すら無い)を書いてみて, 動作を確認してみよう.

以下がソースコードです.

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

で,動作させるためにはOSだけではなくてサンプルプログラムが必要なのだけど, とりあえず以下の main1.c を使ってみる.

まずはディレクトリを作成してこれらのファイルを置いて, main1.c を main.c にリネームして,make してみよう. ちなみにぼくの動作環境は FreeBSD-6.x です. 実は内部で setjmp() 用のバッファを直接操作しているので, Linux だとちょっとどうなるか不明です.
% make
cc -c thread.c  -g -Wall -static
cc -c syscall.c  -g -Wall -static
cc -c main.c  -g -Wall -static
cc thread.o syscall.o main.o -o koz  -g -Wall -static
% 
これで実行形式 koz ができるので,実行してみます.
% koz
main start
thread 1 started
thread 2 started
mainfunc loop 0
mainfunc loop 1
mainfunc end
func1 start 1 ./koz
func1 loop 0
func2 start 1 ./koz
func2 loop 0
func1 loop 1
func2 loop 1
func1 end
func2 end
% 
どうでしょう? 同じ結果になったでしょうか?

現段階では,KOZOSは簡単なスレッド切替えしか行ってくれません. しかしスレッド切替え(コンテキストスイッチ)を行うということは, そのスレッドのコンテキストをスレッド単位で管理しなければなりません. つまり,以下をスレッド単位に持たせる必要があります.

問題は,スタックとレジスタ情報の切替えをどうするかです. これが通常の組み込みOSならば,割り込み契機でレジスタの退避を行い, 割り込み完了命令(たいていのCPUが持っています)を実行すれば, レジスタの復帰を一気に行ってくれる,ということになります. 同じようなことをやろうとすると,レジスタの操作が必要になるため, アセンブラで書かなければなりません.うーん面倒だなどうしようかなーと 考えたのですが,実は同じようなことを行う setjmp()/longjmp() というものがあり, これを使えばコンテキストスイッチができるのでは...というのが, 実は KOZOS 製作の第一歩でした.

KOZOS の本体は thread.c にあります. また KOZOS 上で動作するアプリは main1.c になります. main1.c では,main()関数の先頭で kz_start() により KOZOS に処理を渡します. つまり,KOZOS で最初に呼ばれるのは kz_start() です.

kz_start() は thread.c にあります.以下,抜粋です.

void kz_start(kz_func func, char *name, int pri, int argc, char *argv[])
{
  int signo;

  /*
   * setjmp()は最低位の関数から呼ぶ必要があるので,本体は thread_start() に
   * 置いて setjmp() 呼び出し直後に本体を呼び出す.
   * (setjmp()した関数から return 後に longjmp() を呼び出してはいけない)
   */
  if ((signo = setjmp(intr_env)) == 0) {
    thread_start(func, name, pri, argc, argv);
  }
  thread_intrvec(signo);

  /* ここには返ってこない */
  abort();
}
kz_start() ではいきなり setjmp() を行い, 割り込み(実体はシグナル)発生時のコンテキストとして intr_env を作成します. これは setjmp() がゼロとして返ってくるので,その後は thread_start() が 呼ばれることになります.ではその下の thread_intrvec() はいつ呼ばれるのか? これは,割り込み(実体はシグナル)発生時に intr_env を使って longjmp() する ことで,上の setjmp() の位置にジャンプしてきます.この際には setjmp() の戻り値にはシグナル番号を渡されるので,if 文の下に抜けて thread_intrvec() が呼ばれることになります. thread_intrvec() は割り込みハンドラであり, 割り込み(しつこいけど,実体はシグナル)の種類に応じて適切な処理を行います.

つまり intr_env は,「割り込みハンドラを実行するためのコンテキスト」 ということになります.言いかたを変えると,kz_start() 起動時のコンテキスト (スタックと,レジスタの状態)を intr_env に保存しておき, 割り込み発生時にはそのコンテキストを利用して割り込みを処理します.

と,ここまでざーーーーっと書いてしまったけど,うーん,わかりにくいよねえ... 「コンテキスト」というのは,実行するための環境というか,資源のことね. 平たくいうと,スタックとレジスタの状態だと思ってくれればいい.

まあ説明を先に進めてしまうと,初期化時には kz_start() から thread_start() が 呼ばれます.

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));

  signal(SIGSYS, thread_intr);

  /*
   * current 未定のためにシステムコール発行はできないので,
   * 直接関数を呼び出してスレッド作成する.
   */
  current = NULL;
  current = (kz_thread *)thread_run(func, name, pri, argc, argv);

  longjmp(current->context.env, 1);
}
まずはスレッドの管理用配列(threads)と,レディーキュー(readyque)を 初期化しています.threads はスレッド管理用の構造体(kz_thread)の配列です. レディーキューについては後述します.

signal()によって,thread_intr() を SIGSYS のハンドラとして登録しています. つまり,シグナル SIGSYS の発生時には thread_intr() が呼ばれるわけです. SIGSYS って何? どんなときに発生するの? と思いますよね... 実は KOZOS では,システムコール呼び出しは SIGSYS の発行によって行われます. つまり SIGSYS を発行するのは,システムコールを要求するアプリ自身です. まあこのへんについてはあとで詳しく説明します.

その後に,thread_run() によりスレッドを作成します.この時点で func を メイン関数とするスレッドが作成,というか threads[] の配列に登録されます.

で,最後にキモとなる longjmp() の呼び出しがあります.

longjmp() が呼ばれて,どこに飛んでいくのか? longjmp() の引数は,current->context.env です. current はカレントスレッド(現在動作中のスレッド)です. カレントスレッドは thread_run() によるスレッド作成時に, 今まさに作成されたスレッドになっています.

thread_run() によるスレッド作成では,そのスレッドを動作させるための スタックの確保とレジスタの設定を,構造体 kz_thread の context.env という メンバに対して行っています.

で,longjmp() を呼ぶことで,そのスレッドのコンテキストに切り替わり, メイン関数に飛んでいくというしくみです. 別のいいかたをすると,context.env を引数にして longjmp() が呼ばれたら メイン関数に飛んでくるように,thread_run() の内部で context.env を 初期化している,ということです.

次に,スレッドの作成用の関数である thread_run() について説明します. ちょっと長めの関数なので,分割して説明します.

static int thread_run(kz_func func, char *name, int pri,
                      int argc, char *argv[])
{
  int i;
  char *sp;
  kz_thread *thp;

  for (i = 0; i < THREAD_NUM; i++) {
    thp = &threads[i];
    if (!thp->id) break;
  }
  if (i == THREAD_NUM) return -1;

  memset(thp, 0, sizeof(*thp));

  thp->next = NULL;
  strcpy(thp->name, name);
  thp->id = thp;
  thp->func = func;
  thp->pri = pri;
まずは配列 threads[] から空いてる領域をもらいます. スレッド管理をリンクリストにしなかったのはあまり意味はないのだけれど, まあそのほうがわかりやすいかな,というのと,スレッド構造体はひじょーに 重要なので,デバッグのことなども考えて, 配列で固定で持っておきたかったというのがあります.
  thp->stack = malloc(STACK_SIZE);

  memset(thp->stack, 0, STACK_SIZE);

  sp = thp->stack + STACK_SIZE - 32;
malloc() によってスタックを確保します. 当り前のことなのですが,自分で確保したスタックの上で動作することも できるもんなんですねぇ.

i386ではスタックは下方伸長なので,アドレスの大きなほうにスタックポインタを 合わせておきます. STACK_SIZEは 0x8000 なので32kBとちと大きめなような気もするのですが, 実は製作当初は4kBくらいだったのですが,後々スタック不足で誤動作したので, 大きめにしておきました.最低でも16kB程度は必要なようです.

  /* メイン関数終了時の戻り先 */
  ((int *)sp)[1] = (int)thread_end;

  /* スタック上に引数(argc,argv)を準備する */
  ((int *)sp)[2] = (int)thp;
  ((int *)sp)[3] = argc;
  ((int *)sp)[4] = (int)argv;
このへんからがスレッド初期化のキモとなる部分です. spはスタックポインタです. i386のスタック構造では, 引数をお尻から順にスタックに格納し,一番上に戻り先アドレスを格納して 関数呼び出しを行います. 実際の値設定はスタックが下方伸長なので,上記のようになります. 戻り先には thread_end() を指定しているので,スレッド終了時 (スレッドのメイン関数から抜けたとき)には,thread_end() が呼ばれます. 実は thread_end() は
static void thread_end()
{
  kz_exit();
}
のようになっていて,kz_exit() システムコールによってスレッドを消去します. このためスレッドのメイン関数が終了すれば,自動的にスレッドは消滅することに なります.

引き続き,thread_run() の説明です.

  /*
   * 以下の設定については setjmp()/longjmp()
   * (/usr/src/lib/libc/i386/gen)を参照.
   */
#if 1
  thp->context.env[0]._jb[0] = (int)thread_init;   /* EIP */
  thp->context.env[0]._jb[1] = intr_env[0]._jb[1]; /* EBX */
  thp->context.env[0]._jb[2] = (int)sp;            /* ESP */
  thp->context.env[0]._jb[3] = intr_env[0]._jb[3]; /* EBP */
  thp->context.env[0]._jb[4] = intr_env[0]._jb[4]; /* ESI */
  thp->context.env[0]._jb[5] = intr_env[0]._jb[5]; /* EDI */
  /* thp->context.env[0]._jb[6] = ??? */
#else
  memcpy(thp->context.env, intr_env, sizeof(intr_env));
  thp->context.env[0]._jb[0] = (int)thread_init; /* EIP */
  thp->context.env[0]._jb[2] = (int)sp;   /* ESP */
#endif
ここで,コンテキストの初期化を行っています. 実体は setjmp()/longjmp() 用のバッファである jmp_buf の初期化です.

FreeBSD では,setjmp()/longjmp() は /usr/src/lib/libc/i386/gen/setjmp.S にあり,ここを見れば jmp_buf の構造というか利用方法がわかります. ここで重要なのは,実行再開番地とスタックポインタ(ESP)です. 見たところ,_jb[0]に実行再開番地,_jb[2]にスタックポインタであるESPが 格納されるようですのでそうします (このへんはOS依存(というか setjmp()/longjmp() の実装依存)だと思うので, Linux だとどうなるかわかりません. ていうか多分そのままでは Linux では動かないと思う. 必要なら setjmp()/longjmp() のソースを見て判断してください). 他レジスタの値はたいして重要ではないですが, アドレスベースとなるようなレジスタがある...と思う (実はi386のレジスタ構成をよく知らない)ので, 最初に作成した intr_env の値をコピーしておきます.

ちなみに setjmp()/longjmp() ではシグナルマスク情報も引き継がれます. このへんの情報は,_jb[6] 以降に入っているような気がします(未確認ですが). なので _jb[6] 以降もコピーするかどうかはちょっと微妙です.

  /* 起動時の初回のスレッド作成では current 未定なのでNULLチェックする */
  if (current) {
    putcurrent();
  }

  current = thp;
  putcurrent();

  return (int)current;
}
最後に,カレントスレッドの設定をします. 起動時は current==NULL となっているので,上記の if 文は実行されません. 上記 if 文はシステムコールとして thread_run() が呼ばれた際に, 呼んだもとのスレッドをレディーキューに繋ぐための処理です. なので起動時は不要です.

作成したスレッドを current に代入することでカレントスレッドとし, putcurrent() によってレディーキューに繋ぎます.

static int putcurrent()
{
  kz_thread **thpp;
  for (thpp = &readyque[current->pri]; *thpp; thpp = &((*thpp)->next)) {
    /* すでに有る場合は無視 */
    if (*thpp == current) return -1;
  }
  *thpp = current;
  return 0;
}
putcurrent()では,レディーキューのリンクリストの配列にカレントスレッドを 繋ぎます.すでにレディーキューに繋がれているならば,なにもせずに -1 を 返します. レディーキューはスレッドの優先度ごとの配列になっています. スレッドのディスパッチ時には,優先度の数値の低い順にレディーキューを 検索し,一番最初に見つかったスレッド(すなわち,最も優先度の高いスレッド) が次のカレントスレッドとなります.

ここまでで,スレッドが作成され,新しいスレッドのためのコンテキスト (スタックとレジスタ情報)によってスレッドが動作し始めます. 実際には main1.c のメイン関数で最初に kz_start() を呼び出していましたが, そのときの引数であった mainfunc() 関数が,KOZOS上で動くアプリとして, ひとつのスレッドとして動作を開始します.

さて,これでスレッドが動作開始したのですが, 次にOSが動作するタイミングは何でしょうか? それはアプリがOSに何かお願いをしたとき, つまり,システムコールを呼んだときですね.

KOZOSでは現段階では5つのシステムコールを用意しています.

これらは syscall.h で定義されていますが, サービス関数として表現したほうがわかりやすいかもしれません.
int kz_run(kz_func func, char *name, int pri, int argc, char *argv[]);
void kz_exit();
int kz_wait();
int kz_sleep();
int kz_wakeup(int id);
kz_run(), kz_exit() はまあそのまんまの意味で,スレッドの起動と終了です. UNIXでいうと,fork() と exit() に相当する,というと,実際は全然違うけど, まあ,イメージが湧きやすいかもしれません.

ちょっとわかりにくいのは kz_wait() です.これはスレッドのディスパッチを 行うだけで何もしません.たとえば同じ優先度で動作しているスレッドが 複数あった場合に,片方が kz_wait() を行うと,もう片方のスレッドに処理が 渡ります.同じ優先度で動作しているスレッドが存在しない場合には, kz_wait()によりディスパッチを行ったところで,再び自分がカレントスレッドに なるだけなので,ほんとになにも起きないことになります.

kz_sleep() はスレッドの動作を停止します. 他のスレッドが停止中のスレッドに対して kz_wakeup() を行うと, そのスレッドは動作を再開します. 実際にはレディーキューから外す処理と,再び繋ぐ処理を行うだけです.

システムコールの実際を見てみましょう. システムコール用のサービス関数は syscall.c にまとめられてありますが, 実際には kz_syscall() が呼ばれます.

void kz_syscall(kz_syscall_type_t type, kz_syscall_param_t *param)
{
  current->syscall.type = type;
  current->syscall.param = param;
  kill(getpid(), SIGSYS);
  return;
}
引数を用意して,SIGSYS を発行します. で,ここで kz_start() 実行時の最初に行った, thread_intr() のハンドラ登録が効くのです.

SIGSYS 発行により,thread_intr() が(まるでソフトウエア割り込みのように) 呼ばれます. ここでシステムコール処理用の関数をいきなり呼ぶ,という方法もあるのですが, 動作中のスレッドのコンテキスト(というか,スタック)が利用されてしまうため (シグナルの設定によってはハンドラ用に別スタックを利用させることも可能. 詳しくは後日.まあここではそんなものだと軽く思ってください)が使われて しまうため,なんかイマイチです. というのは疑似的な割り込みとしてシグナルを利用していますが, 割り込みというのは,割り込み発生時のスレッドのコンテキスト(主にスタック)には 影響を与えずに,スレッド側からすれば割り込みが発生したことなど知ることもなく, 終了すべきものだからです.なので,割り込み発生時のスレッドのスタックを利用して 割り込み処理を行うというのは,スレッド側からすればスタックの未使用領域が 突然汚れることになり,今後実装する予定の外部割り込みなども考えると, ちょっとイマイチな実装です.

なので,起動時に作成しておいた割り込み処理用のコンテキストである intr_env を利用して,割り込み処理に入ります.

static void thread_intr(int signo)
{
  /*
   * setjmp()/longjmp() はシグナルマスクを保存し復元するが,
   * _setjmp()/_longjmp() はシグナルマスクを保存しない.
   * (レジスタセットとスタックしか保存および復元しない)
   */
  if (setjmp(current->context.env) == 0) {
    longjmp(intr_env, signo);
  }
}
まあ実はハンドラ内部から setjmp()/longjmp() を呼び出しているので, 割り込み発生時のスレッドのスタックは多少なりとも汚れてしまいます. これに関しては,後日また改良します.

ちなみにシステムコール用に SIGSYS を選んだ理由ですが,とくに特別な理由は ありません.SIGHUP でもいいし,SIGTERM とか SIGUSR1 でもいいと思います. まあ名前がシステムコールっぽい(シグナルの実際の意味は違うのだけど)のと, SIGHUPとかは他で使いたいこともあるだろうという理由で SIGSYS に してしまいました.

setjmp()により,割り込み発生時のスレッドのコンテキストを保存します. で,longjmp() により割り込み処理用のコンテキストに切り替わります. longjmp() では intr_env を引数としていますが, これは実は kz_start() による KOZOS 起動時に最初に setjmp() によって コンテキスト作成しているので,kz_start() の setjmp() 部分にジャンプします.

void kz_start(kz_func func, char *name, int pri, int argc, char *argv[])
{
  int signo;

  /*
   * setjmp()は最低位の関数から呼ぶ必要があるので,本体は thread_start() に
   * 置いて setjmp() 呼び出し直後に本体を呼び出す.
   * (setjmp()した関数から return 後に longjmp() を呼び出してはいけない)
   */
  if ((signo = setjmp(intr_env)) == 0) {
    thread_start(func, name, pri, argc, argv);
  }
  thread_intrvec(signo);

  /* ここには返ってこない */
  abort();
}
シグナルハンドラ(thread_intr())からはシグナル番号を引数として longjmp() が行われるため,上記の if 文は実行されず,割り込みベクタである thread_intrvec() が呼ばれます.
static void thread_intrvec(int signo)
{
  switch (signo) {
  case SIGSYS: /* システムコール */
    syscall_proc();
    break;
  case SIGBUS: /* ダウン要因発生 */
  case SIGSEGV:
  case SIGTRAP:
  case SIGILL:
    {
      fprintf(stderr, "error %s\n", current->name);
      /* ダウン要因発生により継続不可能なので,スリープ状態にする*/
      getcurrent();
    }
    break;
  default:
    break;
  }
  dispatch();
  longjmp(current->context.env, 1);
}
現在はシステムコールとして SIGSYS が発行されているため, システムコールの処理関数である syscall_proc() に処理が渡ります.
static void syscall_proc()
{
  /* システムコールの実行中にcurrentが書き換わるのでポインタを保存しておく */
  kz_syscall_param_t *p = current->syscall.param;

  getcurrent();

  switch (current->syscall.type) {
  case KZ_SYSCALL_TYPE_RUN:
    p->un.run.ret = thread_run(p->un.run.func, p->un.run.name, p->un.run.pri,
                               p->un.run.argc, p->un.run.argv);
    break;
  case KZ_SYSCALL_TYPE_EXIT:
    /* スレッドが解放されるので戻り値などを書き込んではいけない */
    thread_exit();
    break;
  case KZ_SYSCALL_TYPE_WAIT:
    p->un.wait.ret = thread_wait();
    break;
  case KZ_SYSCALL_TYPE_SLEEP:
    p->un.sleep.ret = thread_sleep();
    break;
  case KZ_SYSCALL_TYPE_WAKEUP:
    p->un.wakeup.ret = thread_wakeup(p->un.wakeup.id);
    break;
  default:
    break;
  }
  return;
}
syscall_proc()では,システムコールの内容に応じて,処理用の関数が呼ばれます. システムコールの実行結果は,各関数の戻り値として, p->un.XXXX.ret に格納されます.実はシステムコールを行うとスレッドの ディスパッチが行われるため,戻り値は(グローバル変数を使うなどして) そのままスレッドには返せません.なのでシステムコール呼び出し時の パラメータ領域(これはシステムコールのサービス関数によってスレッドのスタック上に 確保されているので,スレッドごとに存在する)に戻り値を保存しておきます.

syscall_proc() の先頭で getcurrent() を行っていることに注目してください. これはカレントスレッドを,レディーキューから抜きます. つまりこの時点で,カレントスレッドはスリープ状態 (レディーキューに繋がれていないため,ディスパッチを行っても引っかからないので 動作できない状態)になっています.

static void getcurrent()
{
  readyque[current->pri] = current->next;
  current->next = NULL;
}
getcurrent() では,レディーキューの先頭からカレントスレッドを抜き取ります. 一見して,リンクリストの検索処理を行わなくていいのか?と疑問に思って しまいそうですが,カレントスレッドは前回のディスパッチにより レディーキューの先頭にあるスレッドが選択されているはずなので, 必ず先頭にあるため,これでいいことになります.

ここで,各システムコールの処理関数について説明しておきます.

まずはスレッド作成のシステムコールである kz_run() の処理用関数の thread_run() ですが,すでに説明済みのために割愛します.ただ1点,thread_run() の内部で

  /* 起動時の初回のスレッド作成では current 未定なのでNULLチェックする */
  if (current) {
    putcurrent();
  }

  current = thp;
  putcurrent();
のようにしている部分がありました. この段階ではカレントスレッドは kz_run() を「呼び出した」スレッドに なっています.なので,putcunrent() によりレディーキューに繋ぎます.

syscall_proc() の先頭で getcurrent() を行っていることを思い出してください. この時点では,カレントスレッドはレディーキューに繋がれていないため, ディスパッチを行っても見つからない状態(スリープ状態)です. よってこれを行わないと,kz_run()を呼び出すとスレッドが固まる (スリープしたまま),ということになります.

さらに新規に作成したスレッドをカレントスレッドとして putcurrent() を 呼ぶことで,やはりレディーキューに繋ぎます. まあ実はカレントスレッドはこの後のディスパッチ処理で変更されるため current に設定する意味は無いのですが,putcurrent() を current に対して 処理を行うように書いてしまったので,このようになっています (引数取るようにすればよかったね...まあいいや).

次に,スレッドの終了処理です.

/* スレッドの終了 */
static int thread_exit()
{
  free(current->stack);
  memset(current, 0, sizeof(*current));
  return 0;
}
カレントスレッドを解放するだけですね. current はこの後のディスパッチによりやはり変更されるため, 解放済みのスレッドを指し続けるようなことはありません.

次に,スレッドのディスパッチを行うだけで何もしないという, よくわからんシステムコールの kz_wait() の処理です.

/* スレッドの実行権放棄(同一priの別スレッドに実行権を渡す) */
static int thread_wait()
{
  putcurrent();
  return 0;
}
getcurrent() によって抜かれたカレントスレッドを,putcurrent() によって 繋ぎなおすだけです.ただし putcurrent() はレディーキューのリンクリストの お尻に繋ぐため,同一優先度に別スレッドが存在する場合には,今度はそちらが ディスパッチされて動作が渡ることになります.

次に,スリープです.

static int thread_sleep()
{
  return 0;
}
何もしません.getcurrent()によりスレッドはレディーキューから抜かれているので, そのままにしておけばスリープすることになります.

次に,wakeup です.

static int thread_wakeup(int id)
{
  putcurrent();
  current = (kz_thread *)id;
  putcurrent();
  return 0;
}
引数として渡されたスレッドをカレントスレッドとし,putcurrent() を行うことで レディーキューに繋ぎ直します.これにより再び動作を再開することになります. putcurrent() を2回行っていますが,前者は kz_wakeup() を呼び出した側の スレッドをレディーキューに繋ぐためのものです. これを行わないと,kz_run() で説明したのと同様に, kz_wakeup()を呼び出すと,呼ばれた側のスレッドは動作再開するが, 呼び出した側のスレッドはスリープしてしまう,ということになります.

ここまでで各システムコールの処理について説明しましたが, 各処理を行った後にはスレッドのディスパッチが行われます. 再び thread_intrvec() に戻ります.

static void thread_intrvec(int signo)
{
  switch (signo) {
  case SIGSYS: /* システムコール */
    syscall_proc();
    break;
  case SIGBUS: /* ダウン要因発生 */
  case SIGSEGV:
  case SIGTRAP:
  case SIGILL:
    {
      fprintf(stderr, "error %s\n", current->name);
      /* ダウン要因発生により継続不可能なので,スリープ状態にする*/
      getcurrent();
    }
    break;
  default:
    break;
  }
  dispatch();
  longjmp(current->context.env, 1);
}
syscall_proc() 呼び出し後には dispatch() が呼ばれます.これにより スレッドのディスパッチ(次にカレントスレッドとなるべきスレッドの検索. あれ,それはスケジュールと呼ぶべきか?うーんちょっと名前間違ったかも...) が行われます.
static void dispatch()
{
  int i;
  for (i = 0; i < PRI_NUM; i++) {
    if (readyque[i]) break;
  }
  if (i == PRI_NUM) {
    /* 実行可能なスレッドが存在しないので,終了する */
    exit(0);
  }
  current = readyque[i];
}
dispatch()では,レディーキューを優先度が高い順(=優先度の数値が低い順)に 検索し,一番最初に見つかったスレッドをカレントスレッドとします. ただしレディーキューがまったく空(すべてのスレッドがスリープ状態に なってしまっている)の際には,動く意味は無いので終了してしまいます.

thread_intrvec() では,dispatch() によるディスパッチ(スケジュール?)の後には longjmp() が呼ばれます.

  dispatch();
  longjmp(current->context.env, 1);
}
ここで,カレントスレッドのコンテキストを,longjmp() にバッファとして 渡しています.このため先程ディスパッチされたスレッドが,次に動き出します.

さて,次に動き出すのはどこからでしょうか? スレッドは割り込み発生した状態で,その位置のコンテキストを保存して 停止しています.これは外部割り込み,もしくはシステムコールによる ソフトウエア割り込みが入った位置です. よってディスパッチされて新しいカレントスレッドとなったスレッドが, 前回に割り込まれた位置から動作再開します.

現状のKOZOSでは,割り込み発生時のハンドラである thread_intr() の内部で, setjmp() によりコンテキスト保存し,longjmp() により割り込みハンドラ用の コンテキストで割り込み処理を開始しています. もう一度,thread_intr() を見てみましょう.

static void thread_intr(int signo)
{
  /*
   * setjmp()/longjmp() はシグナルマスクを保存し復元するが,
   * _setjmp()/_longjmp() はシグナルマスクを保存しない.
   * (レジスタセットとスタックしか保存および復元しない)
   */
  if (setjmp(current->context.env) == 0) {
    longjmp(intr_env, signo);
  }
}
dispatch() によるディスパッチ後の longjmp() で飛んでくるのは, 上の setjmp() の部分です. setjmp() により,スレッドのコンテキストが保存されているからです.

つまり動作はカレントスレッドが割り込まれて(=シグナルを受けて)シグナルハンドラ が起動され,setjmp() を呼び出した時点に戻ります. そしてそのスレッドのコンテキストでシグナルハンドラが終了し,再びそのスレッドが 動作を開始することになります.

このように KOZOS では,setjmp() による状態保存用のバッファ(jmp_buf)を コンテキスト保存用に流用し,setjmp()/longjmp() によってコンテキストスイッチを 行います(この構造には実はいろいろ問題があるので,後期バージョンで変更します). どうでしょう? シグナルと setjmp()/longjmp() を利用することで, OSもどきの動作ができています.

とりあえずOSの動作の流れをひととおり説明しましたが, うーん,ちょっと駆け足というかわかりにくいですね... どうだったでしょうか?

アプリの実行結果については次回説明します.


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