OS自作入門 -Advent37-

NO IMAGE

Step12 外部割込みを実装する

ようやく最終stepにきた。長い道のりだった。最後のテーマは外部割込み。シリアルから1文字受信割込みと、コマンド応答スレッドを作成する。

割込みとスレッド

step6とstep8で利用したgets()はビジーループによりシリアルの受信待ちをしている。そのため常にCPUがシリアルコントローラのレジスタを監視しており利用効率がよくない。step7のように割込みベースにするのが良い。しかし、step7ではシリアル受信割込みハンドラの内部でコマンド応答処理を行なっており、重たい処理があると応答性を下げるので良くない。 割込み処理は極力短く書く という鉄則がある。

なので、受信文字から処理へ繋げて処理の部分を別なスレッドで行うという方針を取った方が良い。

シリアル受信割込み

  1. シリアル受信割込みハンドラでは受信文字からメッセージによりコマンド応答スレッドに送信。
  2. コマンド応答スレッドは、メッセージを受信してコマンドとして解釈し処理を行う。

こうすることによりシリアル受信の割込みハンドラが受信・割込みのクリア・メッセージ送信のシンプルな処理だけで済む。

シリアル送信割込み

シリアル送信割込みが発生するのは「1文字送信を行った時に、その送信処理が完了して次の文字の送信が可能になった時」である。serial_is_send_enable()で送信可能かどうかのチェックを行っているが、ビジーループによる待ち合わせである。これはSCI(シリアルコミュニケーションインターフェース)の総員バッファが1文字しか格納できない構造であるため、前の文字の出力が完了するまで次の文字が入ってくると問題となるためである。しかしこれもまた無駄な処理であり、このビジーループの間に他の処理を行うことができるはず。それを回避するために以下のような実装にする。

  1. シリアル送信の割込みハンドラとシリアル送信スレッドを作成
  2. シリアル送信スレッドは送信要求を受けたら、送信バッファに送信する文字列を書き込み先頭の1文字を送信。割込み待ちにする。
  3. 送信完了したら、シリアル送信割込みを発生させ割込みハンドラで未送信の文字が残っているなら送信する。
  4. 送信が終わるまで繰り返し。

と言う感じに送信用のバッファを作って、それを中継して割込み => 送信の処理を分離しておくような感じ。受付は都度行い、暇な時に処理を行うみたいな。

「シリアル送信スレッド」を「コンソール・ドライバ・スレッド」として実装する。

非タスク・コンテキスト

割込み中にシステム・コールを行うと、割込み時に動作していたスレッド(カレントスレッド)のコンテキスト情報が割込み中のシステム・コールにより破壊されてしまい、スレッドが正常に動作再開できなくなってしまう。そのため 割込み処理は中断と再開ができない 。こういった状態を 非タスク・コンテキスト と呼ぶ。なので、割込み処理は非タスク・コンテキスト、スレッドの状態はタスク・コンテキストと呼ばれている。そのため割込みハンドラからシステム・コールを呼び出すことはできない。

サービス・コール

割込み処理は割込みハンドラから呼ばれているため、システム・コールが処理される場合とコンテキストは同一であるため、OS内部が持つシステム・コールの処理関数をそのまま呼んでも実は実害がないとか。。。なので今回は非タスク・コンテキスト向けのサービスとして、サービス・コールを実装する。

KOZOSでは割込み処理中は割込み禁止になっているので関数の再入は考慮する必要はない。注意点としては、タスク・コンテキストであるスレッドからはサービス・コールは利用してはならないということだ。サービス・コールは基本として割込みハンドラ向けに提供するサービス関数である。

以下のような関数を用意する(kz_xxxからkxに名前を置き換えたものを用意する)

  • kx_wakeup
    割込みが入るまでスリープして、割込みハンドラからウェイクアップすることで動作開始するようなスレッドを作成したい場合に利用。
  • kx_send
    割込みハンドラから処理スレッドに受信データを渡したい場合に利用。
  • kx_kmalloc, kx_kmfree
    割込みハンドラから処理スレッドにデータを渡す際にメモリ獲得が必要な場合がありその際に利用。

排他

