(注意)このブログは本家のほうの文章部分のみの転載です.ソースコードの配布,画像などについては本家のほうを参照してください.文章中のリンク先は面倒なのですべて本家のほうに変換してしまっているのでご注意ください.

いよいよ,割り込みに優先度づけをすることで,KOZOSを完全にリアルタイムOS化する.まあ僕的に「完全なリアルタイムOS」ってなものがどんなものかまだいまいちよく理解できていないところがあるのだが,とりあえずこれでリアルタイムOSと言えるのではないか...というところまで持っていってみよう.

割り込みの優先度づけなのだけど,前回実装した kz_precall() システムコールにより,スレッドのディスパッチの直前に特定の関数が呼ばれるように登録しておいて,で,その関数内部で優先度の低い割り込みをマスクすることで実装する.これにより,ある割り込みが入ってその処理スレッドが動作している最中に,別の優先度が低い割り込みが入ることはなくなり,割り込みに優先度をつけて,完全にリアルタイムOS化することができる.

実はこの優先度づけの実装の前段階として,第36回,第37回で準備を行ってきたので,今回の実装はそれほど難しくはない.ソースは以下のような感じ.

変更点については diff.txt を参照してほしいのだが,まず,割り込みのマスク/マスク解除用のサービス関数として mask, unmask という関数ポインタをstruct interrupts に追加している.これはそれぞれ割り込みをマスク/マスク解除するためのサービス関数になる.で,タイマ割り込みとコンソール割り込み用のマスク/マスク解除関数として,timer_mask(), timer_unmask(), cons_mask(), cons_unmask() を追加している.内容は,制御ソケットを使って子プロセスに割り込みのマスク要求を出すだけだ.これはKOZOSを実機で動作させる場合には,割り込みマスクレジスタを直接操作するという実装になる.

さらに,kz_precall() で登録する割り込みマスク処理関数として,preproc() を追加している.内容は,自分より優先度の高い割り込みは unmask() を呼び出してマスク解除し,優先度の低い割り込みは mask() を呼び出して割り込みマスクをしているだけだ.

あと kz_precall() によって,スレッドごとにマスク処理関数が呼ばれるようにしてある.extintr の場合は全マスク,その他のスレッドの場合はマスク無しになるように手を入れている.ここまでが extintr.c の修正内容だ.

main.c は,スレッドの優先度を割り込み優先度に合わせて調整した.前回説明したが,現状で利用されている割り込みの優先度は,以下のようになっている.(これは extintr.c の extintr_intr_regist() の呼び出しで決定されている)
  • 優先度0の割り込み ... タイマ割り込みの0番
  • 優先度1の割り込み ... コンソール割り込みの0番
  • 優先度2の割り込み ... タイマ割り込みの1番
で,上から順に clock.c, command.c, timerd.c で利用されている.なので clock スレッドを優先度10,commandスレッドを優先度11,timerdスレッドを優先度12として優先度の順番をそろえた.あとそれ以外のスレッドは割り込みとは関係が無いので,優先度を20番代にして低くしておいた.

thread.c は,kz_precall() でデフォルトの関数を登録できるように改造した.これにより,kz_precall() でディスパッチ前のコールバック関数を登録していないスレッドの場合に,デフォルトのコールバック関数を呼び出させることができる.これは優先度の低い一般スレッドがディスパッチされる際に,すべての割り込みのマスクを解除するために利用する.

最後に,timerd.c をちょろっと修正.

で,動作させてみたところ,とりあえずタイマとコンソールは従来通り動いた.ただ,たまに動作が停止して固まってしまうようなんだよね...うーん原因不明.
AD
(注意)このブログは本家のほうの文章部分のみの転載です.ソースコードの配布,画像などについては本家のほうを参照してください.文章中のリンク先は面倒なのですべて本家のほうに変換してしまっているのでご注意ください.

割り込みの優先度づけ(割り込み優先度の導入)なのだけど,現状の extintr の作りが タイマとコンソールにべったりと張りついたかんじになっていて,このまま 優先度を導入すると,なんかぐちゃぐちゃになってしまいそう.

なので,準備としてちょっと extintr を整理しよう.

