OS自作入門 -Advent30-

NO IMAGE

Step8 スレッドを実装する 延長戦

Step8 後半戦から実装を進めており、OS本体の実装から再開する。スレッド管理とシステムコールの受付、割込み処理を行なっていくことになる。

OSの実装

スレッド管理

作成したkozos.cを置いておく。

簡単に説明を加えていくと、まず3つの構造体が定義されている。

  • タスク・コンテキスト(kz_context)
    スレッドの実行を中断・再開するために必要なCPU情報を保存するための構造体。
    ただし、各汎用レジスタの値はブートローダ側で保存しているのでここではスタックポインタの値のみ保存すれば十分である。
  • タスク・コントロール・ブロック(kx_thread)
    各タスクの情報を格納する領域を Task Controll Block (TCB) と呼ぶ。
    ここにはスレッドの様々な情報を保持している。
    例えばスレッド名である name や、スレッドの持つスタック領域の先頭アドレスである stack などである。
  • スレッドのレディー・キュー(readyque)

最後のレディーキューは説明が長くなりそうなので分けて解説を載せよう。

レディーキュー

kozosの実行状態は、実行可能状態・待ち状態の二つである。CPUは一つなので1度に実行できるのは常に一つのスレッドのみ。この実行中のスレッドのことをカレント・スレッドと呼ぶ。実行可能なスレッドは レディーキュー に繋いでおく。レディーキューはTCB(上述)が繋がっているキューのことであり、カレントスレッドの実行に区切りがついたら、カレントスレッドはレディーキューの終端に接続し、レディーキューの先頭のスレッドを新たにカレントスレッドとするようにしている。

なので、KOZOSでは実行可能なスレッドは公平に順番に実行される。これを ラウンドロビン・スケジューリング と呼ぶ。

レディーキューの操作

作成したkozos.cのこの部分がレディーキューの操作に該当する。

  • getcurrent()
    readyque.headの指す先を次のエントリに書き換えている。
    FIFO(First In First Out)の実装。
    また元々の構造体のnextが指す先もNULLに設定しておく。
  • putcurrent()
    キューの終端を示すreadyque.tailの指す先をカレントスレッドに書き換えて、終端にエントリを追加している。
    また元々の終端エントリのnextが指す先にcurrentを設定する。

この辺りのキュー自体の実装は通常のプログラミングの話と大きく逸脱している訳ではないので理解がしやすい。この後の実際にこの構造体を使って処理を行う箇所の方が難しそう。

スレッドの起動と終了

この辺りがスレッドの起動と終了の部分。

スレッドはthread_run関数によって起動される。kz_run()kz_start()は内部でこれを呼ぶことになる。thread_run関数の補足説明を少し記載しておくと、TCBから空いている領域を探し、これから生成するスレッドのTCBの設定を行う。

strcpy(thp->name, name);
htp->next = NULL;
....
thp->init.func = func;
thp->init.argc = argc;
thp->init.argv = argv;

このような感じで。

その後スタックを獲得する(領域の確保を行う)。thread_stack += stacksize;とすることでthread_stackの指す位置を必要サイズ分ずらしているのがスタックの獲得に該当する。スレッドの動作を開始するには、プログラムカウンタの初期値とCCRの初期値をスタックに積んでrteを実行すれば、それらの値がプログラムカウンタとCCRに設定され、そこから処理を始めることができる。スレッドが中断される際には、割込みハンドラの入り口で各汎用レジスタの値がスタックに保存される。dispatch()によるスレッドのディスパッチの際には、スタック上から各汎用レジスタに値を復旧しrteが実行されプログラムカウンタとCCRの値がスタックから復旧されCPUはスレッドの処理実行を再開することができる。なので、その割込みハンドラの入り口と同様に、スレッドの動作開始位置で各汎用レジスタの値をスタック上に保存しておけば、dispatch()を呼ぶだけでスレッドの動作を開始することができるようになる(はずである)。

  sp = (uint32 *)thp->stack;
  *(--sp) = (uint32)thread_end;

  *(--sp) = (uint32)thread_init;

  *(--sp) = 0; /*ER6*/
  *(--sp) = 0; /*ER5*/
  *(--sp) = 0; /*ER4*/
  *(--sp) = 0; /*ER3*/
  *(--sp) = 0; /*ER2*/
  *(--sp) = 0; /*ER1*/

  *(--sp) = (uint32)thp; /*ER0*/

この辺りが、レジスタの値をスタック上に保存している処理に該当する。スレッドのスタートアップからreturnで戻った時にthread_endが呼ばれるようにするために、戻り先アドレスとして設定し、dispatch()でのrte命令の実行によってプログラムカウンタに設定される値としてthread_initを設定(実際に実行しているのではなく開始位置を指定しているだけ)し、各汎用レジスタの初期値の設定を行っている。

あとは、thp->context.sp = (uint32)sp;でコンテキスト(スタックポインタのみだが)を保持する。これによりdispatchの内部でスタックポインタを参照することができ、スタック上に保存されている各レジスタの値を復旧することができるようになる。

最後に、putcurrent()を呼び出し、スレッドをレディーキューに追加してexitしてロジックは終わり。

現状の実装では、ユーザ・スタックは単に未使用領域をthread_run()のthread_stackでポイントし、thread_stackを増加させることで、どこまで利用しているかを指し示しているだけです。したがってスタックの解放処理は行えません。このため、スレッドの消去と生成を繰り返すと、いずれスタック不足になって動作不能に陥ります。

なるほど。確かにTHREAD_NUM以上の数のスレッドは生成できない感じの実装になっている気がするな。


今日はここまでか。次は割込みハンドラの登録部分の実装から。step7でブートローダにソフトウェア・組み込みベクタを実装したが、ソフトウェア・割込みベクタの各割込みに応じたハンドラをhandlers[]という配列に持っており、割込みが発生したらそのハンドラを実行する。その部分の実装を行なっていく。