(第40回)最悪応答時間について考える

2009/02/25

あなたは 人目のお客様です.

さて,ここまででKOZOSのリアルタイムOS化を進めてきたのだけど,いよいよ大詰めだ.

まず手始めとして,割り込みの最悪遅延時間について考えてみよう.

前回説明した通り,KOZOSはOS内部は割り込み禁止で走っている. なので割り込みが入ったときに,実際にはすぐに割り込みハンドラなどが 呼ばれるわけではなく,ある程度待たされる.問題は,これが最悪の場合に どれくらい待たされるのかということだ.この最悪遅延時間がきちんと見積もれる ことは,リアルタイムOSとしての必須条件だ.

まず前回までの復習だが,カーネル内部処理(システムコール処理)中に割り込みを 受け付けると,割り込み処理後にカーネル内部処理に戻って来なければならず, 割り込み処理後のスレッドの動作のリアルタイム性を保証できない. 割り込みハンドラにレベルを設けて,ハンドラ実行中の(もっと優先度の高い) 割り込みを受け付けたとしても,同様の問題が発生する. これは既知の問題で,この防止策としてKOZOSではOS内部処理 (kz_sethandler()で設定した割り込みハンドラ実行中や,システムコールの処理中) は割り込み禁止にしている.

また,割り込みハンドラ中でキューの検索とか行うと,割り込み禁止区間の処理時間が 見積もれないことになり,リアルタイム性が無くなる原因になる.よって割り込み ハンドラで,リアルタイム性の無い(処理時間を見積もれない)処理をしてはいけない.

結局のところ割り込みハンドラで行うのは,割り込みの刈り取りとデータ受信と スレッドへのメッセージ送信くらいだ.これは kz_setsig() により extintr で 行って,スレッドはメッセージを受けるようにすれば,割り込みハンドラは不要と なる.なのでKOZOSは割り込みハンドラは使わずに,kz_setsig() & extintr で 割り込み処理するという方向性になっている. (kz_sethandler() もいちおう残してはいるが,推奨しない)

また割り込みハンドラからのシステムコール呼び出しだが,割り込み中の割り込みに なってしまい,そのままでは呼び出せない (割り込み中は割り込み禁止.OS内部は割り込み禁止で走っている). まあ実際にはソフトウエア割り込みなので,特別扱いしてもいいような気もするし, システムコール割り込み(実体はSIGSYS)だけ蓋を開けることはできるのだが, なんか気持が悪いので,OSのサービスを,スレッド用のもの(システムコール)と 非コンテキスト用のもの(サービスコール)で明確に分けるようにした. で,サービスコールは単なる関数呼び出し(カーネル内のサービス関数を直接呼び出す) になっている.これは前回の話.

さらに現状で,KOZOSのカーネル内部では,キューの検索などといったような, 処理時間を見積もれないような箇所は無い. これは前回にちょっと説明を忘れたことなのだけど,前回新設したサービスコールに しても,処理時間を見積もれない処理は無く,すべて処理時間が見積もれる (kx_kmalloc()によるメモリ獲得も,キュー検索などはしないように書いてある). これはリアルタイムOS化する上で,非常に重要なことだ.

なお extintr は最優先のスレッドなので,extintr の場合は全割り込みマスクする. でないとあるスレッドの実行中に割り込みが入りextintrが実行され, ここで割り込みマスクが開いてしまうと,もっと優先度の低い割り込みを 受け付けてしまい,もとのスレッドが割り込みマスクしている意味が無くなって しまうので.

で,割り込み最悪応答時間なのだけど,たとえばある割り込みAを受けてスレッドAが 動作するとして,割り込み要因発生からスレッドAが動作するまでの動作を 考えてみよう.

  1. 割り込みAが発生.(子プロセスが SIGHUP を発行)
  2. KOZOSの割り込みハンドラ(thread_intr())が呼ばれ,その延長でextintrに向けて メッセージ発行される.(extintrをkz_setsig()で登録してあるので)
  3. 割り込み処理の終了後にスケジューリングが行われ,extintrがスケジュール される.(extintrは最高優先度なので)
  4. extintrのディスパッチ前ハンドラが呼ばれて割り込みが全マスクされる.
  5. extintrがディスパッチされる.
  6. extintrがメッセージを受け,extintr_handler()により割り込みチェックを行う. 割り込みAが発生しているので,その処理スレッド(スレッドA)に対して メッセージを投げる.
  7. extintrの処理が終了し,スレッドAがスケジューリングされる.
  8. スレッドAのディスパッチ前ハンドラが呼ばれ,(extintr実行によって) マスクされていた割り込みが解除される.
  9. スレッドAがディスパッチされる.
注意として,途中で割り込みAよりももっと優先度の高い別の割り込みがかかることは 考えない.というのは,優先度の高い割り込みがかかったらスレッドAの処理を後回し にしてそっちを優先的に処理する必要があるので,割り込みAの応答は遅れて当然 だからだ. (たとえば優先度の高い割り込みがかかりっぱなしになったらスレッドAは一切動作 できないので,スレッドAの最悪応答時間は無限大になってしまうが,これは当り前の ことである.なので,割り込みAよりももっと優先度の高い割り込みがかかることを 考える意味は無い)