まず,子プロセスで行っている作業(割り込みコントローラの動作に相当する部分)は, extcont.c に移動する.あと,ライブラリ関数的なものは extsub.c に移動. で,KOZOSでの割り込み処理スレッド(extintrスレッド)の処理は extintr.c に残す.

これによりソケットまわりの処理は完全に extcont.c に切り離せることになる. もともとソケットを利用しているのは,子プロセスがシリアルコンソールの動作を telnet接続によって模擬しているためだ(KOZOSからは,ソケット通信なのかどうかは わからない).なのでソケットまわりをぜんぶ子プロセス処理(extcont.c)に 持っていって,KOZOSカーネルとは関係が無いようにしてしまうわけだ.

さらに割り込み優先度を設定するために,割り込みを一般化(抽象化)したものとして struct interrupts というクラスを作成する.まあちょっとクラスっぽくない部分も あってクラスと言うにはちょっと語弊があるのだけど,まああまり気にしない.

で,struct interrupts の配列として interrupts[] というのを用意して, タイマとコンソールの各割り込み(timerreg[] と consreg[])は,interrupts[] の 配列要素のどれかに対応するようにする. 現状でタイマ割り込みは4本,コンソール割り込みは4本あるので, interrupts[] は8個の要素を用意して,それぞれ対応させるようにする.

タイマとコンソールの各割り込みは struct timerreg と struct consreg で管理されて いるのだけど,これらはそのまま残す.というのは, 実際のハードウエア・プラットホームではハードウエアのレジスタ操作により タイマコントローラやシリアルコントローラを操作することになる. で,KOZOSではこれを子プロセスとのパイプ通信で模擬している. なので,レジスタ操作にかかわる部分(パイプ通信部分)はそのまま timerreg, consreg として残して,明確にしておきたいからだ.この部分はKOZOSを実ハードウエア上で 動かす際に,実際のハードウエアレジスタの操作に置き換えれば,そのまま動くことに なる.(実はもっとオブジェクト指向っぽくして,継承や多態性を使ってきれいに できるとも思うのだけど,そーいう理由であえてそうしない)

あとメッセージフォーマットとかをちょっと見直してみた. ソースは以下のような感じ.

いちおう,タイマとコンソールがきちんと動作することは確認した. extintr まわりを前回からわりとごっそり書き換えてしまったのだけど, まあやってることは変わっていないのでそれほどわかりにくくはないと思う.

割り込み優先度(まだ優先度は無いのだけど)を登録しているのは extintr.c:extintr_main() の以下のぶぶん.

extintr_intr_regist(&interrupts[0], &timerreg[0], timer_init);
extintr_intr_regist(&interrupts[1], & consreg[0], cons_init);
extintr_intr_regist(&interrupts[2], &timerreg[1], timer_init);
extintr_intr_regist(&interrupts[3], & consreg[1], cons_init);
extintr_intr_regist(&interrupts[4], &timerreg[2], timer_init);
extintr_intr_regist(&interrupts[5], & consreg[2], cons_init);
extintr_intr_regist(&interrupts[6], &timerreg[3], timer_init);
extintr_intr_regist(&interrupts[7], & consreg[3], cons_init);

たとえば上記1行目は,こーいうふうに読む.

「タイマ割り込みの0番を優先度0の割り込みとして登録し, timer_init()を呼び出して初期化する」

これにより,以下のように割り込みが登録される.
  • 優先度0の割り込み ... タイマ割り込みの0番
  • 優先度1の割り込み ... コンソール割り込みの0番
  • 優先度2の割り込み ... タイマ割り込みの1番
  • 優先度3の割り込み ... コンソール割り込みの1番
  • 優先度4の割り込み ... タイマ割り込みの2番
  • 優先度5の割り込み ... コンソール割り込みの2番
  • 優先度6の割り込み ... タイマ割り込みの3番
  • 優先度7の割り込み ... コンソール割り込みの3番
今回は優先度をつける準備として,ソースコードを整理しなおした. 次回は,いよいよ実際に優先度づけをしていこう.
AD
(注意)このブログは本家のほうの文章部分のみの転載です.ソースコードの配布,画像などについては本家のほうを参照してください.文章中のリンク先は面倒なのですべて本家のほうに変換してしまっているのでご注意ください.

