(第41回)最悪応答時間についてもう少し考える

2009/02/26

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

前回に引続き,最悪応答時間についてもう少し考えてみよう.

前回は,割り込み検知の際に割り込みレベルに応じて割り込みチェックするように 修正した.で,現在のソースで実際に割り込みが入った場合のことを考えてみる.

たとえばある割り込みAが発生して,それに応じてスレッドAが動作するような場合を 考えてみよう.

割り込みAの発生により,KOZOSの割り込みハンドラ(thread_intr())が呼ばれる. 外部割り込み(SIGHUP)に対しては kz_setsig() で extintr が設定されているので, extintr スレッドにメッセージが投げられることになる.で,extintr は割り込みの 発生状態を調べて(extintr_handler()),各割り込みの処理スレッド (この場合はスレッドA)に対してメッセージを投げることになる.スレッドAは メッセージを受けて,当該の処理を行うわけだ.

図にするとこんなかんじか.

             カーネル         子プロセス         extintr          スレッドA
                │                │                │                │
                │                │           kz_recv()待ち     kz_recv()待ち
                │     SIGHUP     │←割り込み発生  │                │
                │←───────│                │                │
           割り込み処理           │  メッセージ    │                │
                │────────────────→│                │
            schedule()            │                │                │
     (extintrがcurrentになる)     │                │                │
                │                │                │                │
             precall()            │                │                │
    (割り込みが全マスクされる)(*2)│                │                │
                │                │                │                │
          割り込み蓋開け(*1)      │                │                │
                │                │                │                │
            dispatch()            │                │                │
                │────────────────→│                │
                │                │           割り込み検知           │
                │                │                │   メッセージ   │
                │                │                │───────→│
                │                │           kz_recv()待ち          │
                │←────────────────│                │
            schedule()            │                │                │
     (スレッドAがcurrentになる)   │                │                │
                │                │                │                │
             precall()            │                │                │
    (割り込みA以上のレベルの      │                │                │
     割り込みがマスクされる)      │                │                │
                │                │                │                │
  割り込み優先度が低くなるので,  │                │                │
  残っている割り込みが無いか検索  │                │                │
  するために,extintrに割り込み   │                │                │
  処理メッセージを投げる          │                │                │
                │   メッセージ   │                │                │
                │────────────────→│                │
                │                │           割り込み検知           │
                │                │                │                │
                │                │           kz_recv()待ち          │
                │←────────────────│                │
            schedule()            │                │                │
     (スレッドAがcurrentになる)   │                │                │
                │                │                │                │
    currentスレッドがスレッドAの  │                │                │
    ままで変化していないので,    │                │                │
    precall()処理は行われない     │                │                │
                │                │                │                │
          割り込み蓋開け          │                │                │
                │                │                │                │
            dispatch()            │                │                │
                │─────────────────────────→│
                │                │                │           割り込み処理
                │                │                │                │
                │                │                │                │
問題は(*1)の割り込み蓋開け処理(thread_intrvec()の末尾で setcontext() による ディスパッチの直前に SIG_UNBLOCK でシグナルマスクを開けている処理)なのだけど, これはディスパッチ前に一瞬だけ割り込みを開けて,待たされている割り込みがある ならば割り込み処理を行うようになっている.これは実は KOZOS をPC-UNIX上で動作 させるときの対処で,setcontext()でコンテキスト切替えを行うとシグナルが 引き継がれないために割り込みが消えてしまうことの対処なのだけど, ここで割り込みを受け付けてしまうので,たとえば(*2)での割り込み全マスク前に 別の割り込みが入っていたとすると,割り込み蓋開けによって(*1)の位置で割り込みを 受け付けてしまう.これは優先度の低い割り込みでも受け付けてしまうので,問題だ.

まあ実はこれはPC-UNIX上で動作させる場合の問題であり,実ハードウエアで動作させる ときには問題にはならないような気もするのだが,リアルタイム性ということを 考えるときにはちょっとなんだかなあなつくりだ.

このへんのつくりの問題は,そもそも割り込み処理を extintr というスレッドに 任せているという点にあるとおもう.このつくり自体はけっこう気に入っているの だが,割り込みの受け付けをスレッドが行っているため,割り込み処理の最中に 割り込みマスクの切替えなどを気をつけなければならないようなつくりになって しまっている.これは複数の割り込みがもっと複雑にからみあった状態で最悪応答時間 を見積もる際に,非常に複雑になってしまい都合が悪い.

ちなみに以下は現時点のソースでの割り込み処理の最悪応答見積もりのメモ. システムコールの処理中に割り込みが入って,システムコールの処理が終るまで 割り込み処理が待たされて,さらに割り込み処理中に別の優先度の低い割り込みが 入って...というイメージなのだけど,うーんわからん.ほんとはこれを図にして 載せようかと思っていたのだけど面倒なのであっさり断念.

ということで,割り込み検知を extintr でなく割り込みハンドラの延長で行うことで 割り込みマスクの遷移を無くして,もっと単純化しようと思う.

...と思ったのだけど,そーいうふうに修正してみたのだけどなんかうまく 動かない...以下が修正したソース.

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

