(第24回)プリエンプティブってどーいうこと?

2007/11/25

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

前回までで,KOZOSの大枠はだいたいできあがった. というより,最後のほうはGDBスタブの実装の話がメインになっていて, あんましOSの話題になっていなかった.まあGDBスタブに関しては, 組み込みでは必須の機能だとも思うのでそれはそれでいいと思うのだけど, 今回から「第2部」と称して,OSっぽい話をしていきたいと思う. それも理論的な話とか概念的な話ではなく, 実際にKOZOSを動かして試してみる,という路線で説明していきたい.

今回は手始めに「優先度ベースのスケジューリング」という点について試してみよう.

他の組み込みOSでも一般的にそうなのだが,KOZOSのスケジューリング方式は 「優先度」をベースとしたものだ. 簡単に言うと,スレッドごとに優先度が決まっていて, 「動作可能状態」かつ「優先度が最も高い」スレッドが, 次にディスパッチされるというものだ.

ここでいきなりだけどちょっとソースを修正する.

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

修正点は以下.

diff -ruN kozos23/thread.c kozos24/thread.c
--- kozos23/thread.c	Sun Nov 25 19:50:10 2007
+++ kozos24/thread.c	Sun Nov 25 19:52:06 2007
@@ -254,7 +254,7 @@
   return 0;
 }
 
-void alarm_handler()
+static void alarm_handler()
 {
   kz_timebuf *tmp;
 
@@ -369,7 +369,7 @@
   return;
 }
 
-static void dispatch()
+static void schedule()
 {
   int i;
   for (i = 0; i < PRI_NUM; i++) {
@@ -412,7 +412,7 @@
     break;
   }
   extintr_proc(signo);
-  dispatch();
+  schedule();
 
   /*
    * スタブでの read() 待ちブロック中に SIGALRM (および SIGHUP)が発生した
まあたいした修正ではない,というか論理上は影響の無い修正なのだけど, 従来の dispatch() 関数で行っていた処理は,実はディスパッチではなく スケジューリングだ.なので関数名を schedule() に変更した. 実はこの関数名の間違い,ずっと気になっていたんだよね...なので今回, いい機会なので直してしまった.

言葉の定義についてちょっとはっきりさせておくと,

となる.

schedule() (旧dispatch())で行っている処理は

static void schedule()
{
  int i;
  for (i = 0; i < PRI_NUM; i++) {
    if (readyque[i]) break;
  }
  if (i == PRI_NUM) {
    /* 実行可能なスレッドが存在しないので,終了する */
    exit(0);
  }
  current = readyque[i];
}
となっている.優先度キューから最も優先度の高いスレッドを検索し, current に設定して返している. よって,ここで行っているのは「スケジューリング」だ.

ちなみにディスパッチを行っている部分は, thread_intrvec() から schedule() を呼び出してスケジューリングした後の

static void thread_intrvec(int signo)
{
  ...
  setcontext(¤t->context.uap);
}
という,setcontext() している部分だ.KOZOSではスレッドのディスパッチ処理は setcontext() にお任せしてしまっているので,この1行で済んでしまっているが, 実際にはレジスタの設定,スタックの設定,新しいコンテキストへの一斉切替え (この処理は一気に行う必要があるので,通常は割り込みからの復帰用命令など, 特殊な命令が用いられる)などの難しい処理が行われる. もちろんアセンブラで記述する必要がある.

このようにディスパッチ処理は,「選択したそのスレッドの動作を開始すること」 なのだけど,人によっては 「スレッドに処理を渡す」とか「スレッドにCPUを渡す」とか 単に「ディスパッチする」とか言ったりする. で,なにも知らないと「CPUを渡す」とか言われてもいったい何をするんじゃいと 思ってしまいがちなのだが,ディスパッチをするということなので注意してほしい.

前にも書いたけど,OSの仕事は 「共通の資源を各スレッドに公平に(均等に,ではない)割り当てること」だ. なので,CPUの実行時間も資源のひとつであり,OSによって管理されているという 視点からだと,「CPUを渡す」という言い方はとっても的を得ているといえるのだ.