前回までで割り込みとタイマ周りをすっきりさせることができたので,いよいよ割り込みに優先度をつけて,リアルタイム性をさらに高めたい.

まず割り込みの優先度についてなのだけど,やりたいことは,ある割り込みを受けてあるスレッドが動作可能になったときに,特定の割り込みをマスクすることで,優先度の低い割り込みに割り込まれないようにすることだ.

優先度の低い割り込みが発生したときに,(たとえスレッドが切り替わらないにしても)割り込みハンドラが呼ばれてしまっては,結局はもともと動いていた(もっと優先度の高い)スレッドの処理が待たされてしまう.なので割り込みをマスクすることで,優先度の低い割り込みを完全に停止させなければならない.

結局のところ,スレッドのディスパッチ時に割り込みマスクされるような機能を追加することで,割り込みに優先度をもたせてリアルタイム性を確保することができる.

割り込みのマスクは,OSのカーネルが行う必要がある.というのはスレッドのディスパッチ直前(もしくはディスパッチと同時)に,そのスレッドの優先度に応じて割り込みマスクをかける必要があるからだ.

で,KOZOSでの割り込みのマスクのしかたなのだけど,現在,割り込みまわりはextintr スレッドが管理している.なので手っ取り早く考えつくのは,スレッドのディスパッチ前に extintr に割り込みマスクを行ってもらうという方法だ.具体的には,KOZOS がスケジューリングの後,スレッドをディスパッチする前にextintr スレッドに(カーネル内部から)依頼メッセージを投げて,さらに extintr をいったんディスパッチして,extintr スレッドに割り込みマスク処理をしてもらうようにする.(カーネル内部からメッセージを投げるには,メッセージ送信関数であるthread_send()を直接呼べばよい)

でもこの方法はダメ.というのは,extintr スレッドに依頼して割り込みマスクをしてもらった後にスケジューリングして本来のスレッドをディスパッチしようとするが,ここでまた extintr への依頼が発生してしまい,無限ループになってしまうからだ.これを防ぐためには,2度目は extintr にメッセージを投げないようになんらかの対処をする必要があるが,これはちょっとめんどいし,多重に割り込みがかかった場合になんかよくわからんことになりそうな気がする.

もうひとつの方法は,extintr が行っている割り込みまわりの処理をKOZOSのカーネルに持っていき,カーネルにやらせるというものだ.現状では各スレッドは extintr にメッセージを投げて割り込み処理を依頼しているが,これらのサービスはカーネルがシステムコールとして実装することになる.割り込みまわりはすべてカーネルの管理になるので,ディスパッチ前の割り込みマスク処理も容易だ.

で,ふつうの組み込みOSはそーいう作りになっている思うのだけど,KOZOSはなるべくサービスをスレッド化して,カーネル内部はすっきりさせていきたいという考えがある.実際に割り込みまわりのサービスも extintr スレッドが行っているし,前回はタイマサービスもスレッド化している(まあこれは,リアルタイム性の無い部分を外に切り出す,という意味も強いのだが).

ということで,割り込みまわりの管理とサービスは extintr に任せたまま,スレッドのディスパッチ前には割り込みマスクをかけるような実装にしたい.いろいろ考えたのだが,こんな実装はどうだろうか.
  • スレッドのディスパッチ前に特定の関数を呼ばせる機能をカーネルに追加する.具体的には,新規システムコール(kz_precall)を追加する(スレッドIDと関数ポインタを指定して kz_precall を呼ぶと,指定したスレッドがディスパッチされる直前に指定した関数が呼ばれるようになる).
  • extintr は各スレッドから割り込み処理(割り込み発生時には,そのスレッドにメッセージを投げる,という処理)の依頼を受けたら,kz_precall()でそのスレッドがディスパッチされる前に呼ばれる関数を設定する.
  • 上記関数には,特定の割り込みをマスクするような処理を記述しておく.これにより,割り込みが入って処理スレッドが動作する前に,特定の割り込みがマスクされるようになる.(マスクパターンを階段状にすることで,割り込みに優先度をつけることができる)