シリアル送信時の動作で考えなくてはいけないことがある。それは送信バッファの取り扱いである。シリアル送信の割込み処理と、コンソール・ドライバ・スレッドの両者から触られる可能性があるため、例えばコンソール・ドライバ・スレッドから大量の文字をバッファにコピーしている最中に、送信割込みが入り1文字送信することにより送信バッファの内容が変化し、コピー処理が再開されて変化した後のバッファに上乗せされる形となるとおかしな処理結果となる可能性がある。

そのためstep11で出てきた送信バッファに対する ロック処理 を施しスレッド間で動機を取るようにする。そのため、「ロック」「ロック解除」「ロック中の待ち合わせ」を行うようなサービスをシステム・コールとしてOSに実装する必要がある。

が、このような同期処理は、シリアル割込みへの適用は不的確らしい。前述の通り、割込み処理は中断と再開はできないので、「待ち合わせる」と言う動作は基本的にできないためである。

なので今回は排他の最も簡単な実現方法として、割込み禁止を利用することで実現する。

割込みの管理

KOZOSではOSの内部で割込みハンドラの配列を持っていて、ここの登録されている関数を実行している。

static kz_hanlder_t handlers[SOFTVEC_TYPE_NUM]; // ここで定義されている

if(hanlders[type])
  handlers(type)(); // これで実行

setintr(SOFTVEC_TYPE_SYSCALL, syscall_intr); // システム・コールハンドラ登録
setintr(SOFTVEC_TYPE_SOFTERR, softerr_intr); // エラー処理用のハンドラ登録

今回はここに、割込みハンドラを自由に登録できるようにOSのサービスとして公開、利用できるようにする。

このような仕様になる。

int kz_setintr(softvec_type_t type, kz_hanlder_t handler);

UML

結構複雑な動きなのでUMLがあったので転用させてもらおう。

実装

ということで実装。以下のファイルを追加・修正する。

  • consdrv.h, consdrv.c(新規)
    コンソールドライバスレッド
  • command.c(新規)
    コマンドスレッド
  • defines.h(修正)
  • syscall.h, syscall.c(修正)
  • kozos.h, kozos.c(修正)
  • main.c(修正)
  • Makefile(修正)

メッセージIDの定義

defines.hにメッセージIDを定義。

typedef enum {
  MSGBOX_ID_CONSINPUT = 0,
  MSGBOX_ID_CONSOUTPUT,
  MSGBOX_ID_NUM
} kz_msgbox_id_t;

システム・コールとサービス・コールの追加

syscall.hを修正。

kz_setintr()のためのシステム・コール番号であるKZ_SYSCALL_TYPE_SETINTRとパラメータ領域を加える。

syscall.cを修正。

システム・コールの本体であるkz_setintr()を追加。
kx_wakeup, kx_send, kx_kmalloc, kx_kmfreeを追加。kxシリーズの違いはkz_syscallの代わりにkz_srvcallを使ってサービス・コールを呼び出している。

システム・コールとサービス・コールの実装

kozos.hを修正。

システム・コールとしてkz_setintr、サービスコールとしてkx_wakeup, kx_send, kx_kmalloc, kx_kmfreeの4つのプロトタイプ宣言を追加。サービス・コールを実際に発行する関数としてのkz_srvcallの追加。システム・タスクとして consdrv_main、ユーザ・タスクとしてcommand_mainのプロトタイプ宣言の追加。

kozos.cを修正。

割込みハンドラの登録を行う。もともとあったsetintr()という関数を少し変更を食わせてユーザ側から操作できるようにした。thread_setintr()という名称に変更。これはsoftvec_setintr()によりソフトウェア・割込みベクタにKOZOSの割込み処理の受け口であるthread_intr()を登録している。

call_functions

case KZ_SYSCALL_TYPE_SETINTR:
  p->un.setintr.ret = thread_setintr(p->un.setintr.type, p->un.setintr.handler);
  break;

を追加。(書籍にはこのswitch..case..のリファクタの方法も載っている)

srvcall_proc(サービス・コールの呼び出し)も追加する。syscall_procとの違いは、current(カレントスレッド)の取り扱いで、処理内部にcurrentの状態を使っている箇所があるため、誤動作する可能性がある。なのでsrvcall_procではcurrent=NULLとしている。

kz_startでハンドラの登録にthread_setintrを利用するように修正。

最後にサービス・コールの呼び出しのための関数kz_srvcallを追加。

コンソール・ドライバ・スレッド