あと優先度についてなのだけど,このへんもあんまりちゃんと説明しなかったの だけど,一般に優先度は数値が低いほど,優先度が高い. KOZOSもそーいう作りになっていて,優先度がゼロだと,もっとも優先度が高い. ここでもう一度 schedule() を見てみよう.

static void schedule()
{
  int i;
  for (i = 0; i < PRI_NUM; i++) {
    if (readyque[i]) break;
  }
  if (i == PRI_NUM) {
    /* 実行可能なスレッドが存在しないので,終了する */
    exit(0);
  }
  current = readyque[i];
}
for文で優先度キューの検索をする際に,readyque[0]から順に検索している. つまり,優先度ゼロがもっとも先に検索されるので,もっとも優先度が高い, ということになる.

で,何が言いたいかというと, 「優先度が高い」と言うと「優先度の数値が小さい」という意味になるが, 「優先度が大きい」と言うと「優先度の数値が大きい」つまり「優先度が低い」 という意味になるということだ.よーするに優先度が「高い」と「大きい」とでは 全然意味は違ってきてしまい,こーいうのはおもいっきり誤解の原因になる. なので,読む側で十分に注意して読まなければならない,ということだ.

人によっては「優先度」といったときにその数値(つまり,大きいか小さいか)を 表わし,「プライオリティ」といったときにはその優先順位(つまり,高いか低いか) を表わしたりすることもある.区別するために別途「優先順位」という言葉を 使ったりすることもある.でもきちんと区別して,言葉を (その人なりに統一感を持って)選んで使ってくれる人もいれば, まあ適当に統一感無く言葉を使う人ももちろんいる.

このへんはきっと言葉の定義としてはきちんとしたものがあるとは思うのだが, 説明する人がすべてきちんと言葉を使い分けてくれるということも無いし, 人によっても違うので,結局は文脈を見て自分できちんと判断するしかないとおもう.

あと,優先度ベースの動作を見るために main.c を修正.

diff -ruN kozos23/main.c kozos24/main.c
--- kozos23/main.c	Sun Nov 25 19:50:10 2007
+++ kozos24/main.c	Sun Nov 25 21:13:05 2007
@@ -3,15 +3,60 @@
 
 #include "kozos.h"
 
+static char *outnum[] = { "ABCDEFGHIJ", "abcdefghij", "0123456789" };
+
+static int sample_main(int argc, char *argv[])
+{
+  int i, count;
+  char *p;
+
+#define INTERVAL 7 /* CPU能力に応じて調整してください */
+  while (1) {
+    count = 0;
+    kz_timer(INTERVAL - argc);
+    kz_recv(NULL, NULL);
+    p = kz_memalloc(128);
+    p[0] = '[';
+    p[1] = '0' + argc;
+    p[2] = ']';
+    p[3] = '\0';
+    kz_send(outlog_id, 0, p);
+    for (i = 0; i < 70; i++) {
+      p = kz_memalloc(128);
+      p[0] = outnum[argc][(count++) % 10];
+      p[1] = '\0';
+      kz_send(outlog_id, 0, p);
+    }
+    p = kz_memalloc(128);
+    p[0] = '\n';
+    p[1] = '\0';
+    kz_send(outlog_id, 0, p);
+  }
+  return 0;
+}
+
 int mainfunc(int argc, char *argv[])
 {
+  int sample1_id;
+  int sample2_id;
+  int sample3_id;
+
   extintr_id  = kz_run(extintr_main,  "extintr",   1, 0, NULL);
+#if 0
   stubd_id    = kz_run(stubd_main,    "stubd",     2, 0, NULL);
+#endif
   outlog_id   = kz_run(outlog_main,   "outlog",    3, 0, NULL);
   idle_id     = kz_run(idle_main,     "idle",     31, 0, NULL);
+#if 0
   clock_id    = kz_run(clock_main,    "clock",     7, 0, NULL);
   telnetd_id  = kz_run(telnetd_main,  "telnetd",   8, 0, NULL);
   httpd_id    = kz_run(httpd_main,    "httpd",     9, 0, NULL);
+#endif
+  httpd_id    = kz_run(httpd_main,    "httpd",     9, 0, NULL);
+
+  sample1_id  = kz_run(sample_main,   "sample1",  10, 0, NULL);
+  sample2_id  = kz_run(sample_main,   "sample2",  11, 1, NULL);
+  sample3_id  = kz_run(sample_main,   "sample3",  12, 2, NULL);
 
   return 0;
 }