で,上記4の動作の部分には現状でちょっと問題がある.ここで割り込みは全マスク され,その後は8の動作までマスクされたままなので,この間に優先度の低い 割り込みが発生することは無い.マスクされているので,割り込み要因が発生しても SIGHUPが発行されないわけだ. (まあもっとも現状では子プロセスが割り込みコントローラを模しているので, 子プロセスの動作が遅れたらすれ違いでSIGHUPが発行されてしまう可能性はあるの だが,実ハードウエアでは割り込みマスクはレジスタ操作で即有効になるので, ここではそのようなことは考えないことにする)

もしも上記6の動作の前に,割り込みAよりも優先度の低い別の割り込みが発生していた 場合のことを考えよう.この場合,優先度の低い割り込みはマスクされているので SIGHUPは発行されないのだが,6の処理では以下のようにして,割り込み状態を 総なめで調べている.

static void extintr_handler()
{
  int i;
  struct interrupts *intp;
  for (i = 0; i < EXTINTR_INTERRUPTS_NUM; i++) {
    intp = &interrupts[i];
    if (intp->type == INTERRUPTS_TYPE_UNKNOWN)
      continue;
    if (intp->checkintr(intp)) {
      intp->intr(intp);
    }
  }
  return;
}
なので,優先度の低い割り込みがかかっていた場合に,その割り込みが検知され, 処理関数が呼ばれてしまうことになる.まあ処理関数と言っても実際には サービスコールによってスレッドにメッセージを投げるだけなのだが, 優先度ということを考えると,なんかいまいちな作りだ. (割り込み応答性能を考える際に,優先度の低い割り込みの処理時間もすべて計上 しなければならないことになる)

まずいのは上記4の動作の直前に優先度の低い割り込みが発生した場合だ. この場合にはマスク前なので,SIGHUPが発行されてしまう.この場合には,4の直後 (ディスパッチ直前)の割り込み蓋開け処理でシグナルハンドラが呼ばれることになる. で,extintr_handler()による割り込み検知が行われてしまうことになる (検知するのが優先度の高い割り込みならばこれはしょうがないのだが, 優先度の低い割り込みであっても検知してしまう). なので最悪応答時間の見積もりでは,4の直後にSIGHUPの処理が行われる, ということも考慮しなければならない.

まあもっともこれらはすべてカーネル内部処理と extintr の処理なので, 最悪値として見積もることはできるのだが. (カーネル内部処理中と割り込みハンドラ実行中の割り込みを許していない (カーネル内部処理はネストできない)ので,割り込み応答の最悪時間が長くなるのは しかたがないといえる)

このように割り込みAの処理中に優先度の低い割り込みが検知されてしまう可能性が あるのだが,優先度の低い割り込みを検知したところで,その割り込み処理スレッド が動作できるのはずっと後だ(割り込みAとスレッドAのほうが優先度が高いので, その前にそっちの処理が行われるから).ということで,extintr_handler() で 割り込み検知する際に,割り込み優先度に応じて必要なぶんだけ調べるように改良 しよう.これにより,優先度の低い割り込みの処理は先送りされるので,割り込みの 最悪応答時間の見積もりが楽になる.(先述したように,途中でもっと優先度の高い 割り込みがかかった場合のことは考えない)

ただしそのように修正すると,優先度の低い割り込みのSIGHUPが空打ちされてしまい (たとえば割り込み禁止区間で割り込みAと同時に優先度の低い割り込みが発生すると, SIGHUPは1発しか発行されない),スレッドAの処理が完了した後で,その優先度の 低い割り込みの処理が行われない可能性がある. (extintr_handler()が呼ばれれば割り込み検知されるのだが, SIGHUPが消えてしまっているので,extintr_handler()が呼ばれるきっかけが無い)

この対策として,割り込みの優先度を上げる場合には,extintr に対してSIGHUP発生時 相当のメッセージを投げて,extintr_handler()が呼ばれるように手を入れる.

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

(2009/04/10 ライセンスに関する文書として,KL-01とLICENSEを追加. 詳しくは第43回を参照)

修正内容は上に書いた通り. ついでに extintr.c:preproc() で,必要なもののみマスク処理を行うように修正した.

さらに,前回の修正では実はサービスコール呼び出し時に変数 current がそのまま 残っていて,kz_send() が誤動作していた.この対策として,サービスコールの 呼び出し(srvcall_proc())の先頭で current をNULLにするように修正.

あと,これは前回ちょっと誤解していたのだけど,割り込み処理からシステムコールを 呼んだ場合には current == NULL となっているので, thread_intrvec() の

  if (current == NULL) {
    return;
  }
という処理でそのままシグナルハンドラから戻り,前の処理が続行されるように なっていた.なので,前回言った 「システムコールのあとにそのまま処理が戻ってこない」というのは間違いだった. ただ,サービスコール呼び出し時に current を NULL にするように修正したので, 上記処理が残っているとまずいし,そもそもサービスコールを呼んだ場合に current が変更されてしまい,NULLになっている可能性もあるので,やっぱし 上記処理はまずい.もともと上記の処理は第32回に割り込みハンドラ中から システムコールを呼べるようにするために追加した処理で,これは前回サービスコール を実装することで廃止した動作なので,今となっては不要な処理だ. ということで,上記の処理は削除する.

ついでに前回のコードだと,たまに動作が止まってしまうことがあったのだが, 今回の修正でなんかそれが発生しなくなった.優先度がらみで割り込みがマスクされ, そのままコンテキストが変わってしまうと,発生したシグナルが消えてしまうのかも しれない.まあちょっとよくわかっていないのだが.うーん不明.


メールは kozos(アットマーク)kozos.jp まで