【解説】12ステップで作る組込みOS自作入門 8thステップ ~初期スレッドの作成と起動~
8thステップ「スレッドを実装する」の解説記事、第2弾です。
今回は初期スレッドの作成と起動について解説します。
記事を書くために内容の整理を繰り返した結果、8thステップは
が肝だとわかりました。(ここさえ理解できれば理解すべきことは抑えられているハズ)
目次
概要
全体の流れを説明した記事の「main関数開始~main関数終了」を詳細に解説します。
下図は前の記事をちょっとだけバージョンアップしたシーケンス図です。
commandスレッドと (割込みが発生しないので) 割込みハンドラ (ISR: Interrupt Service Routine) は登場しません。
ソースコードも交えたいので関数名 (今回はkz_start関数) を図中に登場させました。
シーケンス図の書き方として正しくないのかもしれませんが、わかりやすさ重視で関数の生存期間も表しています。
今回はkz_start関数から処理が始まります。
main関数はOSの一部!(大事なことなので)
初期化@kz_start関数
初期化の対象は4つあります。
起動直後なのでスレッドがまったくない状態です。
そのことを前提に考えると理解しやすいと思います。
- カレントスレッド
- レディーキュー
- タスクコントロールブロック (TCB)
- 割込みハンドラ
current = NULL; readyque.head = readyque.tail = NULL; memset(threads , 0, sizeof(threads)); memset(handlers, 0, sizeof(handlers));
カレントスレッドの初期化
カレントスレッドは実行中のスレッド (OSの処理中は実行予定のスレッド) のことです。
カレントスレッドをNULLにすることで何もないことを表現します。(1行目)
レディーキューの初期化
レディーキューは実行可能なスレッド (実行中 (実行予定) のスレッドを含む) のことです。
レディーキューの先頭、末尾をNULLにすることでスレッドがつながれていないことを表現します。(3行目)
タスクコントロールブロック (TCB) の初期化
TCBはスレッド (タスク) の情報を格納する領域のことです。
スレッドがないので、すべて0で初期化しておきます。(4行目)
割込みハンドラの初期化
割込みハンドラは誤動作防止のために、いったんすべて初期化します。 (5行目)
割込みハンドラの登録@kz_start関数
/* 割込みハンドラの登録 */ setintr(SOFTVEC_TYPE_SYSCALL, syscall_intr); /* システムコール */ setintr(SOFTVEC_TYPE_SOFTERR, softerr_intr); /* ダウン要因発生 */
システムコール発生時にはsyscall_intr関数、ダウン要因発生時にはsofterr_intr関数が呼ばれるように割込みハンドラを登録しています。
以下はソースコードを理解する上で重要です。
登録した割込みハンドラが直接呼ばれるわけではありません。
割込み要因がシステムコールであろうが、ダウン要因発生であろうが必ずthread_intr関数が呼ばれる作りになっています。
thread_intr関数の中でsyscall_intr関数とsofterr_intr関数が呼び分けられます。
※システムコールの発行は次の記事 (システムコールを発行してスレッドを作成する) になる予定なので、そちらで詳しく説明する予定です。
初期スレッドの作成@kz_start関数
本当はシステムコールを使ってスレッドを作成したいのですが、システムコールはスレッドからしか呼べない仕様になっています。
そのため、OSの機能を直接使用することで初期スレッドを作成します。
スレッドを作成するOSの機能はthread_run関数です。
これからthread_run関数について説明します。
折りたたみでthread_run関数を載せようかと思いましたが、はてなブログのHTMLとの相性が悪く、対応できたとしても時間がかかりそうなので別記事にしました。
コードで流れを確認したい場合はご利用ください。
スレッドの作成@thread_run関数
thread_run関数では以下のことを行っています。
- 空いているTCBの検索
- TCBの設定
- スタック領域の確保
- スタックの設定
- 作成したスレッドをレディーキューに接続
以降の詳細な説明では関係するコードだけを抜粋してあります。
空いているTCBの検索
INT i; kz_thread_t *thp; /* 空いているタスクコントロールブロックを検索 */ for (i = 0; i < THREAD_NUM; i++) { thp = &threads[i]; /* 見つかった */ if (!thp->init.func) { break; } } /* 見つからなかった */ if (i == THREAD_NUM) { return -1; }
空いている (使用していない) TCBはinit.funcが登録されていません。(8行目)
今回は初期スレッドなのでTCBはすべて空いています。
そのためi=0の段階で空いているTCBが見つかります。
TCBの設定
kz_thread_t *thp; UINT32 *sp; memset(thp, 0, sizeof(*thp)); /* タスクコントロールブロックの設定 */ strcpy(thp->name, name); thp->next = NULL; thp->init.func = func; thp->init.argc = argc; thp->init.argv = argv; /* スタック領域の確保 */ /* (中略) 結果としてspに確保された領域の直後のアドレスが入る */ /* スレッドのコンテキストを設定 */ thp->context.sp = (UINT32)sp;
初期スレッドでは無意味なコードですが、TCBを再利用する場合にゴミ情報が残っていたら困るので0埋めしておきます。(4行目)
その後、TCBにthread_run関数の引数を詰めていきます。
具体的には下記の項目です。
- スレッド名name (デバッグ用)
- 次のスレッドnext
- スレッド起動後に呼ばれる関数funcとその引数argc, argv
- スレッドを再開するためのスタックポインタsp
なお、作成したスレッドはレディーキューの末尾に接続されるため、nextにはNULLが代入されます。
また、スタック領域はスレッドごとに確保されます。
KOZOSはスレッドの処理を再開するためには、スレッドのスタックポインタさえわかれば良い作りになっています。
スタック領域の確保
kz_thread_t *thp; UINT32 *sp; extern INT8 userstack; /* リンカスクリプトで定義されるスタック領域 */ static INT8 *thread_stack = &userstack; /* スタック領域の確保 */ memset(thread_stack, 0, stacksize); thread_stack += stacksize; thp->stack = thread_stack;
ここのポイントはthread_stackという変数にstaticが付いていることです。
staticをつけることで関数を抜けても値が記憶されるようになります。
その結果、スタック領域は重複されることなく確保されます。
なお、スタックは下方伸長 (アドレスが小さい方向に延びる) です。
そのためスタック領域を確保した直後のアドレスを初期値として持つことになります。(8-9行)
スタックの設定
kz_thread_t *thp; UINT32 *sp; /* スタックの初期化 */ 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 */ /* スレッドのスタートアップ (thread_init()) に渡す引数 */ *(--sp) = (UINT32)thp; /* ER0 */ /* スレッドのコンテキストを設定 */ thp->context.sp = (UINT32)sp;
順に以下の内容をスレッドのスタックに積んでいきます。
- スレッドを終了するときに呼ばれる関数 (thread_end関数, 6行目)
- スレッドを開始するときに呼ばれる関数 (thread_init関数, 9行目)
- thread_init関数を実行するときの汎用レジスタの値 (11-19行 ※19行目はthread_init関数の第1引数)
その後、TCBにスレッドのコンテキストとしてスタックポインタを保持させます。(再掲。TCBの設定で1回紹介しました)
※スレッドを開始するときはこのスレッドのスタックポインタを基準に汎用レジスタを復元してthread_init関数の呼び出しを行います。
作成したスレッドをレディーキューに接続
/* システムコールを呼び出したスレッドをレディーキューに戻す */ put_current(); /* 新規作成したスレッドをレディーキューに接続する */ current = thp; put_current(); return (kz_thread_id_t)current;
初期スレッド作成においてput_current関数では何も行いません。(current=NULLだと即時に関数を抜けるため)
6行目のput_current関数でレディーキューに初期スレッドをつなぐことで、実行する予定のあるスレッドであることを表現します。
初期スレッドの起動 (スレッドのディスパッチ)
dispatch(¤t->context);
kz_thread関数でdispatch関数を呼び出しています。
以下はdispatch関数の具体的な処理です。
.global _dispatch .type _dispatch,@function _dispatch: mov.l @er0,er7 mov.l @er7+,er0 mov.l @er7+,er1 mov.l @er7+,er2 mov.l @er7+,er3 mov.l @er7+,er4 mov.l @er7+,er5 mov.l @er7+,er6 rte
アセンブリ言語で記述されています。
dispatch関数が呼ばれると4行目以降が実行されます。
4行目ではスタックポインタを第1引数として渡されたスレッドのスタックポインタで上書きします。
これにより、これ以降はスレッドのスタック領域が使用されることになります。
5-11行では汎用レジスタER0-6をスタック領域から復旧します。
スタックの設定ですべて0にしておいたので0で埋められます。
ここだけ見れば直接0を埋めれば良いような気もしますが、割込み発生時の処理と共通にするために、汎用レジスタの退避、復旧を必ず行います。
最後に12行目のRTE命令により、RTE命令実行時にスタック領域の一番上に積まれているthread_init関数が実行されます。
ここがよくわからないという場合は過去記事を読むことを推奨します。
あとがき
冒頭にも書きましたが、記事を書くために内容の整理をしていると理解が深まってきました。
最終的にはC言語が分からなくても、なんとなくやっていることが理解できる記事or資料を作るべきという気持ちが強くなっています。
今のところの構想はこんな感じです。
この順番にパワポ的な資料を作ればかなり読みやすいはずです。
最近、言葉で説明するのも大変だけど、言葉を読んで理解するのも大変だと感じることが多くなりました。
図を使って説明するのは、準備段階で時間がかかりますが、双方の認識ずれが発生しづらくてとても良いですね。(同様の理由で数字や数学も素晴らしいと思います。数学は難しいところも多いですが。)
非常事態宣言も解除され、ステイホーム感も少しずつ弱まっていくと思います。
それにともない、私も記事を書く時間を確保するのが少しずつ大変になっています。
それでもできるだけ週1ペースで記事を書こうと思います。
正直、現状では読者がいないのでモチベーション的に大変つらい部分があります。
Google検索をするときに、たまにでいいので「1か月以内」などの条件を付けて検索することで記事を書いている人のモチベーションに繋がると思います。
※後発は本当に大変と思い知らされました。初学者レベルから勉強を始める人も一定数はいるはずなので、うまいビジネスにならないものですかね。