ということで,まずは準備として kz_precall() システムコールを追加してみた.ソースは以下のような感じ.修正内容は簡単で,スレッド構造体に関数ポインタ保存用の precall というメンバを追加し,kz_precall システムコールで設定,さらにディスパッチ前に関数を呼ぶような処理を入れているだけ.

ちなみに thread.c:thread_intrvec() でのスケジューリング処理の直後にコメントがあるように,KOZOSではシグナルのとりこぼしを防ぐために,ディスパッチ前に一瞬だけシグナルマスクを開けている.これは setcontext() でコンテキストが切り替わるためにシグナルが失われてしまうことが原因(このため実ハードウエア上で動作させるならば,この処理は不要だろう)なのだけど,precall の実行はこの蓋開けの前に行う必要がある.蓋開け後だと,precall によるマスク処理中に割り込みが入ってきたときに,なんかわけのわからんことになる気がするし,蓋開け前でマスク処理の設定をして,蓋開けによって割り込みが入ったとしても,再度スケジューリングが行われて必要に応じて precall が再度呼ばれてマスクが設定されなおすので,とくに問題は無い.

あー眠い.次回はいよいよ extintr にマスク指定処理を追加して,割り込みを優先度づけしてみよう.
AD
(注意)このブログは本家のほうの文章部分のみの転載です.ソースコードの配布,画像などについては本家のほうを参照してください.文章中のリンク先は面倒なのですべて本家のほうに変換してしまっているのでご注意ください.

前回の話の続きなのだけど,リアルタイム性のある厳密なタイマが必要な場合には,RTCを直接利用するのが適切だ.このため厳密性のあるタイマは,システム内で固定の数しか利用できない.まあタイマサービスを固定サイズの配列化して,配列をなめてタイマをかけるような実装をすれば(軽さを犠牲にして)数を増やすことはできるだろうが,利用数に上限があることには変わりは無い.

半面,一般的なスレッドではそれほど厳密なタイマが必要とされることは少なく,場合によっては数秒の誤差があっても実害は無いことも多い.このようなタイマは,RTCをひとつ利用して,タイマキューによってソフトウエア的に実現しても問題はない.ここまでは前回の話.

で,今回はextintrのもつタイマ資源(TIMER_NUMで定義されている数.現状で4個)のうちのひとつを利用して,タイマキューによるソフトウエアタイマサービスを実装してみよう.

もともとKOZOSはタイマサービスを持っているのだけどこれはタイマキューによって実現されているのでリアルタイム性は無い.なのでOSの内部に持っているのは適切ではない.ということで,従来の kz_timer() によるKOZOSのタイマサービスをスレッド化して,タイマキューを管理するタイマサービススレッドを作成する.

ソースは以下のような感じ.

タイマサービス用スレッドは timerd という名前にした.ファイルは timerd.c だ.

で,KOZOSの kz_timer() の処理部分(タイマキューの処理)を切り出して timerd.c に移動して,スレッド化している.あと kz_timer() のシステムコールまわりの定義をごっそり消してすっきりさせた.

timerd は extintr のタイマをひとつ利用して,ソフトウエアタイマサービスを提供する.使いかたは,タイマを起動したい時間をメッセージで送るだけ.タイマが満了するとメッセージを返してくる.タイマはタイマキューで処理されるので,複数スレッドから同時に利用できる.もちろんひとつのスレッドが複数のタイマをかけることもできる(ただしその場合,どのタイマが満了したのかをメッセージから判断できないね.これはそのうち対処が必要だろう).

くどいようだけど,内部ではタイマキューの検索処理などを行っているので,タイマに厳密性はない.タイマキューの長さによって,タイマ満了が多少遅れる可能性も十分にある.外部割り込みなどで extintr スレッドが動きっぱなしになってtimerd が動けないと,タイマが全然かからない,という可能性もある.

あと,細かいバグをちょっと直してある.まあこのへんは diff.txt を参照.

で,実験用に clock.c, clock2.c, clock3.c の3つのファイルを用意した.clock.c は extintr のタイマをひとつ利用して,1秒間隔でメッセージ出力する.clock2.c, clock3.c は timerd のタイマサービスを利用して,それぞれ1.5秒,1.8秒間隔でメッセージ出力する.

実行結果は以下.問題なく動作しているようだ.