とりあえず extintr.c の USE_MESSAGE という #define で動作を切替えられるように しておいたので,USE_MESSAGE を有効にして,従来動作(割り込み検知を extintr が 行う)で最悪応答を検証してみよう.

ここでは簡単のために,上の図の(*1)での割り込み蓋開け時の割り込みは考えない こととする.というのは,実ハードウエアではおそらくこのような蓋開け処理は 不要だからだ.

で,割り込みAが発生してからスレッドAが動作するまでの時間が最悪応答になるのは, あるスレッドのシステムコール処理中で割り込み禁止状態になっていて,その最中に 割り込みを受け付けた場合だ.この場合,システムコールの処理終了まで待ち合わせて から割り込み処理を行うことになる.で,実際の割り込みハンドラ呼び出しから スレッドAが動き出すまでは,上の図の動作をすることになる. ここで(*2)の前に優先度の低い割り込みが発生したとしても,(*1)の位置での 蓋開けを考えないならば,その割り込みの検知はずっとあとに遅れさせられるので, スレッドAの動作に影響を及ぼすことはない.(*2)のマスク処理以降は,優先度の 低い割り込みはマスクされるので,SIGHUPが発生することはないので問題は無い.

問題は,割り込み優先度が高→低に戻されるときに,優先度の低い割り込みが 残っていたら割り込み処理を行う,という動作だ.これは preproc() の末尾で 以下のようにして行っている.

#ifdef USE_MESSAGE
  if (id) {
    intrpri_old     = intrpri_current;
    intrpri_current = n;
    if (extintr_id && (intrpri_current > intrpri_old)) {
      kx_send(extintr_id, 0, NULL);
    }
  }
#else
  intrpri_old     = intrpri_current;
  intrpri_current = n;
  if (intrpri_current > intrpri_old) {
    extintr_handler(SIGHUP);
  }
#endif
しかしこれは preproc() の内部で行われているので,kz_precall()に設定した スレッドのディスパッチ前ハンドラ呼び出しによって行われる. なので,割り込み禁止状態で行われる.つまりここの処理は割り込み応答に そのまま影響する.で,最悪の場合には全割り込みをサーチすることになるので, 最悪応答時間に計上することになり,最悪応答時間がそれなりに悪化することになる.

う〜〜〜ん,いろいろ考えたんだけど,結局のところ全割り込みサーチはどこかで 行われることになるので,最悪応答に必ず計上しなければならないことに なるのかなあ...ちなみに以下が,修正後の場合の最悪応答の見積もりのメモ.

ちなみに,割り込み処理を extintr でなく割り込みハンドラで行う場合, extintr との排他を考える必要がある.どういうことかというと,extintr は 通常スレッドからの割り込み監視要求を受け付けて,割り込み処理用のテーブルの 設定を行う(extintr_mainloop()の延長で,interrupts[]に設定する). このテーブル設定と,割り込みハンドラでの割り込み処理時のテーブル参照が ぶつかると,なんかまずそうな気がするからだ.

まあもっとも現状で動的に設定しているのが interrupts[].id だけで, interrupts[] の他のパラメータは初期化時に設定してそのまま変化しないので, とくに問題は無いような気もする.あと extintr が動くときは allmask() によって 割り込み優先度が最高になっているために外部割り込みは入らないので, 実際には排他のことは考えなくてもいい気もする.ただ,本来ならばこのような 割り込み設定処理は extintr のようなスレッドに任せるのでなく, カーネルがシステムコールを持って,カーネル内部でやるべきなのかもしれない. (extintr が行う場合,extintr の実行中は割り込みマスクしているので, これはこれで最悪応答性に影響する)

うーん,なんか難しいなあ...とりあえず言えることは,KOZOSは今までOSとしての 機能を極力スレッドによって実現するというポリシーで作ってきた (割り込み処理を extintr スレッドに任せるなどが代表的). これはこれですっきりしたつくりになるのだが,しかし割り込み処理に関していうと, スレッドに任せるというのはリアルタイム性を考える上で非常によろしくない. KOZOSの現状のソースコードでは,スレッド化というポリシーと,リアルタイム性の 追求という方向性がぶつかってしまい,矛盾に陥っているように感じる.

リアルタイム性を考えるならば,割り込み設定処理は専用のシステムコールを作成し, 割り込み検知処理は割り込みハンドラ内部で行うことで,すべてカーネル内部で行う べきのように感じる.これらをスレッドに任せると,ディスパッチ時の優先度切替え とか割り込み受け付けとかを非常に気にしなければならなくなり,破綻するようだ.

まあちょっとよくわからなくなってきたので,残念だがこれはこれでとりあえず 置いておこう.ちょっと他にやりたいことがたまっているので,今後はそっちを 進めることにしよう.

ちょっと extintr.c が複雑化しているので,いっそのことリアルタイム性を まったく考えずにシンプルさとわかりやすさを追求したバージョンを別に作っても いいかもしれないなあ.実は今後は実ハードウエアへの移植を考えたいのだけど, 一回,もうちょっとシンプルに書き直してもいいかもしれない.


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