(注意)このブログは本家のほうの文章部分のみの転載です.ソースコードの配布,画像などについては本家のほうを参照してください.文章中のリンク先は面倒なのですべて本家のほうに変換してしまっているのでご注意ください.

KOZOSとは?でも書いたように,KOZOSはFreeBSD上で動作します.

しかし,まあぼくみたいに普段から FreeBSD をメインOSとして利用しているならばそれでもいいのだけど,KOZOSを試してみるためにわざわざFreeBSDをインストールしなければならないというのは,やっぱり面倒です.ここでは他OS上で動かす方法についてちょっと説明します.

FreeBSD以外のOS上で動かそうとするならば,いくつか選択肢があると思います.
  • Windows で cygwin 上で動かす.
  • Linux 上で動かす.
  • VMwareなどのエミュレータを利用して,FreeBSD環境を仮想的に作る.
まず cygwin についてですが,見てみたところ setjmp()/longjmp() は利用できるようですが,getcontext() は使えないみたいです.なので,第12回以降のソースコードは利用できません.

次に Linux ですが,やはり多少の修正が必要になるようです.とくに setjmp()/longjmp() 利用のコードでは,jmp_buf の構造が違うのでなんか適当に修正しないとダメでしょう.あと getcontext()/setcontext() を使ったソースについてはちょっとぼくのほうでも試してみたのですが,とりあえず第25回のソースに以下の修正を加えることでコンパイルはできました.

Linuxでコンパイルするための修正

が,実行してみても Segmentation Fault で落ちてしまい,まともに動きませんでした.ちょっと詳しくは調べてないのですが,ということで Linux の動作も現状,確認できていません.ちなみに上で試したディストリビューションは,Fedora Core 8 でした.

※ もしも,Linuxで動いたというかたがいればぜひ連絡ください.

で,どうするかなのですが,エミュレータを使うのがとりあえず手軽で確実だと思います.QEMUというフリーのPCエミュレータがあって,Windows などのOS上に仮想PCを作成し,そこに FreeBSD をインストールして動かすことができます.ぼくも使ってみましたが,実行速度さえ気にしないのならば,何の問題もなく確実に動きます.

QEMUについては,以下を参照してください.

で,本来ならばここでインストール済みのHDDイメージを配布しようかなーなんて思ってたのですが,ためしにインストールしてみたら,最小構成でもやっぱしそれなりのサイズになってしまったのであきらめました.まあ FreeBSD のインストールはそんなに難しくないので,各自,やってみてくださいな.

参考までに,Windows 上で QEMU で FreeBSD 環境を作成する手順は以下の通り.FreeBSDのインストール方法に関しては,まあ他にも探せばいっぱい出てくると思うので,てきとうになんか参考にしてほしい.
  1. FreeBSDのインストール用CD-ROMを作成しておく.
  2. Windows にQEMUをインストールする.
  3. 空のHDDイメージを作成する.(DOSプロンプトで実行)

    qemu-img create -f qcow freebsd.qcow 8G

  4. CD-ROMからブートし,HDDにインストールする.(DVDも同様の操作で利用可能)(e:のところには,CD-ROMのドライブ名を指定する)

    qemu -L . -m 256 -boot d -cdrom //./e: -hda freebsd.qcow -localtime

  5. インストールが終了したら,HDDイメージからブートすることでFreeBSDが利用可能.

    qemu -L . -m 256 -hda freebsd.qcow -localtime

ちなみにCD-ROMを利用したい場合には,以下のオプションをつけて起動する.CD-ROMがE:ドライブならば

-cdrom //./e:

あと snapshot モードというのがあって,起動時に -snapshot オプションを指定すると,HDDイメージに書き込まないで動作する.実験用で,常に同じ状態で立ち上げたい場合には便利.

あとQEMUは,Cirrus Logic GD5446ビデオカードをエミュレートするので,Xのドライバに cirrus を指定することでXも利用できる.具体的には,/usr/X11R6/lib/X11/xorg.conf でドライバの指定部分を以下のように書けばよい.

# Driver "i810"
# VendorName "Intel Corporation"
# BoardName "945G Integrated Graphics Controller"
# BusID "PCI:0:2:0"
Driver "cirrus"

あとQEMUでは,最低限,以下の操作を覚えておくといいだろう.
  • マウスカーソルをウィンドウ内に出し入れする ... Ctrl + Alt
  • フルスクリーンで使用する ... Ctrl + Alt + f
  • 終了する ... Ctrl + Alt + 2 でモニタに入って quit する.
ネットワークは,普通に使えば DHCP でアドレス取って,telnet, ftp, ssh などを使って仮想PCから外に繋げることができます(内部で仮想的にDHCPサーバ,DNSサーバ,NATなどが動作しています).ただし仮想PCから外に対して ping は通らないので注意.僕はこれでハマりました...あと外から中に繋げるには別途設定が必要.あとftpはうまく繋がらなかったらPASSIVEモードでやってみること(もしくはsftpを使う).

使ってみて思ったけど,こーいう実験のためには,仮想PCはいろいろと融通が効いて,とっても便利で良いです.実は Linux (Fedora Core 8)上での動作実験も,Windows 上のQEMU環境で行いました.おすすめ!
(注意)このブログは本家のほうの文章部分のみの転載です.ソースコードの配布,画像などについては本家のほうを参照してください.文章中のリンク先は面倒なのですべて本家のほうに変換してしまっているのでご注意ください.

前回は優先度をベースにしたプリエンプティブな動作について試してみたが,今回はノン・プリエンプティブな動作について試してみたい.

ノン・プリエンプティブとはどんな動作かというといわゆる汎用OSがそーいう動きをするのだけど,まあ簡単に言うと前回当たり前のように行われていた「あるタスクの動作中に,もっと優先度の高いタスクが平気で割り込んでくる」という動作が無いものだ.というより,汎用OSではタスクに優先度というものが無く,みんな平等,と思ってくれていい(優先度を設定できる汎用OSもあるが,大抵は変に設定すると即固まったりするので優先度の変更は推奨されていなかったり,裏技のようなものであったりするので,ここではそーいうのは言及しない).

まあ論より証拠なので,汎用OSの詳しい説明は後にしてとりあえずノン・プリエンプティブな動作を見てみよう.ノン・プリエンプティブな動作を見るためには,とりあえず前回のソースでsample1, sample2, sample3 の優先度を全部同じにしてしまうのがてっとりばやい.

diff -ruN kozos24/main.c kozos25_2/main.c
--- kozos24/main.c Sun Dec 2 21:10:47 2007
+++ kozos25_2/main.c Sun Dec 2 21:24:08 2007
@@ -10,7 +10,7 @@
int i, count;
char *p;

-#define INTERVAL 7 /* CPU能力に応じて調整してください */
+#define INTERVAL 5 /* CPU能力に応じて調整してください */
while (1) {
count = 0;
kz_timer(INTERVAL - argc);
@@ -55,8 +55,8 @@
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);
+ sample2_id = kz_run(sample_main, "sample2", 10, 1, NULL);
+ sample3_id = kz_run(sample_main, "sample3", 10, 2, NULL);

return 0;
}

sample1, sample2, sample3 の優先度をすべて10で同じにしてみた.このため,他のもっと優先度の高いスレッドが割り込んでくることはあるが,この3つのスレッドに関してのみいうならばノン・プリエンプティブで動作する.

しかし,このような修正を入れて実行すると,こんな感じの結果になる.

...
8e9f0g1h2i3j4a5b6c7d8e9f0g1h2i3j4a5b6c7d8e9f
ghijabcdefghijabcdef[0]gAhBiCjDaEbFcGdHeIfJgAhBiCj[2]D
0E1F2G3H4I5J6A7B8C9D0E1F2G3H4I5J6A7B8C9D0E1F2G3H4I5J6A7B8C9D0E1F2G3H4I[1]5Ja6Ab7Bc8Cd9De0Ef1Fg2Gh3Hi4Ij5Ja6Ab7Bc8Cd9De0Ef1Fg2Gh3Hi4Ij5Ja6
b7c8d9e0f1g2h3i4j5a6b7c8d9e
fghijabcdefghijabcdefghijabcdefghij
[0]ABCDEFGHIJABCDEFGHIJABCDE[2]F0G1H2I3J4A5B6C7D8E9F0G1H2I3J4A5B6C7D8E9F0G1H2I3J4A5B6C7D8E9F0G1H2I3J4A5B6C7D8E9F0G1H2I3J4
5678901234567890123456789
[1]abcdefghijabcdefghijabcdefghijabcdefghija[2]b0c1d2e3f4g5h6i7j8a9b0c1d2e3f4g5h6i7j[0]8aA9bB0cC1dD2eE3fF4gG5hH6iI7jJ8
A9B0C1D2E3F4G5H6I7J8A9B0C1D2E3F4G5H6I7J8A9B0C1D2E3F4G5H6I7J8A9[1]B0aC1bD2cE3dF4eG5fH6gI7hJ8iA9jB
aCbDcEdFeGfHgIhJiAjBaCbDcEdFeGfHgIhJi
ja[2]b0c1d2e3f4g5h6i7j8a9b0c1d2e3f4g5h6i7j8a9b0c1d2e3f4g5h6i7j8a9b0c1d2e3f4g5h6i7j8
9012345678901234567890123456789
[0]ABCDEFGHIJABCDEFGHIJABCDEFG[2]H0I1J2A3B4C5D6E7F8G9H0I1J2A[1]3Ba4Cb5Dc6Ed7Fe8Gf9Hg0Ih1Ji2Aj3Ba4Cb5Dc6Ed7Fe8Gf9Hg0Ih1Ji2Aj3Ba4Cb5Dc6Ed7Fe8Gf9Hg0Ih1Ji2
j3a4b5c6d7e8f9g0h1i2j3a4b5c6d7e8f9g0h1i2j3a4b5c6d7e8f9g
hijabcdefghij
[0]ABCDEFGHIJABCDEFGHIJABCDEFG[2]H0I1J2A3B4C5D6E7F8G^C
...

