マルチタスクを実装し、その原理を理解しよう:H8マイコンボードで動作する組み込みOSを自作してみよう!(4)(3/3 ページ)
連載「H8マイコンボードで動作する組み込みOSを自作してみよう!」の第4回。今回は、“タスク切り替え”の動作原理を理解しながら、マルチタスクを実装し、よりOSらしく発展させていきます。
6.タスク切り替えの実装
ここでは、KOZOSのタスク制御の実装について、簡単に説明します。
以下のリスト17が、割り込みの入口です。具体的には「シリアル割り込み」の入口であり、ブートローダー側が持っている処理です。この処理は、割り込みハンドラとして登録されているため、シリアルで文字受信が行われて、割り込みが入った際に、リスト17の処理が呼ばれることになります。
……(中略)…… .global _intr_serintr # .type _intr_serintr,@function _intr_serintr: mov.l er6,@-er7 mov.l er5,@-er7 mov.l er4,@-er7 mov.l er3,@-er7 mov.l er2,@-er7 mov.l er1,@-er7 mov.l er0,@-er7 mov.l er7,er1 mov.l #_intrstack,sp mov.l er1,@-er7 mov.w #SOFTVEC_TYPE_SERINTR,r0 jsr @_interrupt mov.l @er7+,er1 mov.l er1,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
リスト17 割り込みの入口(intr.S) |
リスト17の処理はアセンブラで書かれていますが、行っていることはそれほど難しいことではありません。ER0〜ER6の汎用レジスタの値をスタック上に保存し、「_interrupt()」という関数を呼び出しています。これは、図3のコンテキストの退避と関数呼び出しに当たる操作です。また、最後の「rte」命令が、図3のRFI命令の実行に相当します。
リスト18〜20は、KOZOSのタスク制御部分です。リスト18ではタスク制御用に、「kz_context」「kz_thread」「readyque」という3つの構造体を定義しています。
/* スレッドコンテキスト */ typedef struct _kz_context { uint32 sp; /* スタックポインタ */ } kz_context; /* タスクコントロールブロック(TCB) */ typedef struct _kz_thread { ……(中略)…… kz_context context; /* コンテキスト情報 */ } kz_thread; /* スレッドのレディーキュー */ static struct { kz_thread *head; kz_thread *tail; } readyque[PRIORITY_NUM]; static kz_thread *current; /* カレントスレッド */ static kz_thread threads[THREAD_NUM]; /* タスクコントロールブロック */ ……(後略)……
リスト18 タスク制御用の構造体の定義(kozos.c) |
「kz_context」は、タスクのコンテキストの保存用の構造体で、図4のコンテキストの保存先に当たるものです。H8用のKOZOSでは、汎用レジスタの値はスタック上に保存されるため、コンテキストとしてはスタックポインタの値のみを保存するようにしています。
「kz_thread」は、タスクの各種パラメータの保存用の構造体です。このような構造体は一般に「TCB(Task Control Block)」と呼ばれます。さまざまなパラメータの保存の他に、コンテキストの保存領域も持たせています。
「readyque」は、タスクの「レディーキュー」と呼ばれるものです。実行可能なタスクはリンクリストにより、キュー状に管理されています。
「current」は、現在実行中のタスクを指すポインタで、(リスト中のコメントでは「カレントスレッド」になっていますが)「カレントタスク」などと呼ばれます。
リスト19は、KOZOSの割り込み処理部分です(thread_intr())。まず、タスクのコンテキストとして、TCBにスタックポインタを退避します。その後、割り込みに応じた処理を呼び出し、「schedule()」が呼ばれます。「schedule()」では、スケジューリングが行われ、次に動作するタスクを選出します。さらに、「dispatch()」を呼び出すことで、そのタスクのコンテキストをレジスタに復元し、タスクの動作を再開させます。
……(中略)…… /* 割込み処理の入口関数 */ static void thread_intr(softvec_type_t type, unsigned long sp) { /* カレントスレッドのコンテキストを保存する */ current->context.sp = sp; /* * 割込みごとの処理を実行する。 * SOFTVEC_TYPE_SYSCALL、SOFTVEC_TYPE_SOFTERRの場合は * syscall_intr()、softerr_intr()がハンドラに登録されているので、 * それらが実行される。 */ if (handlers[type]) handlers[type](); schedule(); /* スレッドのスケジューリング */ /* * スレッドのディスパッチ * (dispatch()関数の本体はstartup.sにあり、アセンブラで記述されている) */ dispatch(¤t->context); /* ここには返ってこない */ } ……(後略)……
リスト19 KOZOSのタスク制御(kozos.c) |
リスト20は、スケジューリング処理です。タスクのレディーキューを検索し、見つかったタスクを変数「current」に設定することでカレントタスクとしています。
……(中略)…… /* スレッドのスケジューリング */ static void schedule(void) { int i; /* * 優先順位の高い順(優先度の数値の小さい順)にレディーキューを見て、 * 動作可能なスレッドを検索する。 */ for (i = 0; i < PRIORITY_NUM; i++) { if (readyque[i].head) /* 見つかった */ break; } if (i == PRIORITY_NUM) /* 見つからなかった */ kz_sysdown(); current = readyque[i].head; /* カレントスレッドに設定する */ } ……(後略)……
リスト21は、タスクのディスパッチ処理です。これもアセンブラで書かれていますが行っていることはそれほど複雑ではなく、引数としてタスクのコンテキストへのポインタが渡されるため、その先(正確にはスタック上)に退避されているレジスタの値を復元し、rte命令を呼ぶことで処理を再開しています。
……(中略)…… .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
7.次回
今回は、マルチタスクを実装しました。次回はこれらのタスクを発展させ、“実践的なコンソール応答プログラム”を作成してみます。お楽しみに! (次回に続く)
筆者紹介:
坂井弘亮(さかいひろあき)
某企業でネットワーク系の開発を行いながら、個人で組み込みOS「KOZOS」の開発や書籍/雑誌記事執筆、各種セミナーでの発表やイベント出展、勉強会の開催など多方面で活動中。IPAセキュリティ&プログラミングキャンプ 2010講師。著書は「12ステップで作る組込みOS自作入門」など多数。
Copyright © ITmedia, Inc. All Rights Reserved.