(注意)このブログは本家のほうの文章部分のみの転載です.ソースコードの配布,画像などについては本家のほうを参照してください.文章中のリンク先は面倒なのですべて本家のほうに変換してしまっているのでご注意ください.
えーと,前回はKOZOSの動作の流れについて説明しただけでサンプルのアプリの動作説明をしてませんでした.ということで今回は,サンプルアプリの説明.
まず,前回紹介した main1.c をもう一度.こちらは実行結果.
では,main1.c の動作を順に説明しよう.
まず,C言語の常識に違わず main() が実行されるのだけど,kz_start() によっていきなり KOZOS が起動され,スレッド "main" が生成される.スレッド "main" は,mainfunc() をメイン関数として実行開始する.
で,mainfunc() はこんなふうになっている.
最初に 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() の先頭と実行結果を見てほしい.
func1(), func2() とも,先頭で fprintf() によりメッセージを出力しているのに,実際の実行結果では mainfunc() 内部のメッセージ出力が行われている.つまり,スレッド "func1","func2" は実際には動作開始せず,スレッド "main" がそのまま動作していることがわかる.
スレッド "main" はさらに for ループに入り,メッセージを出力する.
実行結果をもう一度,今度は全体を見てみよう.
mainfunc() のループが終了した後で,ようやく func1(),func2() 先頭のメッセージ出力が行われている.mainfunc()のループ内では kz_wait() によるディスパッチが行われているのだが,結局のところスレッド "main" の優先度が一番高いので,"main" が動作している限りはカレントスレッドは "main" のままとなり,"main" が終了してからはじめて "func1","func2" が動き出していることになる.
func1()とfunc2()ではどちらもループしながらメッセージ出力しているが,実際にはメッセージは交互に出力されている.これはどちらもループ内でkz_wait() により処理を相手に渡しあっているからだ.
このように,アプリの動作は優先度に左右される.この「優先度」というのがイマイチピンとこないひとも多いと思うし,それってそんなに重要なの? といったように思うひとも多いと思うのだが,すっげー重要です.この優先度の設計によって,動作ががらりと変わったり,おかしなバグが出たり解決したり,排他の問題が出たりやんだり,リアルタイム性があったりなくなったりという,とにかく組み込みOSのスレッド設計の根幹となると言ってもいいくらいのものです.
優先度を理解するために,ちょっと優先度を変更して実行してみましょう.main1.c の kz_start() 実行時の優先度を1→3に変更してみます.
上記の main2.c を main.c にリネームして make することで,スレッド "main" の優先度を1→3に変更した実行形式を作成できます.で,実行してみます.
どうです? ぜんぜん違うでしょ?
注目すべきは,スレッド "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" の優先度を変えてみましょう.
スレッド "main" は優先度1,スレッド "func1" は優先度2,スレッド "func2" は優先度3になっています.まずスレッド "main" が走りきり,次にスレッド "func1",最後にスレッド "func2" が走っていますね.
ここまではスレッドの優先順位による単純な処理でしたが,main2.c に対して,スレッドのスリープと再起動(wakeup)を入れてみましょう.
実行結果は次のようになります.
さて,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の動作の流れについて説明しただけでサンプルのアプリの動作説明をしてませんでした.ということで今回は,サンプルアプリの説明.
まず,前回紹介した 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 の位置を変えたりしていろいろ試してみてください.数値をひとつ変えるだけで,こんなにも動作が変わるものなのかということが実感できると思います.