hiroaki@teapot:~/kozos35>% ./koz
clock1 Mon Jan 19 21:51:36 2009
clock2 Mon Jan 19 21:51:36 2009
clock3 Mon Jan 19 21:51:36 2009
clock1 Mon Jan 19 21:51:37 2009
clock1 Mon Jan 19 21:51:38 2009
clock2 Mon Jan 19 21:51:38 2009
clock3 Mon Jan 19 21:51:38 2009
clock1 Mon Jan 19 21:51:39 2009
clock2 Mon Jan 19 21:51:39 2009
clock1 Mon Jan 19 21:51:40 2009
clock3 Mon Jan 19 21:51:40 2009
clock1 Mon Jan 19 21:51:41 2009
clock2 Mon Jan 19 21:51:41 2009
clock1 Mon Jan 19 21:51:42 2009
clock3 Mon Jan 19 21:51:42 2009
clock2 Mon Jan 19 21:51:42 2009
clock1 Mon Jan 19 21:51:43 2009
clock1 Mon Jan 19 21:51:44 2009
clock3 Mon Jan 19 21:51:44 2009
clock2 Mon Jan 19 21:51:44 2009
...

(注意)このブログは本家のほうの文章部分のみの転載です.ソースコードの配布,画像などについては本家のほうを参照してください.文章中のリンク先は面倒なのですべて本家のほうに変換してしまっているのでご注意ください.

前回の続きで,割り込みまわりを改造していきたい.今回はタイマ割り込みだ.

タイマ処理についてなのだが,現状では各スレッドが kz_timer() システムコールによって自由にタイマ設定できる.KOZOSの内部では,タイマキューを検索して適切な位置にタイマ登録し,タイマキューの先頭に対して getitimer() で SIGALRM をかけている.実際のハードウエアでは,SIGALRM はリアルタイムクロック(RTC)からのタイマ割り込みということになる.

このようなタイマキューの使いかたは,タイマ処理をしたい場合の定石の書き方だ.タイマキューを使うことで,ひとつしかないタイマ資源(KOZOSの場合は,SIGALRMによるシグナル)でいくつものタイマをかけることができる.

ただしこれはリアルタイムOSとして考えると,あまり良い方法ではない.タイマ設定時に必ずタイマキューの検索が入ってしまうため,リアルタイム性を確保できないからだ.

で,リアルタイムOSではどのようにするかなのだけど,ちょっといろいろ考えたのだけど,本当に厳密なタイマ処理をしたい場合には,タイマキューのような仕組みはNGだ.しかし,各スレッドがちょっと数秒単位でのタイマをかけたい場合には,厳密なタイミングやリアルタイム性が必要ではない場合も多い.

ということで結論としては,厳密なリアルタイム性が必要なタイマは,ハードウエアが持っているRTCをそのまま使うしかない.たとえばハードウエアが4つのRTCを持っているならば,タイマ資源を4つのみに限定して,タイマを必要とするスレッドは,RTCを直接利用しまえば,リアルタイム性を確保できる.

ただしこれでは,タイマは4つまでしか利用できないので,他のスレッドがすでにタイマを利用していると,もうタイマを使うことはできなくなってしまう.ということで,4つのうちのひとつだけはタイマキューを実装して,精度を必要としないスレッドは,タイマキューのほうのタイマを利用すれば,タイマはいくらでも使うことができることになる.

この場合,厳密なリアルタイム性を持つタイマとして,3つのタイマ資源を利用できる.またそれとは別に,あまり厳密でない(そしてリアルタイム性も無い)タイマを(こちらはキュー管理しているので,論理上は無限に)利用できることになる.

ついでにタイマキューの処理はリアルタイム性を確保できない(タイマキューの検索があるので).このためタイマキューの処理は専用のスレッドに任せてしまうのがいいと思う.

