皆さん こんにちは。
今回は4つ目のソースコードに入れ替えて動作確認してみます。
第3部2章のタイトルは「実行すべきタスクとその順番を決めるスケジューラ」です。
この章ではタスクの生成、レディ・キュー、スケジューラなどの機能が追加されました。
この記事は開発環境を構築することを前提にしています。
開発環境を構築したい方は ゼロから作るOS 環境構築編 をご覧になってください。
この記事はインターフェース誌2023年7月号の「ゼロから作るOS」を参考にしています。
まずは書籍の本章を読んで予習しておくことをお薦めします。
ソースコードを入れ替える
これから前回製作したソースを削除するので、必要ならバックアップをとっておいてください。
Project Explorerでpico_tryknlのツリー下のフォルダを全て選択し、右クリックして Delete を選択して削除します。

次にソースファイルをIDEにコピーします。
C:\DevTools\src\IF2307TK\part_3\sect_2 下にある以下のフォルダを全て選択して、IDEの Project Explorer の pico_tryknl にドラッグ&ドロップします。
(書籍 第3部2章のソースファイルになります)
次のウィンドウが出るので、そのままOKを押してコピーします。

コピーしたフォルダを展開し、フォルダ下にもファイルがコピーされていれば成功です。

PC側でTera Termの設定を行う
この章のコードでは途中経過をUARTで送信するようになっているので、PC側で受信した文字列を表示する準備をしておきます。
PCアプリのTera Termを起動し新しい接続画面でシリアルを選択し、ポートに適切なCOMポートを選択します。
Tera Termをお持ちでない方はサイトからダウンロードしてインストールしてください。

設定 – シリアルポート でスピートを 115200 に設定します。(それ以外は以下の設定)

設定 – 端末 で改行コード 受信・送信を LF に設定します。(改行して表示を見やすくします)

プログラムを動かしてみる
Run – Debug History から pico_tryknl Debug を選択しResumeボタンが緑色になったらボタン(またはF8キー)を押してプログラムを実行します。
Tera Termに以下の文字列が表示されれば成功です。

