(第2回)サンプルプログラムの動作

2007/10/19

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

えーと,前回はKOZOSの動作の流れについて説明しただけで サンプルのアプリの動作説明をしてませんでした. ということで今回は,サンプルアプリの説明.

まず,前回紹介した main1.c をもう一度.

こちらは実行結果.
% koz
main start
thread 1 started
thread 2 started
mainfunc loop 0
mainfunc loop 1
mainfunc end
func1 start 1 ./koz
func1 loop 0
func2 start 1 ./koz
func2 loop 0
func1 loop 1
func2 loop 1
func1 end
func2 end
% 
では,main1.c の動作を順に説明しよう.

まず,C言語の常識に違わず main() が実行されるのだけど, kz_start() によっていきなり KOZOS が起動され, スレッド "main" が生成される. スレッド "main" は,mainfunc() をメイン関数として実行開始する.

int main(int argc, char *argv[])
{
  kz_start(mainfunc, "main", 1, argc, argv);
  return 0;
}
で,mainfunc() はこんなふうになっている.
int mainfunc(int argc, char *argv[])
{
  int i;
  int id1, id2;

  fprintf(stderr, "main start\n");

  id1 = kz_run(func1, "func1", 2, argc, argv);
  fprintf(stderr, "thread 1 started\n");

  id2 = kz_run(func2, "func2", 2, argc, argv);
  fprintf(stderr, "thread 2 started\n");

  for (i = 0; i < 2; i++) {
    fprintf(stderr, "mainfunc loop %d\n", i);
    kz_wait();
  }

  fprintf(stderr, "mainfunc end\n");

  return 0;
}
最初に kz_run() によって "func1" スレッドが生成される.これは func1() を メイン関数として実行開始する.で,kz_run() の呼び出し後にはどうなるかというと, なんだか func1() がそのまま呼ばれそうな気がするのだけど, ここで注意しなければならないのはスレッドの優先度だ.

kz_start() でスレッド "main" を起動したときの優先度(kz_start()の第3引数) は「1」となっている. これに対して,kz_run()によってスレッド "func1" を起動したときの優先度 (kz_run)の第3引数)は「2」だ.つまり kz_run() 実行後のディスパッチ処理によって カレントスレッドになるのは,優先度の高い(=優先度の数値の小さい) スレッド "main" ということになる.なので,mainfunc() 内の処理がそのまま 継続される.

mainfunc()で次に行われるのは,kz_run() によるスレッド "func2" の生成だ. これに関しても,優先度「2」で生成されるので, スレッド "main" はスレッド "func2" を生成したままさらに処理を進めることになる.

ここで,func1(),func2() の先頭と実行結果を見てほしい.