KOZOSではデバイス・ドライバをOSのカーネルに内包せず、独立したスレッドとして実装する。コンソールへの出力はコンソール・ドライバ・スレッドへメッセージを送信することにより行うようにする。今回はuという文字を受信したら初期化、wXXXという文字を受信したらXXXという文字列を出力するように実装する。ここでのuwをコマンドと呼ぶことにする。

consdrv.hを追加する。

コンソールの個数と、二種類のコマンドのを定義する。

consdrv.cを追加する。

consreg[]というコンソール管理用の構造体を定義。内部には送受信用のバッファを備える。

シリアル送信と割込みハンドラ

consdrv.cに送信処理部分を追加する。

文字送信用のsend_charと文字列送信用のsend_stringを実装。

割込みハンドラを追加する。consdrv_intrがシリアルの割込みハンドラとして登録される。内部で受信と送信を分岐させてる。シリアルの送信/受信割込みの状態をチェックし、割込み発生している場合にはconsdrv_intrprocを呼び出す。受信処理の特徴は、受信バッファの内容をコピーしkx_sendによってMSGBOX_ID_CONSINPUTのメッセージボックスに送信している。これは割込みの延長で行われているためサービス・コールを利用した形となっている。送信処理の特徴は、ビジーループではなく最初の1文字目以降の後続の文字列は割込みを発生させて終端まで出力するような形となっている。

コマンド処理

consdrv.cuwに対するコマンドの処理を追加。

CONSDRV_CMD_USE(u)を受けたら初期化を行う。CONSDRV_CMD_WRITE(w)を受けたら送信処理を行う。send_stringは割込み禁止にする必要があるためINTR_DISABLEINTR_ENABLEで囲んでおく。このためsend_stringを実行している最中には送信割込みが入ることはない。これは送信バッファの排他処理を保証する必要があるためである。send_charを呼ばれるとsend_bufの内容を書き換えてしまうのでそのための対処。

コンソール・ドライバ・スレッドのメイン関数

consdrv.ccondrv_main関数を追加。

構造体の初期化(consdrv_init
割込みハンドラの登録(kz_setintr(SOFTVEC_TYPE_SERINTR...)

を行い、メインループに入る。

メインループでは、kz_recvで他のスレッドからコマンドの要求を待ち、consdrv_commandでコマンド処理を実行する。

コマンド・スレッド

command.cを追加。

コンソールを初期化するためのsend_useコマンド。コンソールへの文字列出力のためのsend_writeを実装。どちらもコンソール・ドライバ・スレッドに対してコマンドを送信することによって処理を実行している。

コマンドのメイン関数であるcommand_mainを実装。

まず、send_useを呼びコンソール・ドライバ・スレッドの初期化。
メインループに入り、kz_recvによってコンソール・ドライバ・スレッドからメッセージ受信を待つ。
コンソール・ドライバ・スレッドの割込みハンドラからMSGBOX_ID_CONSINPUT宛に受信文字列が送信される。
受信文字列の最初がechoの場合には後続の文字列を出力する。それ以外はunknown
文字列出力はsend_writeを呼び出し、コンソール・ドライバ・スレッドにCONSDRV_CMD_WRITEのメッセージが送信される。

main.cで起動するスレッドを修正。
Makefileにコンパイル対象を追加。

実行

kzload> run
starting from entry point: ffc020
kozos boot succeed!
command> echo aaa
 aaa
command>

できた。


ようやく全部終わった!最後の最後でserial.cがミスってたのに気づいて直した。割込みでシリアルの状態を判定するってのが無かったから気づかなかったのか。だいぶ時間使ってしまった。

さて、これでOSとしての「CPU」「メモリ」「I/O」「割込み」という大きな仕組みを実装することができた。まだまだ知識レベルが低いけど、やる前よりだいぶ言葉の定義とかどんな感じなのかとか掴めてよかったなー。あとは時間のあるときに最初から流し読みだけしておこう。

なんかOS周りのワードとか実装イメージに困ったら戻って来れそうな良書だったと思う。さて、そろそろ勉強の時間をやめて新しくなんか作る方にシフトしようかな。今年のアドベントカレンダーはCPUの作り方でも良いかも知れん。新しい言語を扱ってみるってのもありだなぁ。

以上でアドベントカレンダー2017はおしまい!!!
ソースコードはこちらから