結局どのような構造になるかというと,たとえばタイマ資源としてRTCを4つ搭載しているようなハードウエアならば,以下のような感じだ.
  1. 4つのタイマ資源(RTC)のうち,3つは一般スレッド用に解放する.このためユーザスレッドは,リアルタイム性のある(厳密なタイミングで動作できる)タイマを全体で3つまで利用できる.
  2. 4つのRTCの残りのひとつは,タイマ管理スレッドが利用する.タイマ管理スレッドはタイマキューを持ち,,RTCを利用してタイマ動作することで,ユーザスレッドにタイマサービスを提供する.
  3. 厳密なタイミングを必要とする場合は,上記1のタイマ資源を利用する.これは有限の資源なので,あるスレッドがすでに利用していたら,他のスレッドは利用できないことになる.
  4. 厳密なタイミングを必要としない(数ミリ秒ずれても構わない)場合は,上記2のタイマサービスを利用する.タイマ登録は,タイマ管理スレッドにメッセージを投げることで依頼する(タイマ満了でメッセージが返ってくる).タイマ管理スレッドはタイマキューの検索を行うためその動作にリアルタイム性は無いが,スレッド化してあるのでOSのリアルタイム性への影響は無い.
つまり,リアルタイム性を確保できない部分(タイマキューの処理)はタイマ管理スレッドに任せてOSの外に出すことで,OSのリアルタイム性に悪影響が出ないようにしている.

ひとつのタイマで汎用的なタイマサービスを実現したい場合,タイマをキュー管理することでリアルタイム性が無くなってしまうことは避けられない.このため,リアルタイム性と厳密さが必要な処理に関しては,専用のRTCを割り当てるのが正しい考えだ.

で,extintr.c にタイマ割り込み処理を実装してみた.ソースは以下のような感じ.

ついでにコンソール処理での w コマンドによる出力処理でレジスタの検索を忘れていたバグを修正.(以下)

case 'w': /* 出力 */
+ irp = intrreg_search(id);
write(irp->write_fd, &p[1], size - 1);
break;



タイマ割り込みの処理を図にするとこんな感じ.ちなみに従来の clock スレッドを,新設したタイマを利用するように修正してある.

(タイマ設定と動作時)

サービススレッド extintr 子プロセス
(clockスレッドなど)
| | |
| kz_recv()待ち select()待ち
| | |
kz_send()=========>| |
| kz_recv() |
| write()----------->|
kz_recv()待ち | read()
| | |
| | select()
| | |
| | (タイマ満了)
| | |
| |<------------write()
| |<======------SIGHUP
| kz_recv()(※1) |
| read() |
| | |
|<===========kz_send() select()待ち
kz_recv() | |
| kz_recv()待ち |
| | |

---> は,ソケットによる接続や通信
===> は,KOZOSのメッセージ送信

※1 kz_setsig() システムコールにより,シグナル受信は
メッセージによりextintrに通知される.

extintr.c の処理には,タイマ処理が追加してある.前回実装したソケット通信処理(コンソール処理)に対して,同じような感じでタイマ処理が追加されている.(タイマとコンソールを別の子プロセス&別スレッドにしてもよかったのだが,将来的にタイマとコンソールをひとつの処理で割り込みマスクできるように,extintr.c にまとめてある)

まあ具体的な処理は,前回追加したコンソールの処理とだいたい同じだ.extintrスレッドと子プロセスの間でソケットを張り,ソケット経由でタイマの起動を依頼すると,子プロセスがタイマ動作して満了時にはソケットに通知し,SIGHUPを上げてくる.で,extintr は SIGHUP を受信したらソケットを調べ,タイマ満了している場合には当該スレッドにメッセージを送信する.

あと clock.c は,従来はKOZOSのタイマサービスを利用していたが,extintr が持つタイマを利用するように修正してある.

extintr がタイマ実装されたことで,KOZOSのタイマサービス(kz_timer())は不要になる.まあこの処理はリアルタイム性が無いのでOSの中に残しておくのはまずいので,そのうち削除しよう.あーそれと,汎用的なタイマサービスのためにタイマ管理スレッドを実装する必要があるなあ(KOZOSのタイマ処理をもとにすればいいだろう).まあこれは後ほど.

実行結果は以下.いちおう,ちゃんとタイマ動作しているようだ.telnetによる接続も問題ないみたい.

hiroaki@teapot:~/kozos34>% ./koz
Mon Jan 19 00:30:13 2009
Mon Jan 19 00:30:14 2009
Mon Jan 19 00:30:15 2009
Mon Jan 19 00:30:16 2009
Mon Jan 19 00:30:17 2009
...