int func1(int argc, char *argv[])
{
  int i;

  fprintf(stderr, "func1 start %d %s\n", argc, argv[0]);
  ...
int func2(int argc, char *argv[])
{
  int i;

  fprintf(stderr, "func2 start %d %s\n", argc, argv[0]);
  ...
% koz
main start
thread 1 started
thread 2 started
mainfunc loop 0
...
func1(), func2() とも,先頭で fprintf() によりメッセージを出力しているのに, 実際の実行結果では mainfunc() 内部のメッセージ出力が行われている. つまり,スレッド "func1","func2" は実際には動作開始せず,スレッド "main" が そのまま動作していることがわかる.

スレッド "main" はさらに for ループに入り,メッセージを出力する.

  for (i = 0; i < 2; i++) {
    fprintf(stderr, "mainfunc loop %d\n", i);
    kz_wait();
  }

  fprintf(stderr, "mainfunc end\n");
実行結果をもう一度,今度は全体を見てみよう.
% koz
main start
thread 1 started
thread 2 started
mainfunc loop 0
mainfunc loop 1
mainfunc end
func1 start 1 ./koz
func1 loop 0
func2 start 1 ./koz
func2 loop 0
func1 loop 1
func2 loop 1
func1 end
func2 end
% 
mainfunc() のループが終了した後で,ようやく func1(),func2() 先頭の メッセージ出力が行われている.mainfunc()のループ内では kz_wait() による ディスパッチが行われているのだが,結局のところスレッド "main" の優先度が一番 高いので,"main" が動作している限りはカレントスレッドは "main" のままとなり, "main" が終了してからはじめて "func1","func2" が動き出していることになる.

func1()とfunc2()ではどちらもループしながらメッセージ出力しているが, 実際にはメッセージは交互に出力されている.これはどちらもループ内で kz_wait() により処理を相手に渡しあっているからだ.

int func1(int argc, char *argv[])
{
  int i;

  fprintf(stderr, "func1 start %d %s\n", argc, argv[0]);

  for (i = 0; i < 2; i++) {
    fprintf(stderr, "func1 loop %d\n", i);
    kz_wait();
  }

  fprintf(stderr, "func1 end\n");

  return 0;
}
int func2(int argc, char *argv[])
{
  int i;

  fprintf(stderr, "func2 start %d %s\n", argc, argv[0]);

  for (i = 0; i < 2; i++) {
    fprintf(stderr, "func2 loop %d\n", i);
    kz_wait();
  }

  fprintf(stderr, "func2 end\n");

  return 0;
}
このように,アプリの動作は優先度に左右される. この「優先度」というのがイマイチピンとこないひとも多いと思うし, それってそんなに重要なの? といったように思うひとも多いと思うのだが, すっげー重要です.この優先度の設計によって, 動作ががらりと変わったり,おかしなバグが出たり解決したり, 排他の問題が出たりやんだり,リアルタイム性があったりなくなったりという, とにかく組み込みOSのスレッド設計の根幹となると言ってもいいくらいのものです.

優先度を理解するために,ちょっと優先度を変更して実行してみましょう. main1.c の kz_start() 実行時の優先度を1→3に変更してみます.

--- main1.c     Fri Oct 19 00:49:30 2007
+++ main2.c     Fri Oct 19 00:49:36 2007
@@ -60,6 +60,6 @@
 
 int main(int argc, char *argv[])
 {
-  kz_start(mainfunc, "main", 1, argc, argv);
+  kz_start(mainfunc, "main", 3, argc, argv);
   return 0;
 }
上記の main2.c を main.c にリネームして make することで, スレッド "main" の優先度を1→3に変更した実行形式を作成できます. で,実行してみます.
% koz
main start
func1 start 1 ./koz
func1 loop 0
func1 loop 1
func1 end
thread 1 started
func2 start 1 ./koz
func2 loop 0
func2 loop 1
func2 end
thread 2 started
mainfunc loop 0
mainfunc loop 1
mainfunc end
% 
どうです? ぜんぜん違うでしょ?

注目すべきは,スレッド "func1" がまっさきに動作してループ処理して終了して しまっている点です.スレッド "main" の優先度は3ですが,スレッド "func1" の 優先度は2です.なので kz_run() によりスレッド "func1" が生成されると, その後のディスパッチによってスレッド "func1" が動き出してしまいます. この間,スレッド "main" の動作は待たされます. func1() のループの内部では kz_wait() によるディスパッチが行われますが, スレッド "main" が待たされているためにスレッド "func2" がまだ起動していません. よってスレッド "func1" がそのまま動き続け,終了します. スレッド "func1" が終了すると,ようやくスレッド "main" が動き出します. ここで,ようやく「thread 1 started」という,スレッド "func1" を開始した旨の メッセージが出力されます.さらに kz_run() により今度はスレッド "func2" が 起動されるのですが,スレッド "func1" と同様に優先度がスレッド "main" よりも 高いので,そのままループに入り,終了するまで突き進みます. スレッド "func2" も終了した後に,ようやくスレッド "main" がループ処理を進めて 終了することになります.

今度は,main1.c に対してスレッド "func2" の優先度を変えてみましょう.

--- main1.c     Fri Oct 19 00:49:30 2007
+++ main3.c     Fri Oct 19 00:49:43 2007
@@ -45,7 +45,7 @@
   id1 = kz_run(func1, "func1", 2, argc, argv);
   fprintf(stderr, "thread 1 started\n");
 
-  id2 = kz_run(func2, "func2", 2, argc, argv);
+  id2 = kz_run(func2, "func2", 3, argc, argv);
   fprintf(stderr, "thread 2 started\n");
 
   for (i = 0; i < 2; i++) {
% koz
main start
thread 1 started
thread 2 started
mainfunc loop 0
mainfunc loop 1
mainfunc end
func1 start 1 ./koz
func1 loop 0
func1 loop 1
func1 end
func2 start 1 ./koz
func2 loop 0
func2 loop 1
func2 end
% 
スレッド "main" は優先度1, スレッド "func1" は優先度2, スレッド "func2" は優先度3になっています. まずスレッド "main" が走りきり, 次にスレッド "func1",最後にスレッド "func2" が走っていますね.

ここまではスレッドの優先順位による単純な処理でしたが, main2.c に対して,スレッドのスリープと再起動(wakeup)を入れてみましょう.

--- main2.c     Fri Oct 19 00:49:36 2007
+++ main4.c     Fri Oct 19 00:49:56 2007
@@ -8,6 +8,8 @@
   int i;
 
   fprintf(stderr, "func1 start %d %s\n", argc, argv[0]);
+  kz_sleep();
+  fprintf(stderr, "func1 wakeup\n");
 
   for (i = 0; i < 2; i++) {
     fprintf(stderr, "func1 loop %d\n", i);
@@ -52,6 +54,9 @@
     fprintf(stderr, "mainfunc loop %d\n", i);
     kz_wait();
   }
+
+  fprintf(stderr, "thread 1 wakeup\n");
+  kz_wakeup(id1);
 
   fprintf(stderr, "mainfunc end\n");
 
実行結果は次のようになります.
% koz
main start
func1 start 1 ./koz
thread 1 started
func2 start 1 ./koz
func2 loop 0
func2 loop 1
func2 end
thread 2 started
mainfunc loop 0
mainfunc loop 1
thread 1 wakeup
func1 wakeup
func1 loop 0
func1 loop 1
func1 end
mainfunc end
% 
さて,main2.c の実行結果に対してどのように変化しているでしょうか? main2.c ではスレッド "main" の優先度が最も低かったために スレッド "func1" が生成されるとまずは "func1" が走りきり, 次にスレッド "func2" が生成されると今度は "func2" が走りきり, 最後に "main" が走り終っていました.

しかし今回の結果では,同様に "func1" が走ろうとしますが, 起動直後にループの手前で "func2" に切り替わっています. これは func1() の先頭で kz_sleep() によりスレッドがスリープしているためです. スレッド "func1" はスリープしてしまうために今度は "func2" が動作を始め, 止まる部分が無いためにそのまま終了まで突き進んでいます.

"func2" が終了した後にはどうなるでしょうか? "func1" は相変わらずスリープ中 であるために動作できません.よって優先度は低いのですが,"main" が動作を 始めます."main" は mainfunc() 内部でのループ処理を行った後, kz_wakeup() によってスレッド "func1" を起こします. これによりスレッド "func1" が動作を再開し,優先度の高い "func1" のほうが, 今度は終了まで突き進みます. "func1" の終了後は再び "main" が動作を始め,終了しています.

優先度をいろいろ変えたり,sleep と wakeup の位置を変えたりしていろいろ 試してみてください.数値をひとつ変えるだけで,こんなにも動作が変わるもの なのかということが実感できると思います.


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