とりあえず telnet とか http とかは停止する. 起動時にgdbで接続しなきゃならんのも面倒なので,stubd も停止する.

sample_main()は,タイマでてきとうに待ちながら文字を繰り返し出力する関数だ. これを sample1, sample2, sample3 という3つのスレッドとして起動する. たとえば sample1 は

[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
という出力をタイマでてきとうに待ちながら行う. A〜Jまでの文字を繰り返し70個表示し, タイマでしばらく待って,また再度表示する,という動作をするわけだ. sample2 は
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij
という出力になる.こちらはa〜jの小文字を繰り返し70個出力する. あと先頭の[0]が[1]になっているのに注意. さらに,sample3は
[2]0123456789012345678901234567890123456789012345678901234567890123456789
という出力を行う.こちらは0〜9の数字になる.

出力は,for文でぐるぐる回しながら1文字ずつ出力していく. つまり,出力中(forでまわっている途中)にもっと優先度の高いスレッドのタイマが 起動すると,そちらに動作が切り替わることになる.そーいう,優先度ベースの 動作を見るのが今回の目的だ.

ここで各スレッドの優先度だが,mainfunc()での kz_run() によるスレッドの 起動部分では

となっている.sample1が最も優先度が高く(=優先度の値が大きく) なっていることに注意.

ちなみに main.c には

#define INTERVAL 7 /* CPU能力に応じて調整してください */
という部分があるが,これによってタイマによる待ち時間が微妙に変わるので, 以下で説明する「プリエンプティブなスレッド切替え」が発生しやすくなるように, CPU能力に応じて値をてきとうに調整していろいろ試してみてほしい. ぼくの環境はここで書いた通り (Core2Duo E4300)だ. 高速なCPUのばあいには,INTERVALを少なくすると 「プリエンプティブなスレッド切替え」が起きやすくなるようだ.

あと第14回で説明したように,KOZOSにはディスパッチの直前のスキマに たまたまタイマ割り込みが発生すると,以後タイマが起動しなくなるという バグがまだ残っていて,残念ながら未だ解決していない.なので, やっていてもしも固まるようなことがあったらそれはそーいうバグのせいなので, まあINTERVALの値を調整していろいろ試してみてほしい (INTERVALを小さくすると「プリエンプティブなスレッド切替え」は起きやすくなるが, バグも発生しやすくなるので,てきとうに値を変えて繰り返しいろいろ試して みてほしい).

で,以下が実行結果.

...
[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij
[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij
[2]01234567890123456789012345[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij
678901234567890123[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij
45678901234567890123[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij
456789
[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij
[2]01234567890123456789012345678901234567890123456789[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij
01234567890123456789
[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij
[2]01234567890123456789012345678901234567890123456[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij
78901234567890123456789
[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij
...
文字列が
[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij
[2]0123456789012345678901234567890123456789012345678901234567890123456789
...
のように規則正しく出力されるだけではなく,なんか不規則にバラバラ表示されて しまっている.こーいうのがうまく発生すれば成功だ.逆にいうと,ずーーーっと 規則正しく表示されてしまったらそれは失敗. INTERVALをてきとうに調整して,再度試してみてほしい.

で,解説なのだが,たとえば出力結果の最初のほうに

[2]01234567890123456789012345[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij
678901234567890123[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij
45678901234567890123[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij
456789
となっている行がある.これは何が起きてこんなんなっているのかというと, スレッドのディスパッチごとに改行を入れると
[2]01234567890123456789012345
[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij
678901234567890123
[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij
45678901234567890123
[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij
456789
という10個の行に分割できる.これを1行ずつ (つまり,スレッドのディスパッチごとに)説明していこう.

まず1行目の

[2]01234567890123456789012345
だが,これは先頭が[2]になっているのと,表示内容が0〜9の文字なので, sample3 の出力だとわかる. これは sample_main() 内部の for 文によって1文字ずつ出力されているのだが, 途中で sample1 のタイマが起動したらしく,[0]が表示され,以降は sample1 の 表示になっている.それが2行目の
[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
という部分だ. sample1 はぜんぶの文字(70個)を一気に表示している. sample3よりもsample1のほうが優先度が高いので,sample3の動作中でも, sample1が割り込んで動作を開始している.

こーいうように,たとえあるスレッドの動作中でも, 割り込みを契機にしてもっと優先度の高いスレッドが動作可能 (この場合には,タイマ割り込みにより sample1 スレッドにメッセージが送られた ため,sample1 がレディーキューに繋がれ,動作可能になった. sample1 は kz_recv() でメッセージの受信待ち状態なので,メッセージが送られた ことで動作可能になったわけだ)になった場合に, そっちに動作を切り替えることを「プリエンプティブ」という.日本語では 「先取り可能」というが,まあ「プリエンプティブ」といったほうが通じる. で,そーでないのは「ノン・プリエンプティブ」という. 「KOZOSはスケジューリング方式が優先度ベースになっていて, プリエンプティブな動作をする」というような言いかたをする.

説明を続けよう.sample1の表示処理(70文字)がすべて終った後には

[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij
のように表示されている(3行目).先頭は[1]なので,sample2が動作しているようだ.

sample1 が動作可能になったために,sample3 は動作を待たされている. このため sample1 が表示処理を終えたならば,sample3 が動作を再開しそうなものだ. しかし sample1 の動作終了直後に sample2 が動き出しているということは, どうも sample1 の動作中に sample2 のタイマが起動して,sample2 が既に動作可能に なっていたようだ.

先ほどは,sample3 の動作中に sample1 が割り込んで動作を開始した. しかし今度は,sample1 の動作中には sample2 は割り込まず, sample1 の終了まで待たされている. これはなぜか? sample2 のほうが,sample1 よりも優先度が低いからだ. このためたとえ sample2 が動作可能になっても (タイマが起動して,レディーキューに繋がれても), もっと優先度の高い sample1 が動作中であるため, sample2 は動作を開始しないのだ.

で,sample2 が動作を開始して70文字を表示しきった後には

678901234567890123
が表示されている(4行目).sample1 も sample2 も動作を停止したため, sample3 がよーやく動作可能になっているわけだ. つまり,sample3 は sample1 と sample2 の動作中, ずっと待たされていたことになる.前回(1行目)は「5」まで表示されていたところで 処理が中断されていたので,今度は「6」の表示から再開している点に注目してほしい.

で,sample3が最後まで表示する...といきたいところなのだけど, ここで再び sample1 のタイマが起動しているようだ(5行目).

[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
sample3 はまた割り込まれ,sample1 が処理を行った.さらにその後に
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij
となっている(6行目)ので,sample1 の処理中に sample2 のタイマが起動し, sample2 が処理を行っている.

で,再び

45678901234567890123
のようにして(7行目),sample3 が処理を再開している.

sample3 の「待たされ」「割り込まれ」はまだまだ続く. さらに同様に sample1, sample2 がタイマ起動し

[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij
が表示され(8,9行目),その後ようやく
456789
が表示された(10行目).sample3の表示はここまででようやく70文字になるので, ひととおり表示しきれたことになる.

ほかにも,INTERVALを5と低めにして試してみたら, 以下のような出力が得られた.

[2]0123456789012345678901234567890123456789012345[1]abcdefghijabcdefghijabcdef[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
ghijabcdefghijabcdefghijabcdefghijabcdefghij
6789012345678901234[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij
56789
これはスレッドのディスパッチごとに改行を入れると,以下のようになる.
[2]0123456789012345678901234567890123456789012345
[1]abcdefghijabcdefghijabcdef
[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
ghijabcdefghijabcdefghijabcdefghijabcdefghij
6789012345678901234
[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij
56789
初めは sample3 が動作している(1行目)のだが,途中で sample2 がタイマ起動した らしく,sample2 が割り込んで動作を開始する(2行目).しかしさらに sample1 が タイマ起動したようで,sample1 が割り込んで動作を開始する(3行目). sample1 はこの3つのスレッドの中で最も優先度が高いので,sample2, sample3 から 割り込まれることはなく,最後まで表示を行っている(3行目で,70文字ぜんぶが 表示されている).sample1 の表示が終った後には,待たされていたうちの 優先度の高いほうである sample2 が表示を再開している(4行目). で,sample2 が70文字を表示しきると,sample3 の動作が再開している(5行目). しかし sample3 は sample1, sample2 に再度割り込まれ(6行目,7行目), その後ようやく70文字の表示処理を完了している(8行目).

これからわかるように,sample2 の表示は

[1]abcdefghijabcdefghijabcdef
[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
ghijabcdefghijabcdefghijabcdefghijabcdefghij
のように中断されることがありうる.sample3 になると,もう頻繁に中断される. これに対して sample1 は他の sample2, sample3 から割り込まれることは無い. よって
[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
の表示は,絶対に1行まとまって表示され,中断されることは無い. こーいうのを「走り切る」とか表現することがある. 「sample1 は sample2, sample3 には邪魔されずに最後まで走り切る」 といった感じの言いかただ. まあ実際には outlog スレッド(優先度は3)が表示のために割り込んで動作している ので,outlog スレッドのことまで考えるならば「走り切っているわけではない」 のだが,仲間である sample1, sample2, sample3 の中で考えるならば, sample1 は常に走り切ることになる. 走り切ると言うのがどこからどこまでかというと, システムコール(kz_recv()による割り込み検知)から 次のシステムコール(kz_recv()によるタイマ起動待ち)までだ. この「走り切る」というのは,今後説明する予定の「排他」とか「再入」とかを 考える上で,とても重要なポイントになる.

さて,今回試してみて思ったことは,sample3 がやたら待たされていると言うことだ. sample1 や sample2 が我がもの顔で割り込んできてしまって, その都度 sample3 は待たされている.ひどい時には,処理が終らないうちに 2度も割り込まれたりしている.こんなに割り込まれてしまって, これでいいのだろうか?

まあ結論から言うと,これでいいということになる. たとえば(ここからはぼくの素人考えなので,実際の自動車はそうなっているという 意味ではなく,説明するための例えのひとつとして聞いてほしいのだが) 自動車で,ワイパーの制御とハンドル制御とエアバック制御がぜんぶひとつの CPUで行われるということを想像してみてほしい. ひとつのCPUで複数のことを行う限り,すべてのことを同時に行うことはできないので, どこかで処理を細かく切替えつつ,3つのことを並行して行うしかない. 全体としては並行して3つの処理が走っているように見えるが,ミクロの視点で 見ると,処理を細かく区切って,切替えつつ行っていることになる.

これは組み込みOSによってスレッド化した設計にするならば, 3つの処理を3つのスレッドにして,てきとうにディスパッチしながら動作する, ということになる. しかしたとえば,ワイパーの制御を行っている最中にハンドルが操作されたら どうすべきか?

ハンドル制御の遅れは,ハンドルが鈍い,うまく効かないということになるから, 安全性に直結する.ていうかそんな車,恐くて乗りたくない.ワイパーの動きなんぞ べつに多少鈍くてもいいので,ハンドルの制御を優先させてほしい. なのでハンドル制御のほうの優先度を高くするべきだ. これを間違って逆にしてしまうと,ワイパーが動いている間はハンドルが鈍くなる (下手したらハンドルが効かない)という,しょーもない車になることになる.

エアバッグ制御はもっと深刻だ. たとえばハンドルの制御を行っている最中に,衝突センサーが衝突を検知したら どうすべきか?

この際に,ハンドルの制御が終るまで待ち,その後エアバッグ制御処理を開始する なんていう動作は論外だ.もう事故が起きてしまっているのだから, もうハンドルが鈍くなるとか気にしている場合ではない. ハンドル制御なんぞよりも,即刻エアバッグ制御を行うべきだ. なので,ハンドル制御スレッドよりも,エアバッグ制御スレッドのほうの優先度を 高くしておかなければならない.

ここで先に説明した sample1, sample2, sample3 の動作で, sample3 はやたら待たされることが多かったが,sample1 は必ず走り切っていた ことを思い出してほしい. sample3 はワイパー制御, sample2 はハンドル制御, sample1 はエアバッグ制御だと思ってみてほしい. ハンドル制御やエアバッグ制御などが起動した場合には, ワイパー制御なぞ待たしてしまって構わない.これに対してエアバッグ制御は もう一刻を争う緊急時なのだから,ほかの処理には割り込まれずに,走り切って くれないと困る(実際には他スレッドのタイマ起動時(SIGALRM発生時)にはKOZOSで タイマ処理が行われるため,sample1 は「まったく割り込まれない」というわけでは ないのだが,まあ説明のためなので気にしないで). でないとなにかほかのことが行われている際にはエアバッグがまともに動作しないと いう,これまた絶っっっ対にそんな車乗りたくない,という自動車になってしまう.

つまり,ハンドル制御はワイパー制御に割り込んで動作して構わない. そしてエアバッグ制御はハンドル制御に割り込んで動作して構わない. これが,プリエンプティブということだ. そして各スレッドがプリエンプティブに動作できるかどうかはOSに依存する. KOZOSは優先度ベースのプリエンプティブな設計をしてあるので, 各スレッドはそーいうふうに動作できる.もちろんそうでなく, ノン・プリエンプティブなOSも存在する.そういうOSの上で今回の sample1, sample2, sample3 を動かした場合には,全然違った動きを することになるだろう.

だいたい基本的に,

という法則がある.ワイパー制御,ハンドル制御,エアバッグ制御を上の法則に 照らし合わせて考えてみてほしい.

ところがまあ実際にはこれでは困る. というのは,こんな複雑な制御を行っていては, バグの入り込む可能性が格段に上がる. OSにもバグがある可能性があるし,そしてそのバグはきっとタイミングによって 起きたり起きなかったりすることがあるため,テストで見つけにくかったりする.

たとえばOSの何らかのバグで,ハンドル制御とエアバッグ制御の優先度が逆転して しまったとしよう. これは実際には,CPUがハンドル制御を定期的に行うスキマでエアバッグ処理が 行われる,ということになる.よってエアバッグはまったく効かない, というわけではないのだが,

  1. ハンドル制御中に衝突が検知されても,エアバッグ展開処理が待たされる. (展開タイミングは遅れるが展開自体は行われるので,展開時間を測らないと 見つけられず,これはこれで見つけにくいバグになる)
  2. エアバッグ展開処理中にハンドルが操作されると,エアバッグ展開処理中に 別処理によってウエイトが入る. もしも「エアバッグ展開処理は『待たされること無く走り切る』」という前提で エアバッグ部品が設計されているならば,エアバッグがまったく*展開しない* という致命的な問題になり得る.
2番目の問題にとくに注意してほしい.たとえば(このへんはもう,完全にぼくの 想像というか,説明しやすくするための勝手な空想なのだが) エアバッグ部品が,ある信号を入れた後にxxマイクロ秒以内に別の信号を入れると 起動する(遅れて信号を入れられても起動しない),というものだったとしよう. エアバッグ展開処理が走り切るならば,そーいうタイミングで信号を入れられる だろうから,エアバッグはふつうに展開する. しかし上記2番目の問題のように,そのわずかな信号のスキマにハンドル制御処理が 発生すると,ハンドル制御処理によるウエイトのために信号のタイミングがずれて (遅れて)しまうので,エアバッグは展開しない. そしてこれはいくらテストをしても,タイミングによっては起きたり起きなかったり するので,テストでは見つけにくい. まあ万が一自動車でこんなバグが見つかったら,即リコールだろう.

もっとも上でもちょっと書いたように,実際には sample2, sample3 のタイマ起動時 にはたとえ sample1 の動作中であってもSIGALRMによる割り込みが発生し, KOZOSでタイマ処理が行われるため,sample1 は「まったく割り込まれない」 「(上述のようなOSのバグが無いならば)タイミングが遅れることは絶対に無い」 というわけではないのだが,まあ説明のためなので, とりあえずは気にしないでほしい. このため本当は,割り込み自体にも優先度が無ければならない. もしくはタイマ割り込み処理に関して,リアルタイム性を確保しなければならない のだが,まあこのへんはそのうち説明する.

こーいうように,優先度は注意しないと致命的な(そしてきっとタイミングに依存 するので,テストでは見つけにくい)問題になり得る. さらにこのような問題をテストで検出しようとするならば, ハンドル操作中のエアバッグ動作とか,その逆とかのテストが必須になる. タイミング依存が考えられるので,何度も繰り返しテストしたり, いろんなタイミングを疑似的に発生させてタイミングパターンを網羅するような テストも必要だ.

ハンドル制御もエアバッグ制御も安全性に直結する機能なので, バグが入り込む可能性は極力,というか確実に排除しなければならない. こーいうときにはソフトウエアを過信してはいけない. よーするに, 「きちんとテストをすればバグは無くせるはずだ」 「バグが無くなるまできちんとテストすべきなのだから,出荷版にはバグは無いはず (そうなるまでテストをしているはず)だ」 という考え方は禁物だ.「それでもバグは発生するかもしれない」というように, 常にバグの先を行くような考え方でならないといけない.

ということで,実際にはこのような場合には, 3つのCPUを別々に積んで,それぞれを独立して制御するというのが本当は正解だ. ここで1つのCPUで処理する,というように説明したのは, 優先度というものを理解しやすくするための,あくまでたとえばの話なので注意 してほしい. 聞いたところでは,今どきの自動車は40個くらいCPUを積んでいるものらしい. まあ上述したように,複雑さは安全性の問題になり得ることを考えると, これは当然のこととも言える.

えー,今回はここまで. スレッドの優先度とプリエンプティブに関しては, 図とかを使って理論的に詳しく説明してある資料はいっぱいある. TECH-I の,高田大先生の書いたOS関連の本とかを参考にしてほしい. ただ,こんなふうに実際に動かしてみてどんなふうになっているのかというのを 説明しているような資料はあまり見かけない. ということで,今回は実際にプリエンプティブな動作をさせてみて説明してみた. まあもともとKOZOSはこーいうふうに,組み込みOSの動作を手元で手軽にパパッと 試せるというのがウリなので,今後もこんな感じで説明をしてみたい.

最後に,以下に参考としてINTERVALを5にしたときの実行結果を ちょい長めに添付しておく. スレッドのディスパッチがどんなふうに行われているのか, 必要ならばよく読んで確認してみてほしい.

[2]0123456789012345678901234567890123456789012345678901234567890123456789
[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij
[2]0123456789012345678901234567890123456789012345678901[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijab[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
cdefghij
234567890123456789
[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcd[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
efghijabcdefghij
[2]012345678901234567890123456789012345[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghija[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
bcdefghij
6789012345678901234567890123456789
[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcde[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
fghijabcdefghij
[2]0123456789012345678901234567890123456[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefg[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
hijabcdefghij
789012345678901234567890123456789
[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijab[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
cdefghijabcdefghij
[2]01234567890123456789012345678901234[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghija[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
bcdefghij
56789012345678901234567890123456789
[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcde[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
fghijabcdefghij
[2]012345678901234567890123456789012345[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghija[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
bcdefghij
6789012345678901234567890123456789
[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabc[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
defghij
[2]0123456789012345678901234567890123456789012345[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghija[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
bcdefghij
678901234567890123456789
[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcd[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
efghijabcdefghij
[2]012345678901234567890123456789012345[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijab[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
cdefghij
6789012345678901234567890123456789
[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcde[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
fghijabcdefghij
[2]01234567890123456789012345678901234567[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabc[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
defghijabcdefghij
89012345678901234567890123456789
[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcd[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
efghij
[2]0123456789012345678901234567890123456789012345678901234567890123456789
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefg[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
hij
[2]0123456789012345678901234567890123456789012345678901234567890123456789
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij
[2]0123456789012345678901234567890123456789012345678901234567890123456789
[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij
[2]0123456789012345678901234567890123456789012345678901234567890123456789
[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij
[2]0123456789012345678901234567890123456789012345678901234567890123456789
[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
[2]01234567890123456789012345678901234567890123456789012345678901[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij
23456789
[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
[2]0123456789012345678901234567890123456789012345678901234567890123456789
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij
[2]0123456789012345678901234567890123456789012345678901234567890123[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij
456789
[2]0123456789012345678901234567890123456789012345678901234567890123456789
[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij
...

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