なんか3つのスレッドの出力が,文字単位で混ざってしまっている感じだ.なぜこんなことになるかというと,文字の出力を1文字ごとに outlog スレッドにお願いしているため,そこで kz_send() によるメッセージ送信が発生する.しかしこれはシステムコールであるため,その都度スレッドのスケジューリングが行われ,sample1, sample2, sample3 は優先度が同じであるためラウンドロビンで実行されてしまっているのだな.

なので今回は,出力を outlog スレッドに任せるのでなく,write() によって直接出力するように修正する.これならば文字出力時にもスレッドのスケジューリングは行われない.

ということで,以下が今回のソース.修正点は以下.

diff -ruN kozos24/main.c kozos25/main.c
--- kozos24/main.c Sun Dec 2 21:10:47 2007
+++ kozos25/main.c Sun Dec 2 21:13:43 2007
@@ -1,5 +1,6 @@
#include
#include
+#include

#include "kozos.h"

@@ -8,29 +9,27 @@
static int sample_main(int argc, char *argv[])
{
int i, count;
- char *p;
+ char buf[128];
+ char *p = buf;

-#define INTERVAL 7 /* CPU能力に応じて調整してください */
+#define INTERVAL 5 /* 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);
+ write(2, buf, 3);
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);
+ write(2, buf, 1);
}
- p = kz_memalloc(128);
p[0] = '\n';
p[1] = '\0';
- kz_send(outlog_id, 0, p);
+ write(2, buf, 1);
}
return 0;
}
@@ -55,8 +54,8 @@
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);
+ sample2_id = kz_run(sample_main, "sample2", 10, 1, NULL);
+ sample3_id = kz_run(sample_main, "sample3", 10, 2, NULL);

return 0;
}

出力に write() を使用し,sample1, sample2, sample3 の優先度を10で同じにしてある.

で,以下が実行結果.