(この章ではタスクを生成して動かした後、休止するところまでの動作になっていてディスパッチするコード等は実装されていません)
この章のプログラムの概略説明です。
つくられるタスクは3つあり、main()で最初のタスク(初期タスク)が生成されます。
その後、tk_sta_task()で初期タスクが実行されます。
このタスクの優先順位は 1 に設定されていて、つくられるタスクの中では一番高くなっています。
初期タスク initsk() では “Start Try Kernel”の文字列をUARTで送信します。
その後、usermain()関数を実行して tk_ext_tsk() で休止します。
usermain()内では、タスク1とタスク2が生成されて実行されます。
実行されると書きましたが実際にはレディ・キューに入れるだけです。
タスク1とタスク2の優先順位は 10 で 1 よりも低いため、初期タスクが休止するまでこれらのタスクは実行されません。
(数値が小さいほど優先順位は高い)
その後、tk_ext_tsk()により初期タスクが休止すると、タスク1が動き出します。
tk_ext_tsk()で初期タスクを休止した後スケジューラーが動くためです。
スケジューラーは次に実行すべきタスクを決定しディスパッチ(タスクの切替)を行います。
タスク1と2は同じ優先順位ですが、スケジューラーは先に生成したタスクから実行します。
タスク1は “Start Task-1” の文字列をUARTで送信します。
その後tk_ext_tsk()でタスク1を休止しタスク2が動き出します。
タスク2は “Start Task-2” の文字列をUARTで送信します。
その後タスク2も休止します。
ここで終わっては味気ないのでコードを少し見ていくことにします。
タスク・コントロールブロック
タスクはC言語の関数とタスクの管理情報の組み合わせで成り立っています。
タスクの管理情報は一般にTCB(Task Control Block)と呼ばれています。
TCBは構造体で定義されていて、現時点で必要最小限なメンバで構成されているようです。
本章以降にメンバが追加されていくものと思われます。
TCB構造体は include\knldef.h で定義されています。
レディ・キュー
タスクのレディ・キューは以下のように定義されています。
TCB *ready_queue[CNF_MAX_TSKPRI]; // タスクのレディキュー
CNF_MAX_TSKPRI は 16 で優先順位の数を表わしています。
優先順位毎にキューのリストが存在することになります。
この配列には優先順位毎のキューの先頭要素が入ります。
キューの要素は TCB構造体で メンバの pre と next を使って双方向リストで連結します。
それでは task_queue.c にあるキューに関連する3つの関数について見ていきます。
ややこしい部分は図に書いてみると理解しやすいと思います。
最初に tqueue_add_entry() です。
tqueue_add_entry()
機能:
キューに TCB* を登録します
第1引数:
TCBのポインタのポインタ
第2引数:
登録するTCB
戻り値:
なし
タスク実行APIの tk_sta_tsk()内で以下のように呼んでいます。
tqueue_add_entry(&ready_queue[tcb->itskpri], tcb);
C言語の関数には値渡しとポインタ渡しがあります。
値を渡す場合には関数内でコピーが使われるため変数の値を変更することはできません。
対してポインタを渡す場合には変数の値を変更することができます。
ポインタのポインタを渡す理由は関数内でポインタ自体を書き換えたいからです。
以下の *queue = tcb; の部分でポインタを書き換えています。
/* エントリ追加関数 */
void tqueue_add_entry(TCB **queue, TCB *tcb)
{
TCB *queue_end;
if(*queue == NULL) { // キューは空なので先頭に追加
*queue = tcb;
tcb->pre = tcb;
} else { // キューの終端に追加
queue_end = (*queue)->pre;
queue_end->next = tcb;
tcb->pre = queue_end;
(*queue)->pre = tcb;
}
tcb->next = NULL;
}
それでは関数の中を見ていきましょう。
TCB *queue_end;
終端の要素を一時的に保存する場所
if (*queue == NULL) {}
キューが空の場合、if() {}の中を実行します。
*queue = tcb;
先頭の要素にtcbを登録します。
tcb->pre = tcb;
先頭の要素のpreにtcbを代入します。
これは先頭の要素のpreに終端の要素を代入する規則に従っています。
tcb->next = NULL;
終端の要素のnextにはNULLを入れます。
else {}
キューが空でない場合 else {} の中を実行します。
queue_end = (*queue)->pre;
一時保存する場所に終端の要素を入れます。
queue_end->next = tcb;
終端の要素のnextに登録するtcbを入れます。
これでtcbが終端の要素になります。
tcb->pre = queue_end;
tcbのpreはqueue_endになるので代入します。
(*queue)->pre = tcb;
先頭の要素のpreにtcb(終端の要素)を入れます。
tcb->next = NULL;
終端の要素のnextにはNULLを入れます。
次に tqueue_remove_top() です。
tk_ext_tsk() でタスクを休止する時に呼んでいます。
tqueue_remove_top(&ready_queue[cur_task->itskpri]);
tqueue_remove_top()
機能:
キューの先頭から TCB* を削除します
第1引数:
TCBのポインタのポインタ
戻り値:
なし
/* 先頭エントリ削除関数 */
void tqueue_remove_top(TCB **queue)
{
TCB *top;
if(*queue == NULL) return; // キューは空
top = *queue;
*queue = top->next;
if(*queue != NULL) {
(*queue)->pre = top->pre;
}
}
それでは関数の中を見ていきましょう。
TCB *top;
先頭の要素を一時的に保存する場所
if (*queue == NULL) return;
キューが空の場合、何もせず return します。
top = *queue;
一時保存する場所に先頭の要素を入れます。
*queue = top->next;
先頭の要素にtopのnextを入れます。
これで先頭の要素が入れ替わりました。
(先頭要素の削除)
if(*queue != NULL) {
(*queue)->pre = top->pre;
}
先頭の要素が空でなければ
先頭の要素のpreにtopのpre(終端の要素)を入れます。
最後に tqueue_remove_entry() です。
この章では使われていないようですが説明しておきます。
tqueue_remove_entry()
機能:
キューから TCB* を削除します
第1引数:
TCBのポインタのポインタ
第2引数:
削除するTCB
戻り値:
なし
/* 指定エントリ削除関数 */
void tqueue_remove_entry(TCB **queue, TCB *tcb)
{
if(*queue == tcb) { // 指定したエントリはキューの先頭
tqueue_remove_top(queue);
} else { // キューの途中から指定エントリを削除
(tcb->pre)->next = tcb->next;
if(tcb->next != NULL) {
(tcb->next)->pre = tcb->pre;
} else {
(*queue)->pre = tcb->pre;
}
}
}
それでは関数の中を見ていきましょう。
if(*queue == tcb) { // 指定したエントリはキューの先頭
tqueue_remove_top(queue);
}
指定したtcbが先頭の要素ならば
tqueue_remove_top()を使って先頭の要素を削除します。
それ以外は以下のコード
(tcb->pre)->next = tcb->next;
tcbのpreのnextにtcbのnextを入れます。
これはnextの連結からtcbを削除しています。
if(tcb->next != NULL) {
(tcb->next)->pre = tcb->pre;
}
tcbのnextがNULLでなければ
tcbのnextのpreにtcbのpreを入れます。
これはpreの連結からtcbを削除しています。
else {
(*queue)->pre = tcb->pre;
}
tcbのnextがNULLならば
先頭の要素のpreにtcbのpreを入れて終端の要素を更新します。
これはtcbが終端要素の場合です。
次にスケジューラーを見てみます。
scheduler()
機能:
実行するタスクを決定します
引数:
なし
戻り値:
なし
/* タスクのスケジューリング */
void scheduler(void)
{
INT i;
for(i = 0; i < CNF_MAX_TSKPRI; i++) {
if( ready_queue[i] != NULL) break;
}
if(i < CNF_MAX_TSKPRI) {
sche_task = ready_queue[i];
} else {
sche_task = NULL; // 実行できるタスクは無い
}
if(sche_task != cur_task && !disp_running) {
dispatch(); // ディスパッチャを実行
}
}
関数の中を見てみます。
forループは最初に要素を見つけたところで break します。
if (i < CNF_MAX_TSKPRI) {
sche_task = ready_queue[i];
}
iがCNF_MAX_TSKPRI未満なら(TCBが見つかったなら)
sche_taskにTCBを入れます。
else {
sche_task = NULL; // 実行できるタスクは無い
}
TCBが見つからなかった場合はsche_taskにNULLを入れます。
if (sche_task != cur_task && !disp_running) {
dispatch(); // ディスパッチャを実行
}
sche_taskがcur_task(現在実行中のタスク)ではなく
かつディスパッチを実行していなければ dispatch() します。
キューとスケジューラーのイメージは掴めましたでしょうか。
ディスパッチ
dispatch.S に書かれているアセンブラのコードは若干手を加えています。
dispatch_entry: の部分です。
手を加えている部分は難しいコードではないので詳しい説明は省略しますがコードを見ておくと良いでしょう。
主な変更点:
入り口で割り込みを禁止し、disp_runningのフラグを立てています。
そして出口で割り込みを許可し、disp_runningのフラグを落としています。
この章以降、この部分のコードに変更はありません。
タスク管理機能
タスク管理機能はAPIとして実装されています。
ここでは
タスク生成API: tk_cre_tsk
タスク実行API: tk_sta_tsk
タスク終了API: tk_ext_tsk
について概要説明しておきます。
これらのAPI関数は kernel\task_mange.c に書かれています。
tk_cre_tsk()
タスク生成APIです。
T_CTSK型の構造体を引数で受け取ります。
(TCB型の)tcb_tbl配列の空いている領域を探して、引数の情報を元に TCBのメンバを設定します。
(スタック領域、優先順位、タスクのアドレス等を設定します)
ID型のidを戻り値として返します。
tk_sta_tsk()
タスク実行API: tk_sta_tsk
ID型のidを引数に受け取り、idから(TCB型の)tcb_tblの領域を確保します。
TCBのstateメンバをTS_READYに設定し、make_context()関数によりTCBのcontextメンバにStackFrameを割り当てます。
contextはタスク毎のレジスタを保存する領域です。
その後レディ・キューにTCBを登録し、スケジューラーを実行します。
実行APIと言っても、必ずその場で実行される保証はありません。
実行するタスクはスケジューラーが決定します。
tk_ext_tsk()
現在実行中のタスクのstateをTS_DORMANTにし、レディ・キューからTCBを削除しスケジューラーを実行します。
機能が増えてきてOSらしくなってきました。
覚えることが増えて来てたいへんですね。
私はデバッガーを使って起動時から関数の中を実行していき、タスク管理情報(構造体などのデータ)とタスク管理機能(関数)について学んでいます。
皆さんはいかがでしょうか。
お疲れさまでした。
この記事の続きは こちら です。