...
[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
[2]0123456789012345678901234567890123456789012345678901234567890123456789
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij
[2]0123456789012345678901234567890123456789012345678901234567890123456789
[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij
[2]0123456789012345678901234567890123456789012345678901234567890123456789
[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
[2]0123456789012345678901234567890123456789012345678901234567890123456789
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij
[2]0123456789012345678901234567890123456789012345678901234567890123456789
[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij
[2]0123456789012345678901234567890123456789012345678901234567890123456789
[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
[2]0123456789012345678901234567890123456789012345678901234567890123456789
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij
[2]0123456789012345678901234567890123456789012345678901234567890123456789
[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
...

出力中に割り込まれること無く,すべてのスレッドが1行単位できれいに表示できている.割り込まれまくってぐちゃぐちゃになっていた前回(第24回)の結果とはえらい違いだ.

ここで説明のために,KOZOSのスケジューリング部分を見てみよう.まずはスレッドをレディーキューに出し入れするgetcurrent(), putcurrent() の処理だ.

static void getcurrent()
{
readyque[current->pri] = current->next;
current->next = NULL;
}

static int putcurrent()
{
kz_thread **thpp;
for (thpp = &readyque[current->pri]; *thpp; thpp = &((*thpp)->next)) {
/* すでに有る場合は無視 */
if (*thpp == current) return -1;
}
*thpp = current;
return 0;
}

getcurrent() はレディーキューの先頭からスレッドを抜き取る.putcurrent() はレディーキューのお尻を検索し,そこにスレッドを接続する.ということで,KOZOSでは同じ優先度のスレッドはラウンドロビンでスケジュールされることになる.ラウンドロビンとは,さっき実行したものが最後に回されるというスケジューリング方式だ.まあ全部にわたって均等に実行される,ということになる.

次に割り込み処理(syscall_proc())と,そこから呼ばれるシステムコールの呼び出し処理部分(thread_intrvec()).

static void syscall_proc()
{
/* システムコールの実行中にcurrentが書き換わるのでポインタを保存しておく */
kz_syscall_param_t *p = current->syscall.param;

getcurrent();

switch (current->syscall.type) {
case KZ_SYSCALL_TYPE_RUN:
p->un.run.ret = thread_run(p->un.run.func, p->un.run.name, p->un.run.pri,
p->un.run.argc, p->un.run.argv);
break;
case KZ_SYSCALL_TYPE_EXIT:
/* スレッドが解放されるので戻り値などを書き込んではいけない */
thread_exit();
break;
case KZ_SYSCALL_TYPE_WAIT:
p->un.wait.ret = thread_wait();
break;
case KZ_SYSCALL_TYPE_SLEEP:
p->un.sleep.ret = thread_sleep();
break;
case KZ_SYSCALL_TYPE_WAKEUP:
p->un.wakeup.ret = thread_wakeup(p->un.wakeup.id);
break;
case KZ_SYSCALL_TYPE_GETID:
p->un.getid.ret = thread_getid();
break;
case KZ_SYSCALL_TYPE_CHPRI:
p->un.chpri.ret = thread_chpri(p->un.chpri.pri);
break;
case KZ_SYSCALL_TYPE_SEND:
p->un.send.ret = thread_send(p->un.send.id, p->un.send.size, p->un.send.p);
break;
case KZ_SYSCALL_TYPE_RECV:
p->un.recv.ret = thread_recv(p->un.recv.idp, p->un.recv.pp);
break;
case KZ_SYSCALL_TYPE_TIMER:
p->un.timer.ret = thread_timer(p->un.timer.msec);
break;
case KZ_SYSCALL_TYPE_PENDING:
p->un.pending.ret = thread_pending();
break;
case KZ_SYSCALL_TYPE_SETSIG:
p->un.setsig.ret = thread_setsig(p->un.setsig.signo);
break;
case KZ_SYSCALL_TYPE_DEBUG:
p->un.debug.ret = thread_debug(p->un.debug.sockt);
break;
case KZ_SYSCALL_TYPE_MEMALLOC:
p->un.memalloc.ret = thread_memalloc(p->un.memalloc.size);
break;
case KZ_SYSCALL_TYPE_MEMFREE:
p->un.memfree.ret = thread_memfree(p->un.memfree.p);
break;
default:
break;
}
return;
}
...
static void thread_intrvec(int signo)
{
switch (signo) {
case SIGSYS: /* システムコール */
syscall_proc();
break;
case SIGHUP: /* 外部割込み */
break;
case SIGALRM: /* タイマ割込み発生 */
alarm_handler();
break;
case SIGBUS: /* ダウン要因発生 */
case SIGSEGV:
case SIGTRAP:
case SIGILL:
if (debug_sockt) {
stub_proc(current, signo);
} else {
fprintf(stderr, "error thread \"%s\"\n", current->name);
/* ダウン要因発生により継続不可能なので,スリープ状態にする*/
getcurrent();
#if 1 /* スレッド終了する */
thread_exit();
#endif
}
break;
default:
break;
}
extintr_proc(signo);
schedule();

/*
* スタブでの read() 待ちブロック中に SIGALRM (および SIGHUP)が発生した
* 場合に,以下の setcontext() によるコンテキストスイッチで SIGALRM が
* 消えてしまう(これはスタブでのブロックに比べると発生頻度は低いが,OSの
* 処理中にシグナル発生した場合も同様).
* 対策として,シグナルのブロックを一瞬だけ開けて,SIGALRM が発生した
* 場合には,再度シグナル処理を行う.
* これはOSの割り込み処理の再入になるが,以下の位置に限定して再入が行われる
* ので問題は無い.
*/
on_os_stack = 1;
sigprocmask(SIG_UNBLOCK, &block, NULL);
sigprocmask(SIG_BLOCK, &block, NULL);
on_os_stack = 0;

/* ここで SIGALRM が発生するとシグナルを取りこぼす...要検討 */

setcontext(&current->context.uap);
}

割り込み発生時には割り込みベクタの処理として thread_intrvec() が呼ばれるが,ここではとくに何もせず,各割り込み処理を呼び出してからスケジューリングのためにschedule() を呼び,最後に setcontext() によってコンテキスト切替え(スレッドのディスパッチ)を行っている.

割り込みの内容がシステムコールの場合には syscall_proc() が呼ばれる.syscall_proc() では getcurrent() が行われた後,必要ならば各システムコールの処理の中で putcurrent() が呼ばれる.

getcurrent() が呼ばれているのは,ダウン要因発生時を除けば,システムコールのときだけだ.ということは,システムコール以外の割り込み処理の場合にはgetcurrent(), putcurrent() が呼ばれないため,スレッドのラウンドロビンは行われないということになる.

ここで sample1, sample2, sample3 について考えてみよう.たとえば sample1 の実行中に SIGHUP が入って,他のもっと優先度の高いスレッドが(プリエンプティブに)実行可能になったとしよう.この場合,次にスケジュールされるのはそっちのスレッドになるが,getcurrent() が呼ばれるわけではないので,sample1 はレディーキューの先頭にいるままだ.なので,割り込んできた優先度の高いスレッドが処理を終了した後,スケジューリングされるのは再び sample1 になる.

さらに sample1 の実行中に,sample2 が設定したタイマ割り込みが発生し,sample2 に対してメッセージが投げられ,sample2 が実行可能になったとしよう.この場合,putcurrent() によって sample2 はレディーキューのお尻に接続される.よって,次にスケジューリングされるのはやはり sample1 だ.結局のところ,システムコールによって getcurrent() が呼ばれてレディーキューからいったん抜かれないことには,sample1 が常にキューの先頭にあるため,sample2, sample3 に処理は回らずに sample1 が実行されることになる.

このようなことを考えると,優先度が同じである sample1, sample2, sample3 は,互いに割り込まれることは無く実行される(そしてシステムコールが呼ばれると,そのタイミングでラウンドロビンが発生し,スレッドが切り替わる).まあもちろんこの3スレッドよりも優先度の高いスレッドが割り込んでくることはあるわけだが,この3つに閉じて言うならば,ノン・プリエンプティブに動作しているといえる.

このようにあるスレッドの動作中に,他のスレッドに割り込まれること無く動作することをノン・プリエンプティブと言う.まあとはいっても,これは sample1, sample2, sample3 のみ同じ優先度にしているのでこのようなことが起きているわけで,優先度の高い他のスレッドが動作可能になった場合にはやっぱり割り込まれるわけだから,OS全体としてノン・プリエンプティブなのではなく,sample1, sample2, sample3 の3つの間だけでノン・プリエンプティブに動作している,ということができる.

で,ここまではノン・プリエンプティブの説明なのだけど,ここからは汎用OSと組み込みOSの違いの話になる.

汎用OSだと,スレッド(汎用OSの場合はプロセスが主体だけど)はこーいうふうに全部が同じ優先度で,ノン・プリエンプティブに割り込まれること無く動く.これに対して組み込みOSだと,優先度ベースのプリエンプティブな動作になるのが普通だ.

まず汎用OSとはどんなものか?なのだけど,まあ一番身近なのは Windows だ.あと Macintosh とか.もしくは Linux や,この連載で利用している FreeBSD も汎用OSの仲間といえる.

汎用OSは,PCを使うためのOSともいえる.PCは,いろいろなアプリをインストールすることで多目的に使える,多目的コンピュータだ.アプリをインストールすることで多目的に使えることがその最大の特徴だ.汎用OSはそれを使うためのOSなので,多目的OSということもできるだろうか.

これに対して組み込みOSとはどんなものか?まず代表的なのは,我が日本が誇るμiTRONと,その実装であるToppersだろう.

なぜ組み込みOSが必要なのか?たとえば最近の携帯電話は,普通の電話機能の他にメーラになったり,ブラウザ代わりになったり,簡単なゲームができたりできる.そしてその最中にも着信があれば受けるし,時計も表示するし,つまり,複数の機能が同時に平行動作している.

これを汎用OSであるUNIX上で同じようなことをやろうとしたら簡単だ.ブラウザを起動して,メーラを起動して,時計アプリを起動するだけだ.これは複数のアプリを起動したときにも,時分割でそれぞれがそれなりに動けるようにOSがCPU時間を配分してくれているからだ.プログラムを書く側からしても,各機能ごとに別々のアプリを作成して別々の実行形式を作成しておけばよい.つまり,UNIXの上で動作しているからこそ,なにも考えずに複数のアプリが同時動作できるわけだ.

では,上記の機能をUNIX上で,ひとつのアプリでぜんぶ行うということを考えてみてほしい.

この場合,複数の機能が同時動作するわけだから,一定時間ずつ処理を行ってはいったんメインループに戻って,次の処理用の関数を呼び出して,これも一定時間たったら処理を中断してメインループに戻って,...ということをしなければならない.たとえばブラウザ機能部分が巨大ファイルのダウンロード処理を行うとしたら,一定時間ごとに中断して戻って時計表示を行ってまたダウンロード処理を呼ぶ,ということをしないと,ダウンロード処理を始めたら時計が固まる,ということになってしまう.もしくはダウンロード処理の中から時計処理を呼ぶという方法もあるが,これをやるときりがなくなる(あらゆる場所から,あらゆる処理用関数をちょこまかと呼ばなければならなくなる)ので,まあまともな作りにはならない.

これが,OSが無いという状態だ.

しかしOSの上で動作させるならば,これらの機能は複数のアプリに分割して書いてしまって構わない.OSが時分割で適当に,複数のプロセスを切替えながら動作させてくれるため,なにもしなくても勝手に並列動作してくれるからだ.なのでブラウザは時計アプリのことは気にしないし,時計アプリはブラウザのことは気にせずに書いてしまって構わない.

つまるところ,機能がひとつしかないのならば,OSなどなくてもかまわない.というより,単機能ならば,OSが存在するメリットはあまり無い.つまり組み込みOSの役割は,複数の機能が実装された際にも,それぞれを独立にプログラミングできるという点にある.だって時計アプリを書くときに,同時動作するかもしれないメーラやブラウザのことなんて考えないよね.そういうとそんなの当たり前じゃん!といわれそうだけど,それはOSの上で書くから当り前になっているのであって,もしもOSが無いのならば,あるアプリがCPU使いすぎないようにするとか,CPU使いすぎてるときには適当にCPUを開放するとかの対処が必要になる.でないと他のアプリが固まることになるわけだ.

で,組み込みOSと汎用OSの違いについて.まあこういうことはいろんなOSの説明資料で説明されていることなのでいまさらなのだけど,ぼくが思う一番重要な違いだけいうと
  • 汎用OS ... ユーザがアプリを好きにインストールして使う
  • 組み込みOS ... ユーザがアプリをインストールすることはない
ということにつきるとおもう.

上でも書いたように,汎用OSの役割はアプリをインストールすることで多目的に使えることだ.ということは,アプリのインストールができること,そのために外部ストレージ(ハードディスクか,それに代わる何か)を持っていることが必須になる.(ここでいう「アプリ」はCPUが直接実行し,さらにOSのシステムコールを直接実行できるという意味なので,たとえば携帯電話のJavaアプリとかフラッシュアプリとかいったものは除外する)

しかしアプリのインストールを許可するか,しないかというだけで,OSのポリシーはぜんぜん異なるものになる.

アプリをインストールできるようにするということは,ユーザがわけのわからんアプリを入れて動かしたりする,ということだ.それは製品版のアプリかもしれないし,GNUのようなそれなりに信頼できる団体やコミュニティで作られたフリーソフトウエアかもしれない.どこぞの馬の骨が作ったしょぼいフリーソフトウエアかもしれない.そもそもそのPCの持ち主がちょろっと作ったテスト用アプリかもしれない.

そして例えばそのアプリは,CPUやメモリを馬鹿食いするものかもしれない.やたらと時間のかかる計算処理をずっと行うかもしれないし,それどころか無限ループに陥ることすらあるかもしれない.バグがあって,頻繁にダウンするものかもしれない.終了時にメモリをきちんと開放していないかもしれない.なのでそーいうようなアプリをユーザが勝手にインストールして動かしたとしても,そこそこ動く,問題があっても極力ほかのアプリに迷惑をかけない,といったことが重要となる.

ということで,汎用OSに求められる動作は
  • アプリが重い処理に入ってCPUを食い続けたとしても,適当なタイミングでOSが他のアプリに処理を渡す.(でないとその1アプリのせいで,他のアプリ全体が固まることになる)
  • アプリがバグでNULLポインタアクセスとかゼロ除算とかしても,そのアプリだけが落ちて,他のアプリは動作を継続する.
  • アプリは別のアプリが使っているメモリ領域を読み書きできない.
ということになる.まあひとことでいうと「アプリを信用しない」ということに尽きる.

この場合,アプリは優先度なしに,全部が同じ優先度で公平に(時分割で)動いてほしい.そしてメモリの保護は,仮想メモリという考えになり,各タスクはプロセス単位でそれぞれの独立した仮想メモリ上で動作する,という思想につながる.

アプリが無限ループに入ってしまったらどうするか?まあ無限ループではないにしても,すごく重い計算とかソート処理とかに入ってしまうことはある.なんにせよ,そのへんから拾ってきたようなアプリを走らせることもあるわけなので,アプリが何をするかは,汎用OSの場合は,ホント,わからない.

で,無限ループをそのまま許容したら,他のアプリは固まることになる.汎用OSはノン・プリエンプティブなので,優先度をベースにして他のタスクが割り込むことはできない.これでは困るので,汎用OSの場合はタイマで一定時間ごとにタスクを切り替えるようになっている.つまり,時分割で動作する.もしもOSを利用しないならば,その重い計算中に,一定時間ごとに他のタスクに処理を渡すというか,そーいうめんどくさい書き方をしなければならなくなる.つまり,他の機能の事まで考えなければならないことになる.

例えばブラウザによる巨大ファイルのダウンロードとメーラとDVDプレイヤーによる動画再生を並列で走らせたときのことを考えてみてほしい.これを汎用OSで,タスクごとに優先度をつけて動かしたらどうなるか?

この中でもっとも優先度を高くすべきなのは,きっと動画再生だ.動画再生は一定時間ごとに確実に処理されないと,コマ落ちの原因になる.次に優先度を高くすべきなのは,メーラだろう.ユーザが操作したときに,レスポンス良く動いてくれないとイライラするからだ.ダウンロードは,まあほっといてもそのうち終わっててくれればいいので,一番優先度は低くなる.

しかし,実はこれではだめなのだ.たとえば動画再生が最高優先度の場合,普段は問題なく動くだろう.しかし動画処理がCPUの限界性能にきてしまったとき,つまりある閾値を超えたときに,とつぜんマウスすら反応しなくなる,なんていうバグの原因になり得る.この場合,ほかのアプリはまったく動かなくなり,マウスポインタすら反応せず,ただ動画だけがコマ落ちしながら動いているという,とっても悲しい状態になるだろう.

では逆に,動画再生の優先度を最低にしたらどうなるか?これはこれで,ちょっと悲しい結果になる.というのはなにもなければ動画は普通に再生できるのだが,ブラウザでダウンロードとか始めると,一気にコマ落ちしまくる,という原因になる.このように,優先度の設計は他のアプリとの関係で相対的に行わなければならないので,非常に難しい(そして間違えると固まるなどの致命的な問題になる).

ということで,タスクに優先度をつけるという考え方は,汎用OSには向かない.すべてのアプリが自分に割り当てられた時間内でそこそこに,均等に動いてほしいからだ.で,うまく動けないアプリがあったとしても,それはCPUパワーが足りない(このため割り当て時間が短い)ということなのでしかたがない,といえる.重要なのは,CPUパワーが足りないような状況になったときに,優先度の高いタスクのみ動き続けられても困る,ということだ(たぶん固まることになる).なので,すべてのアプリは優先度無しに一定時間ずつ動作する(無限ループや重い処理を行うようなアプリがあっても,CPU時間が他のアプリにきちんと割り当てられる),ということが望まれる.

これに対して,組み込みOSを考えてみよう.

組み込み機器では,基本的にユーザが好きなアプリをインストールして使うようなことはない.インストールするアプリはその製品の開発元が厳選し,十分なテストの後に出荷することになる.このためアプリにバグがあったとしても,それは開発元で管理することができる.製品をシステム全体として開発元で管理することができる.(汎用OSではこのような管理は無理.たとえば世の中に出回っている Windows 用のアプリ(フリーソフト含む)を Microsoft がすべてチェックする,なんてことは現実的に無理)

組み込み機器ではインストールされるアプリが限定されるため,アプリごとに優先度を明確につけておくことができる.優先度付けも含めたシステム全体の構成を,開発元で設計することができる.

もしも汎用OSでもこのような「優先度」を採用するとしよう.たとえば Windows ならば,Microsoftが世の中に出回っているすべての Windows 用アプリを調べて適切に優先度をつけるようなことは無理だ.なので,ユーザが自分で設定するか,もしくはアプリを作った人が,自分で好きな優先度を設定するかになる.しかし優先度というのは絶対的な値が意味を持つものではなく,他のアプリとの相対値によって意味を持つものだ.なので,ちょっと優先度の設定を誤ると即システム全体が固まるとか,このアプリを入れるとこのアプリはまともに動作しないというような相性の問題になり得る.例えばマウスを動かしたときには,割り込みが入り,マウスが動かされたという情報がデバイスドライバに蓄えられる.これは割り込みの延長で行われるので,たとえ無限ループに陥っているアプリがあったとしても問題無く実行される.しかしその情報を吸い上げ,マウスカーソルを動かすのは,それはそれでそーいうアプリが行っている.で,他のアプリがCPUを食いまくって,そのアプリのほうが優先度が高いならば,マウスがまったく動かない(ので,そのアプリを止めることすらできない)ということになりうる.

また組み込み機器では,メモリを保護する必要はさほど無い.というよりリアルタイム性が求められることも多いため,むしろ仮想メモリを使うべきではない,ということも多い(仮想メモリを実装せずに,局所的にメモリ保護を行う場合は多いが,この場合はタスク単位でメモリが保護されるわけではない).これは仮想メモリのマッピング切替えに時間がかかるということもあるが,ページングによりリアルタイム性がまったく確保できなくなる,という理由が大きい.

ここで,もう一度汎用OSと組み込みOSの違いを思い出してほしい.
  • 汎用OS ... ユーザがアプリを好きにインストールして使う
  • 組み込みOS ... ユーザがアプリをインストールすることはない
これは,さらに付け加えると
  • 汎用OS ... ユーザがアプリを好きにインストールして使う
    →誰が何をインストールするかわからないので,どんなおかしな アプリやモラルのないアプリをインストールされても,OSがうまく 資源配分して,全てがそこそこ動いてほしい.
  • 組み込みOS ... ユーザがアプリをインストールすることはない
    →何をインストールするかは開発元できちんと管理できるので, システム全体を通してのチューニングが可能. 全アプリをインストール済みの状態で出荷され,追加インストール されることはない. おかしな動作をするアプリがあった場合には, そのアプリのバグを開発元で修正してから出荷するべきなので, バグがあってもOSがそこそこうまく動かしてくれるような必要は無い.
ということになる.こーしてみると,汎用OSは性悪説,組み込みOSは性善説に基づいている,ともいえるように思う.

汎用OSは不特定多数が集まるなんかの大会の会場(なので人の迷惑になることをする人がいるかもしれないし,そのときに他の人に迷惑がかからないようなシステム(警備員を配置するとか,大会本部がきちんと管理するとか,大会規約を決めるとか)が必要),組み込みOSは家族(なので悪いことをする人はいないし,いるとしたらそれはそもそもその人をその場で正すべきなので,他のメンバーに迷惑がかからないようにするようなシステムは不要),と例えられるだろうか.うーん,あんまりわかりやすい例えになっていないかなあ...でも組み込みを家族とか言っちゃうと,まるで家族内で優先度があるみたいでやだね.やっぱしあまりいい例えじゃあないかしら.

まあもっとも組み込みにもいくつか種類があって,汎用OSのようにCPUやメモリが足りない場合にはそこまでで許される,というものもあれば,厳密なリアルタイム性が求められるものもある.なので組み込み機器にLinuxのような汎用OSが用いられたり,Linux をもっと組み込みっぽく改造した組み込み Linux というものが使われることもある.
(注意)このブログは本家のほうの文章部分のみの転載です.ソースコードの配布,画像などについては本家のほうを参照してください.文章中のリンク先は面倒なのですべて本家のほうに変換してしまっているのでご注意ください.

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

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

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

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

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(&current->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 ... 優先度10
  • sample2 ... 優先度11
  • sample3 ... 優先度12
となっている.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
...

(注意)このブログは本家のほうの文章部分のみの転載です.ソースコードの配布,画像などについては本家のほうを参照してください.文章中のリンク先は面倒なのですべて本家のほうに変換してしまっているのでご注意ください.

前回まででスレッド対応は完了のはずだったのだけど,ちょっとバグがあったので修正.

前回のソースコードで info threads を2回実行すると,以下のようになる.

(gdb) info threads
7 Thread 135026528 ( Name: httpd, State: SLP, Priority: 09) 0x0805eeb7 in kill ()
6 Thread 135025760 ( Name: telnetd, State: SLP, Priority: 08) 0x0805eeb7 in kill ()
5 Thread 135024992 ( Name: clock, State: SLP, Priority: 07) 0x0805eeb7 in kill ()
4 Thread 135024224 ( Name: idle, State: RUN, Priority: 1f) 0x0806095b in select ()
3 Thread 135023456 ( Name: outlog, State: SLP, Priority: 03) 0x0805eeb7 in kill ()
2 Thread 135021920 ( Name: extintr, State: SLP, Priority: 01) 0x0805eeb7 in kill ()
* 1 Thread 135022688 ( Name: stubd, State: RUN, Priority: 02) breakpoint ()
at i386-stub.c:1187
(gdb) info threads
8 Thread -1 ( Name: stubd, State: RUN, Priority: 02) breakpoint ()
at i386-stub.c:1187
7 Thread 135026528 ( Name: httpd, State: SLP, Priority: 09) 0x0805eeb7 in kill ()
6 Thread 135025760 ( Name: telnetd, State: SLP, Priority: 08) 0x0805eeb7 in kill ()
5 Thread 135024992 ( Name: clock, State: SLP, Priority: 07) 0x0805eeb7 in kill ()
4 Thread 135024224 ( Name: idle, State: RUN, Priority: 1f) 0x0806095b in select ()
3 Thread 135023456 ( Name: outlog, State: SLP, Priority: 03) 0x0805eeb7 in kill ()
2 Thread 135021920 ( Name: extintr, State: SLP, Priority: 01) 0x0805eeb7 in kill ()
* 1 Thread 135022688 ( Name: stubd, State: RUN, Priority: 02) breakpoint ()
at i386-stub.c:1187
(gdb)

2回目の info threads で,スレッド番号8としてなんかスレッドIDが-1のstubd というのが出てしまっている.stubd はスレッド番号1ですでに出ているので,これは明らかにおかしい.

このときの通信内容は以下.

($T054:58f60e08;5:58f60e08;8:65af0408;thread:080c4860;#7d)[+]
[$M8048088,1:55#c2](+)($OK#9a)[+]
[$T080c4860#21](+)($OK#9a)[+]
[$qfThreadInfo#bb](+)($#00)[+]
[$qL1200000000000000000#50](+)($qM010000000000000000000000000080c4560#99)[+]
[$qL02000000000080c4560#99](+)($qM01000000000080c456000000000080c4860#e6)[+]
[$qL02000000000080c4860#9c](+)($qM01000000000080c486000000000080c4b60#13)[+]
[$qL02000000000080c4b60#c6](+)($qM01000000000080c4b6000000000080c4e60#40)[+]
[$qL02000000000080c4e60#c9](+)($qM01000000000080c4e6000000000080c5160#10)[+]
[$qL02000000000080c5160#96](+)($qM01000000000080c516000000000080c5460#e0)[+]
[$qL02000000000080c5460#99](+)($qM01000000000080c546000000000080c5760#e6)[+]
[$qL02000000000080c5760#9c](+)($qM01100000000080c5760#9d)[+]
[$qThreadExtraInfo,80c5760#22](+)($#00)[+]
[$qP0000001f00000000080c5760#c5](+)($QP0000001f00000000080c5760000000011000000000080c5760000000020110000000403SLP0000000805httpd000000100209#1b)[+]
[$Hg80c5760#4c](+)($OK#9a)[+]
[$g#67](+)($0000000000000000ea04000060570c08fcc5110818c61108ecc7110800000000b7ee050802020000330000003b0000003b0000003b0000003b0000001b000000#1b)[+]
[$qP0000001f00000000080c5460#c2](+)($QP0000001f00000000080c5460000000011000000000080c5460000000020110000000403SLP0000000807telnetd000000100208#e2)[+]
[$Hg80c5460#49](+)($OK#9a)[+]
[$g#67](+)($0000000000000000ea04000060540c08fc35110818361108ec37110800000000b7ee050802020000330000003b0000003b0000003b0000003b0000001b000000#88)[+]
[$qP0000001f00000000080c5160#bf](+)($QP0000001f00000000080c5160000000011000000000080c5160000000020110000000403SLP0000000805clock000000100207#f5)[+]
[$Hg80c5160#46](+)($OK#9a)[+]
[$g#67](+)($0000000098b00d08ea04000060510c083ca7100858a71008eca7100800000000b7ee050806020000330000003b0000003b0000003b0000003b0000001b000000#63)[+]
[$qP0000001f00000000080c4e60#f2](+)($QP0000001f00000000080c4e60000000011000000000080c4e60000000020110000000403RUN0000000804idle00000010021f#22)[+]
[$Hg80c4e60#79](+)($OK#9a)[+]
[$g#67](+)($0400000004000000ffffffff604e0c088c171008b8171008ec171008000000005b09060813020000330000003b0000003b0000003b0000003b0000001b000000#a3)[+]
[$qP0000001f00000000080c4b60#ef](+)($QP0000001f00000000080c4b60000000011000000000080c4b60000000020110000000403SLP0000000806outlog000000100203#e0)[+]
[$Hg80c4b60#76](+)($OK#9a)[+]
[$g#67](+)($0000000060800f08ea040000604b0c083c870f0858870f08ec870f0800000000b7ee050806020000330000003b0000003b0000003b0000003b0000001b000000#84)[+]
[$qP0000001f00000000080c4560#c2](+)($QP0000001f00000000080c4560000000011000000000080c4560000000020110000000403SLP0000000807extintr000000100201#f9)[+]
[$Hg80c4560#49](+)($OK#9a)[+]
[$g#67](+)($0000000006000000ea04000060450c086c660e0888660e08ec670e0800000000b7ee050806020000330000003b0000003b0000003b0000003b0000001b000000#0c)[+]
[$qP0000001f00000000080c4860#c5](+)($QP0000001f00000000080c4860000000011000000000080c4860000000020110000000403RUN0000000805stubd000000100202#18)[+]
[$Hg80c4860#4c](+)($OK#9a)[+]
[$g#67](+)($00880d0800000000ea04000060480c0858f60e0858f60e08ecf70e080000000065af040802020000330000003b0000003b0000003b0000003b0000001b000000#80)[+]
[$T080c5760#21](+)($OK#9a)[+]
[$T080c5460#1e](+)($OK#9a)[+]
[$T080c5160#1b](+)($OK#9a)[+]
[$T080c4e60#4e](+)($OK#9a)[+]
[$T080c4b60#4b](+)($OK#9a)[+]
[$T080c4560#1e](+)($OK#9a)[+]
[$T080c4860#21](+)($OK#9a)[+]
[$qL120f0000000080c5760#d3](+)($qM011f0000000080c5760#d3)[+]
[$qP0000001f00000000ffffffff#28](+)($QP0000001f00000000ffffffff000000011000000000080c4860000000020110000000403RUN0000000805stubd000000100202#7b)[+]
[$Hg-1#0d](+)($OK#9a)[+]
[$g#67](+)($00880d0800000000ea04000060480c0858f60e0858f60e08ecf70e080000000065af040802020000330000003b0000003b0000003b0000003b0000001b000000#80)[+]
[$qP0000001f00000000080c5760#c5](+)($QP0000001f00000000080c5760000000011000000000080c5760000000020110000000403SLP0000000805httpd000000100209#1b)[+]
[$Hg80c5760#4c](+)($OK#9a)[+]
[$g#67](+)($0000000000000000ea04000060570c08fcc5110818c61108ecc7110800000000b7ee050802020000330000003b0000003b0000003b0000003b0000001b000000#1b)[+]
[$qP0000001f00000000080c5460#c2](+)($QP0000001f00000000080c5460000000011000000000080c5460000000020110000000403SLP0000000807telnetd000000100208#e2)[+]
[$Hg80c5460#49](+)($OK#9a)[+]
[$g#67](+)($0000000000000000ea04000060540c08fc35110818361108ec37110800000000b7ee050802020000330000003b0000003b0000003b0000003b0000001b000000#88)[+]
[$qP0000001f00000000080c5160#bf](+)($QP0000001f00000000080c5160000000011000000000080c5160000000020110000000403SLP0000000805clock000000100207#f5)[+]
[$Hg80c5160#46](+)($OK#9a)[+]
[$g#67](+)($0000000098b00d08ea04000060510c083ca7100858a71008eca7100800000000b7ee050806020000330000003b0000003b0000003b0000003b0000001b000000#63)[+]
[$qP0000001f00000000080c4e60#f2](+)($QP0000001f00000000080c4e60000000011000000000080c4e60000000020110000000403RUN0000000804idle00000010021f#22)[+]
[$Hg80c4e60#79](+)($OK#9a)[+]
[$g#67](+)($0400000004000000ffffffff604e0c088c171008b8171008ec171008000000005b09060813020000330000003b0000003b0000003b0000003b0000001b000000#a3)[+]
[$qP0000001f00000000080c4b60#ef](+)($QP0000001f00000000080c4b60000000011000000000080c4b60000000020110000000403SLP0000000806outlog000000100203#e0)[+]
[$Hg80c4b60#76](+)($OK#9a)[+]
[$g#67](+)($0000000060800f08ea040000604b0c083c870f0858870f08ec870f0800000000b7ee050806020000330000003b0000003b0000003b0000003b0000001b000000#84)[+]
[$qP0000001f00000000080c4560#c2](+)($QP0000001f00000000080c4560000000011000000000080c4560000000020110000000403SLP0000000807extintr000000100201#f9)[+]
[$Hg80c4560#49](+)($OK#9a)[+]
[$g#67](+)($0000000006000000ea04000060450c086c660e0888660e08ec670e0800000000b7ee050806020000330000003b0000003b0000003b0000003b0000001b000000#0c)[+]
[$qP0000001f00000000080c4860#c5](+)($QP0000001f00000000080c4860000000011000000000080c4860000000020110000000403RUN0000000805stubd000000100202#18)[+]
[$Hg80c4860#4c](+)($OK#9a)[+]
[$g#67](+)($00880d0800000000ea04000060480c0858f60e0858f60e08ecf70e080000000065af040802020000330000003b0000003b0000003b0000003b0000001b000000#80)[+]
(この状態で停止)

通信内容をよく見ると,

[$qP0000001f00000000ffffffff#28](+)($QP0000001f00000000ffffffff000000011000000000080c4860000000020110000000403RUN0000000805stubd000000100202#7b)[+]
[$Hg-1#0d](+)($OK#9a)[+]

という通信が行われていて,スレッドIDが-1でqPコマンドが発行されているんだな.で,その直後にやはりスレッドIDが-1でHgコマンドが発行されている.

Interface誌の今月号(2007/12)を見ると,HgコマンドがスレッドIDを-1で発行する際には,どうもすべてのスレッドに対して処理を行う,という意味になるらしい.あと remote.c の内部では,Hgコマンドは set_thread() によって発行されるのだけど,

set_thread (-1, 0);

のようにして,スレッドIDを-1として発行している部分がある.まあ上の場合は第2引数がゼロなので,HgではなくHcコマンドが発行されるのだが,スレッドIDが-1というのはやはり特別な意味を持っているらしく,このへんがあやしい.

...とおもっていろいろ調べたり,スレッドIDが-1の場合のqPコマンドやHgコマンドの動作をいろいろ変えたりして試してみたのだけど,おかしいまま.うーんへんだ.

どうもqPがスレッドIDを-1で発行されること自体,おかしな気がする.それでもってそれがまた成功してしまっているので,その後に Hg も -1 で発行されてしまっているのではなかろうか?

で,スレッド関連の通信内容をもう一度チェック.とくに,多少複雑なプロトコルになっているqLコマンドとかをよく見直す.そしたら以下のあやしい部分を発見.

[$qL1200000000000000000#50](+)($qM010000000000000000000000000080c4560#99)[+]
[$qL02000000000080c4560#99](+)($qM01000000000080c456000000000080c4860#e6)[+]
[$qL02000000000080c4860#9c](+)($qM01000000000080c486000000000080c4b60#13)[+]
[$qL02000000000080c4b60#c6](+)($qM01000000000080c4b6000000000080c4e60#40)[+]
[$qL02000000000080c4e60#c9](+)($qM01000000000080c4e6000000000080c5160#10)[+]
[$qL02000000000080c5160#96](+)($qM01000000000080c516000000000080c5460#e0)[+]
[$qL02000000000080c5460#99](+)($qM01000000000080c546000000000080c5760#e6)[+]
[$qL02000000000080c5760#9c](+)($qM01100000000080c5760#9d)[+]

最後のqLに対するqMの応答は,もうそれ以上スレッドが無いため,スレッドIDが空となって返っている.しかし応答では qM011... となっており,doneフラグは立っているのだが,スレッド数が1のままなんだな.i386-stub.c のqLコマンド応答部分を見てみると

case 'q':
switch (*ptr++)
{
case 'L':
{
int startflag, doneflag, countmax, count = 1, i;
...
ptr = remcomOutBuffer;
*ptr++ = 'q';
*ptr++ = 'M';
*ptr++ = hexchars[count >> 4];
*ptr++ = hexchars[count & 0xf];
*ptr++ = doneflag ? '1' : '0';

となっており,スレッド数である count が無条件で1になっている.これはまずい.

ということは,qPがスレッドIDを0xffffffff(つまり,-1)で発行されることと,HgがスレッドIDを-1で発行されることの対処を前回入れているが,実は上記 count がおかしいためにへんなスレッドIDのスレッドが登録されてしまっているのがそもそもの原因なのではなかろうか? count に対する修正を入れれば,このへんの対処は不要なのではなかろうか.

で,修正したのがこんな感じ.前回からの差分については diff.txt 参照.

では修正内容について説明しよう.まずqLコマンドの応答部分.

diff -ruN kozos22/i386-stub.c kozos23/i386-stub.c
--- kozos22/i386-stub.c Sat Nov 24 14:39:50 2007
+++ kozos23/i386-stub.c Sat Nov 24 17:45:25 2007
@@ -986,7 +986,7 @@
{
case 'L':
{
- int startflag, doneflag, countmax, count = 1, i;
+ int startflag, doneflag, countmax, count, i;
unsigned int threadid[2];
kz_thread *thp;

@@ -1008,6 +1008,9 @@
}
}

+ count = 1;
+ if (doneflag) count = 0;
+
ptr = remcomOutBuffer;
*ptr++ = 'q';
*ptr++ = 'M';

スレッド数を,最後のスレッドの場合はゼロにするように修正してある.

さらに,qPコマンドの応答部分.

@@ -1048,16 +1051,8 @@
mode = hexToIntN(&ptr, 4);
threadid[0] = hexToIntN(&ptr, 4);
threadid[1] = hexToIntN(&ptr, 4);
- if (threadid[1] == 0xffffffff)
- {
- /*
- * 何を返すべきかちょっと不明なので,とりあえず
- * カレントスレッドを返す.
- */
- thp = gen_thread; /* current を返すべきか? 不明... */
- } else {
- thp = (kz_thread *)threadid[1];
- }
+
+ thp = (kz_thread *)threadid[1];

ptr = remcomOutBuffer;
*ptr++ = 'Q';

たぶん今回のqLコマンドのバグ修正により,スレッドIDが0xffffffffでqPが発行されるようなことは無くなると思うので,その場合の対処を削除.まあこんなへんな処理,残しといてもしょうがないしね.

次に,Hgコマンドの応答部分の修正.

@@ -1125,8 +1120,11 @@
if (hexToInt(&ptr, &val))
{
if (rev) val = -val;
+ if (val == -1)
+ {
+ break;
+ }
stub_restore_regs(gen_thread);
- if (val == -1) val = (int)current;
gen_thread = (kz_thread *)val;
stub_store_regs(gen_thread);
strcpy (remcomOutBuffer, "OK");

今回の修正で,スレッドIDが-1でHgが発行されることは無くなると思われるのだが,上のほうで書いたように,Interface誌によれば,スレッドIDが-1でHgコマンドが発行された場合はすべてのスレッドを指す(?)らしい(?)ので,まあ未対応として「$#00」を返すようにする.まあたぶん不要だとは思うのだけど,いちおう入れておく.

では動作させてみよう.いつもどおり実行形式を起動して gdb で接続,continue,Ctrl-C でブレークし,info threads を繰り返し実行する.

(gdb) info threads
7 Thread 135026496 ( Name: httpd, State: SLP, Priority: 09) 0x0805eeab in sigprocmask ()
6 Thread 135025728 ( Name: telnetd, State: SLP, Priority: 08) 0x0805eeab in sigprocmask ()
5 Thread 135024960 ( Name: clock, State: SLP, Priority: 07) 0x0805eeab in sigprocmask ()
4 Thread 135024192 ( Name: idle, State: RUN, Priority: 1f) 0x0806094f in tcflow ()
3 Thread 135023424 ( Name: outlog, State: SLP, Priority: 03) 0x0805eeab in sigprocmask ()
2 Thread 135021888 ( Name: extintr, State: SLP, Priority: 01) 0x0805eeab in sigprocmask ()
* 1 Thread 135022656 ( Name: stubd, State: RUN, Priority: 02) 0x0804af59 in breakpoint () at i386-stub.c:1184
(gdb) info threads
7 Thread 135026496 ( Name: httpd, State: SLP, Priority: 09) 0x0805eeab in sigprocmask ()
6 Thread 135025728 ( Name: telnetd, State: SLP, Priority: 08) 0x0805eeab in sigprocmask ()
5 Thread 135024960 ( Name: clock, State: SLP, Priority: 07) 0x0805eeab in sigprocmask ()
4 Thread 135024192 ( Name: idle, State: RUN, Priority: 1f) 0x0806094f in tcflow ()
3 Thread 135023424 ( Name: outlog, State: SLP, Priority: 03) 0x0805eeab in sigprocmask ()
2 Thread 135021888 ( Name: extintr, State: SLP, Priority: 01) 0x0805eeab in sigprocmask ()
* 1 Thread 135022656 ( Name: stubd, State: RUN, Priority: 02) 0x0804af59 in breakpoint () at i386-stub.c:1184
(gdb)

おー,スレッドIDが-1のへんなスレッドが無くなっている.問題なさそうだ.

この場合の通信内容は以下.

($T054:58f60e08;5:58f60e08;8:59af0408;thread:080c4840;#7e)[+]
[$M8048088,1:55#c2](+)($OK#9a)[+]
[$T080c4840#1f](+)($OK#9a)[+]
[$qfThreadInfo#bb](+)($#00)[+]
[$qL1200000000000000000#50](+)($qM010000000000000000000000000080c4540#97)[+]
[$qL02000000000080c4540#97](+)($qM01000000000080c454000000000080c4840#e2)[+]
[$qL02000000000080c4840#9a](+)($qM01000000000080c484000000000080c4b40#0f)[+]
[$qL02000000000080c4b40#c4](+)($qM01000000000080c4b4000000000080c4e40#3c)[+]
[$qL02000000000080c4e40#c7](+)($qM01000000000080c4e4000000000080c5140#0c)[+]
[$qL02000000000080c5140#94](+)($qM01000000000080c514000000000080c5440#dc)[+]
[$qL02000000000080c5440#97](+)($qM01000000000080c544000000000080c5740#e2)[+]
[$qL02000000000080c5740#9a](+)($qM00100000000080c5740#9a)[+]
[$qThreadExtraInfo,80c5740#20](+)($#00)[+]
[$qP0000001f00000000080c5740#c3](+)($QP0000001f00000000080c5740000000011000000000080c5740000000020110000000403SLP0000000805httpd000000100209#17)[+]
[$Hg80c5740#4a](+)($OK#9a)[+]
[$g#67](+)($0000000000000000ea04000040570c08fcc5110818c61108ecc7110800000000abee050802020000330000003b0000003b0000003b0000003b0000001b000000#43)[+]
[$qP0000001f00000000080c5440#c0](+)($QP0000001f00000000080c5440000000011000000000080c5440000000020110000000403SLP0000000807telnetd000000100208#de)[+]
[$Hg80c5440#47](+)($OK#9a)[+]
[$g#67](+)($0000000000000000ea04000040540c08fc35110818361108ec37110800000000abee050802020000330000003b0000003b0000003b0000003b0000001b000000#b0)[+]
[$qP0000001f00000000080c5140#bd](+)($QP0000001f00000000080c5140000000011000000000080c5140000000020110000000403SLP0000000805clock000000100207#f1)[+]
[$Hg80c5140#44](+)($OK#9a)[+]
[$g#67](+)($0000000098b00d08ea04000040510c083ca7100858a71008eca7100800000000abee050806020000330000003b0000003b0000003b0000003b0000001b000000#8b)[+]
[$qP0000001f00000000080c4e40#f0](+)($QP0000001f00000000080c4e40000000011000000000080c4e40000000020110000000403RUN0000000804idle00000010021f#1e)[+]
[$Hg80c4e40#77](+)($OK#9a)[+]
[$g#67](+)($0400000004000000ffffffff404e0c088c171008b8171008ec171008000000004f09060813020000330000003b0000003b0000003b0000003b0000001b000000#a4)[+]
[$qP0000001f00000000080c4b40#ed](+)($QP0000001f00000000080c4b40000000011000000000080c4b40000000020110000000403SLP0000000806outlog000000100203#dc)[+]
[$Hg80c4b40#74](+)($OK#9a)[+]
[$g#67](+)($0000000060800f08ea040000404b0c083c870f0858870f08ec870f0800000000abee050806020000330000003b0000003b0000003b0000003b0000001b000000#ac)[+]
[$qP0000001f00000000080c4540#c0](+)($QP0000001f00000000080c4540000000011000000000080c4540000000020110000000403SLP0000000807extintr000000100201#f5)[+]
[$Hg80c4540#47](+)($OK#9a)[+]
[$g#67](+)($0000000006000000ea04000040450c086c660e0888660e08ec670e0800000000abee050806020000330000003b0000003b0000003b0000003b0000001b000000#34)[+]
[$qP0000001f00000000080c4840#c3](+)($QP0000001f00000000080c4840000000011000000000080c4840000000020110000000403RUN0000000805stubd000000100202#14)[+]
[$Hg80c4840#4a](+)($OK#9a)[+]
[$g#67](+)($00880d0800000000ea04000040480c0858f60e0858f60e08ecf70e080000000059af040802020000330000003b0000003b0000003b0000003b0000001b000000#81)[+]
[$T080c5740#1f](+)($OK#9a)[+]
[$T080c5440#1c](+)($OK#9a)[+]
[$T080c5140#19](+)($OK#9a)[+]
[$T080c4e40#4c](+)($OK#9a)[+]
[$T080c4b40#49](+)($OK#9a)[+]
[$T080c4540#1c](+)($OK#9a)[+]
[$T080c4840#1f](+)($OK#9a)[+]
[$qL12000000000080c5740#9b](+)($qM00100000000080c5740#9a)[+]
[$qP0000001f00000000080c5740#c3](+)($QP0000001f00000000080c5740000000011000000000080c5740000000020110000000403SLP0000000805httpd000000100209#17)[+]
[$Hg80c5740#4a](+)($OK#9a)[+]
[$g#67](+)($0000000000000000ea04000040570c08fcc5110818c61108ecc7110800000000abee050802020000330000003b0000003b0000003b0000003b0000001b000000#43)[+]
[$qP0000001f00000000080c5440#c0](+)($QP0000001f00000000080c5440000000011000000000080c5440000000020110000000403SLP0000000807telnetd000000100208#de)[+]
[$Hg80c5440#47](+)($OK#9a)[+]
[$g#67](+)($0000000000000000ea04000040540c08fc35110818361108ec37110800000000abee050802020000330000003b0000003b0000003b0000003b0000001b000000#b0)[+]
[$qP0000001f00000000080c5140#bd](+)($QP0000001f00000000080c5140000000011000000000080c5140000000020110000000403SLP0000000805clock000000100207#f1)[+]
[$Hg80c5140#44](+)($OK#9a)[+]
[$g#67](+)($0000000098b00d08ea04000040510c083ca7100858a71008eca7100800000000abee050806020000330000003b0000003b0000003b0000003b0000001b000000#8b)[+]
[$qP0000001f00000000080c4e40#f0](+)($QP0000001f00000000080c4e40000000011000000000080c4e40000000020110000000403RUN0000000804idle00000010021f#1e)[+]
[$Hg80c4e40#77](+)($OK#9a)[+]
[$g#67](+)($0400000004000000ffffffff404e0c088c171008b8171008ec171008000000004f09060813020000330000003b0000003b0000003b0000003b0000001b000000#a4)[+]
[$qP0000001f00000000080c4b40#ed](+)($QP0000001f00000000080c4b40000000011000000000080c4b40000000020110000000403SLP0000000806outlog000000100203#dc)[+]
[$Hg80c4b40#74](+)($OK#9a)[+]
[$g#67](+)($0000000060800f08ea040000404b0c083c870f0858870f08ec870f0800000000abee050806020000330000003b0000003b0000003b0000003b0000001b000000#ac)[+]
[$qP0000001f00000000080c4540#c0](+)($QP0000001f00000000080c4540000000011000000000080c4540000000020110000000403SLP0000000807extintr000000100201#f5)[+]
[$Hg80c4540#47](+)($OK#9a)[+]
[$g#67](+)($0000000006000000ea04000040450c086c660e0888660e08ec670e0800000000abee050806020000330000003b0000003b0000003b0000003b0000001b000000#34)[+]
[$qP0000001f00000000080c4840#c3](+)($QP0000001f00000000080c4840000000011000000000080c4840000000020110000000403RUN0000000805stubd000000100202#14)[+]
[$Hg80c4840#4a](+)($OK#9a)[+]
[$g#67](+)($00880d0800000000ea04000040480c0858f60e0858f60e08ecf70e080000000059af040802020000330000003b0000003b0000003b0000003b0000001b000000#81)[+]
(この状態で停止)

qLコマンドの最後が

[$qL02000000000080c5740#9a](+)($qM00100000000080c5740#9a)[+]

となっていて,スレッド数がゼロ,doneフラグが立ったものが返っている.あとよく見るとわかるのだけど,前回まではあった「Hg-1」が今度は無くなっている.またスレッドIDを0xffffffff で qP コマンドが発行されることも無くなっている.うん,問題無しだ.
(注意)このブログは本家のほうの文章部分のみの転載です.ソースコードの配布,画像などについては本家のほうを参照してください.文章中のリンク先は面倒なのですべて本家のほうに変換してしまっているのでご注意ください.

前回までで info threads によるスレッド情報表示ができるようになった.次は thread コマンドによるスレッド切替えだ.ついでに前回問題になった,info threads を2回行うと固まるというバグを修正したい.

まずgdb上で info threads を行うと

(gdb) info threads
7 Thread 135026304 ( Name: httpd, State: SLP, Priority: 09) 0x0805edeb in kill ()
6 Thread 135025536 ( Name: telnetd, State: SLP, Priority: 08) 0x0805edeb in kill
()
5 Thread 135024768 ( Name: clock, State: SLP, Priority: 07) 0x0805edeb in kill ()
4 Thread 135024000 ( Name: idle, State: RUN, Priority: 1f) 0x0806088f in select ()
3 Thread 135023232 ( Name: outlog, State: SLP, Priority: 03) 0x0805edeb in kill ()
* 2 Thread 135022464 ( Name: stubd, State: RUN, Priority: 02) breakpoint ()
at i386-stub.c:1158
1 Thread 135021696 ( Name: extintr, State: SLP, Priority: 01) 0x0805edeb in kill
()
(gdb)

のようにしてスレッド情報が表示されるが,このとき一番左に表示されている1~7の数値が,スレッド番号になる.gdb上でスレッド操作する場合には,この番号によってスレッドを指定する.

たとえばここで

(gdb) thread 3

とかを行うと,カレントスレッドをスレッド番号3である outlog スレッドに切替える.で,where とかやると,そのスレッドのスタックの状態が見れることになる.

で,前回の実装でためしに thread コマンドによるスレッド切替えを実行してみたら,以下のようになった.

(gdb) thread 3
Thread ID 3 has terminated.

(gdb)

このときgdbとスタブの間では,以下の通信が行われている.

[$T080c4a80#4c](+)($#00)[+]

Tコマンドというのがgdbから送られてきているが,現状のスタブでは実装されていないので「$#00」を返している.で,スレッド切替えに失敗しているようだ.ということで,Tコマンドに対して応答するように修正すればいいようだ.

で,いつもどおりremote.cを調べてみる.「'T'」で検索してもそれっぽい部分が見つからなかったので,「"T」で検索したら以下の部分が見つかった.

static int
remote_thread_alive (ptid_t ptid)
{
struct remote_state *rs = get_remote_state ();
int tid = PIDGET (ptid);

if (tid < 0)
xsnprintf (rs->buf, get_remote_packet_size (), "T-%08x", -tid);
else
xsnprintf (rs->buf, get_remote_packet_size (), "T%08x", tid);
putpkt (rs->buf);
getpkt (&rs->buf, &rs->buf_size, 0);
return (rs->buf[0] == 'O' && rs->buf[1] == 'K');
}

関数名から察するに,スレッドの存在を調べているようだ.見たところ,スレッドが存在する場合には"OK"を返せばいいようなのでそーいうふうに実装してみよう.

remote.c を読んだ感じだと,スレッドの存在/消去は適当にremote_thread_alive() が呼ばれることで検出されるようだ.消滅したスレッドは「T」コマンドでOK以外が返ることになり,その場合にはスレッドは消滅したと判断して削除する,という処理になっているようだ.

さらに今回,カレントスレッドの切替えを追っていて気がついたのだが,シグナル発生時のスタブ側のシグナル送信処理(handle_exception()の先頭付近)で,Tコマンドと一緒にスレッド番号を送信することで,カレントスレッドを gdb 側に伝えることができるようだ(さらに新規スレッドの場合には,[New スレッドID]のように表示される).ここで言うTコマンドと言うのは,上で説明したgdb側からスレッドの存在確認のためにgdbから送られてくるTコマンドではなく,ブレーク時などにスタブ側から送信されるものだ(第16回参照).今回はTコマンドがgdb→スタブのものとスタブ→gdbのものの2種類が登場するので,混同しないように注意してほしい.

gdb側がウエイト状態(スタブのブレーク待ちで,gdbプロンプト未表示状態)では,remote.c の remote_wait() によってブレーク待ちとなっている.で,スタブからTコマンドが送られてくるとブレークが発生したと判断してgdbの処理が始まるのだが,ここで

if (strncmp (p, "thread", p1 - p) == 0)
{
p_temp = unpack_varlen_hex (++p1, &thread_num);
record_currthread (thread_num);
p = p_temp;
}

のようにして,"thread" という文字列を見ている部分がある."thread" に続いてスレッドIDを送ることで,カレントスレッドを教えることができるようなのだ.

さらにこの際,record_currthread() という関数が呼ばれている.record_currthread() は以下のようになっている.

static void
record_currthread (int currthread)
{
general_thread = currthread;

/* If this is a new thread, add it to GDB's thread list.
If we leave it up to WFI to do this, bad things will happen. */
if (!in_thread_list (pid_to_ptid (currthread)))
{
add_thread (pid_to_ptid (currthread));
ui_out_text (uiout, "[New ");
ui_out_text (uiout, target_pid_to_str (pid_to_ptid (currthread)));
ui_out_text (uiout, "]\n");
}
}

パッと見た感じだと,カレントスレッドを切替えて,さらにそれが新しいスレッド(既存のスレッドのリスト中に存在しない)ならば,[New ...]というメッセージを表示しているようだ.

なので,今回はスタブのブレーク時のTコマンド送信部分に,スレッドIDを格納するような対処も行う.

で,実装したのがこんな感じ.前回からの差分については diff.txt 参照.

まず,

diff -ruN kozos21/i386-stub.c kozos22/i386-stub.c
--- kozos21/i386-stub.c Sat Nov 24 11:35:14 2007
+++ kozos22/i386-stub.c Sat Nov 24 14:39:50 2007
@@ -831,6 +831,11 @@
ptr = mem2hex((char *)&registers[PC], ptr, 4, 0); /* PC */
*ptr++ = ';';

+ strcpy(ptr, "thread:");
+ ptr += 7;
+ ptr = intNToHex(ptr, (int)gen_thread->id, 4);
+ *ptr++ = ';';
+
*ptr = '\0';

putpacket (remcomOutBuffer);

という部分で,スタブのブレーク時にTコマンドを発行する際に,レジスタ情報をいくつかパラメータとして追加した後,「thread:(スレッドID);」のようなパラメータを追加している.これによりgdb側でカレントスレッドを認識できる.これはスタブからgdbへのTコマンドだ.

さらに,こちらはgdbからスタブに対するTコマンド

#endif
break;

+ case 'T':
+ {
+ int threadid;
+ kz_thread *thp;
+ hexToInt(&ptr, &threadid);
+ thp = (kz_thread *)threadid;
+ if (thp->id) {
+ strcpy (remcomOutBuffer, "OK");
+ } else {
+ strcpy (remcomOutBuffer, "E01");
+ }
+ }
+ break;
+
case 'q':
switch (*ptr++)
{

スタブ側はgdbからTコマンドが送られてきた場合には,そのスレッドの存在の有無を調べてOK or エラーを返している.remote.c を読む限り,エラーは何を返してもよさそうだが,とりあえず E01 を返している.

次に「qP」コマンドに対する修正.前回,スレッドIDが 0xffffffff でqPコマンドが送られてくることの対処だ.

@@ -1029,7 +1048,16 @@
mode = hexToIntN(&ptr, 4);
threadid[0] = hexToIntN(&ptr, 4);
threadid[1] = hexToIntN(&ptr, 4);
- thp = (kz_thread *)threadid[1];
+ if (threadid[1] == 0xffffffff)
+ {
+ /*
+ * 何を返すべきかちょっと不明なので,とりあえず
+ * カレントスレッドを返す.
+ */
+ thp = gen_thread; /* current を返すべきか? 不明... */
+ } else {
+ thp = (kz_thread *)threadid[1];
+ }

ptr = remcomOutBuffer;
*ptr++ = 'Q';
@@ -1041,13 +1069,13 @@
if (mode & TAG_THREADID) {
ptr = intNToHex(ptr, TAG_THREADID, 4); /* mode */
ptr = intNToHex(ptr, 16, 1); /* length */
- ptr = intNToHex(ptr, threadid[0], 4);
- ptr = intNToHex(ptr, threadid[1], 4);
+ ptr = intNToHex(ptr, 0, 4);
+ ptr = intNToHex(ptr, (int)thp->id, 4);
}
if (mode & TAG_EXISTS) {
ptr = intNToHex(ptr, TAG_EXISTS, 4); /* mode */
ptr = intNToHex(ptr, 1, 1); /* length */
- *ptr++ = '1';
+ *ptr++ = thp->id ? '1' : '0';
}
if (mode & TAG_DISPLAY) {
ptr = intNToHex(ptr, TAG_DISPLAY, 4); /* mode */

実は gdb のソースをちょっと追いかけたのだが,どんなときにスレッドIDが0xffffffff として qP が発行されるのかが,ちょっとわからなかった.まあほんとはそんなあまっちょろいこと言ってないでちゃんと調べなければならないのだが,とりあえずカレントスレッドを返してみる.

注意しなければならないのは,「qP」コマンドの応答時には,remote_unpack_thread_info_response()でスレッドIDのチェックをしているので,スレッドIDは送られてきた 0xffffffff をそのまま返して,TAG_THREADID によってカレントスレッドのスレッドIDを返しているという点だ.あとついでに TAG_EXISTS によってスレッドの存在の有無を返す際に,スレッドの存在をちゃんと調べるように修正.

最後に,Hgコマンドに対する修正.

@@ -1098,6 +1126,7 @@
{
if (rev) val = -val;
stub_restore_regs(gen_thread);
+ if (val == -1) val = (int)current;
gen_thread = (kz_thread *)val;
stub_store_regs(gen_thread);
strcpy (remcomOutBuffer, "OK");

実は Hg コマンドによるスレッド切替えがすでに実装されているため,gdb での thread コマンドによるスレッド切替え動作は,スタブ側で今回新規に実装する必要は無い.ただ,Hg に関しても

Hg-1

のようにしてスレッドIDが 0xffffffff で送られてくる場合があるようなので,これもとりあえずカレントスレッドにするように対処してみた.

では,動かしてみよう.いつもどおり実行形式 koz を起動,gdbで接続,continue,Ctrl-Cブレークしてから,info threads を実行してみる.

画像はこちら

[New Thread 135022688] と表示されていて,新しいスレッドが作成されたことが検知できていることに注目.

info threads を実行してみる.

画像はこちら

無事に表示されている.もう一度 info threads を実行してみよう.

画像はこちら

おー,2回繰り返しても問題無くなった.

次に,thread コマンドによってスレッドを切替えてみる.

画像はこちら

で,where によりスタックトレースをとってみよう.

画像はこちら

ふむ,outlog_main() から呼び出されているので,たしかに outlog スレッドのトレースのようだ.こんなふうにして,ブレーク時の各スレッドの情報を見ることができる.当然だけど up コマンドでスタックトレースを追いかければ,関数呼び出しの階層を追うことができる.ソースコードももちろんその都度表示される.

ちなみにブレークポイントもスレッド単位で設定できて,たとえば

(gdb) break func thread 3

みたいに設定すれば,そのブレークポイントはスレッド番号3のスレッドのときのみ,反応する.これはどんなふうに実現されているかというと,
  • 通常通り,ブレークポイントを設定する.
  • ブレーク時にgdbはカレントスレッドを聞きにいき,当該のスレッドならばブレークする.
  • 当該のスレッドでないならば,そのブレークは無視して,動作続行する.
というような処理が行われる.まあ実際には指定されたスレッドでない場合にも,CPUはトラップ命令を実行してブレークが発生しているのだけど,gdbがうまく無視して先に進めてくれているわけだ.このへんの処理は,gdbとスタブの通信内容を見てみるとgdbが実によろしくやってくれていることがわかり,非常に面白い.

ちょっと試してみよう.まず,ブレークポイントの設定.

(gdb) break dummy_func thread 3
Breakpoint 1 at 0x804b11e: file telnetd.c, line 21.
(gdb)

第16回で説明したことでちょっと忘れているかもしれないけど,telnet で接続して call を実行することで,dummy_func() というダミー関数が呼ばれるようになっている.で,dummy_func() に対してスレッド番号3(outlogスレッド)でブレークポイントを張ってみた.

実際に telnet から call を実行してみよう.

% telnet 192.168.0.3 20001
Trying 192.168.0.3...
Connected to 192.168.0.3.
Escape character is '^]'.
> call
OK
>

ブレークポイントが張ってある dummy_func() が呼ばれたにもかかわらず,ブレークせずに正常終了している.このときのgdbとスタブの通信は以下.

...
Sat Nov 24 14:08:43 2007
Sat Nov 24 14:08:44 2007
Sat Nov 24 14:08:45 2007
($T054:ec561208;5:f4561208;8:1fb10408;thread:080c4260;#31)[+]
[$g#67](+)($00000000477a0a080000000060420c08ec561208f4561208ec571208000000001fb1040812020000330000003b0000003b0000003b0000003b0000001b000000#98)[+]
[$P8=1eb10408#ba](+)($OK#9a)[+]
[$M8048088,1:55#c2](+)($OK#9a)[+]
[$M804b11e,1:c7#43](+)($OK#9a)[+]
[$Hc80c4260#42](+)($#00)[+]
[$s#73](+)($T054:ec561208;5:f4561208;8:25b10408;thread:080c4260;#01)[+]
[$m8048088,1#3e](+)($55#6a)[+]
[$M8048088,1:cc#1e](+)($OK#9a)[+]
[$m804b11e,1#8f](+)($c7#9a)[+]
[$M804b11e,1:cc#6f](+)($OK#9a)[+]
[$Hc0#db](+)($#00)[+]
[$c#63](+)Sat Nov 24 14:08:47 2007
Sat Nov 24 14:08:48 2007
Sat Nov 24 14:08:49 2007
Sat Nov 24 14:08:50 2007
...

まず最初に T コマンドが発行されているので,ブレークしているのは確かだ.ただその後,以下の動作が行われているようだ.
  • Mコマンドによりブレークポイントを元に戻す.
  • sコマンドによりステップ実行で処理をちょい先に進める.
  • Mコマンドにより再度ブレークポイントを設定する.
  • cコマンドにより continue する.
つまり,ブレークポイントを無視して continue しているのである.

ステップ実行で処理を進めるのは,continue の直後に同じ場所でまたブレークしないようにするためのおなじみの動作だ.このへんについては第16回を参照.

うーん,さすがgdb.こんなことまでしてくれるんだねえ...ちょっと感動.