timeで見るspawn/channel
time(1)はUNIXでもおなじみだが、引数に与えられたコマンドの実行時間を測定するのに使われる。
; time sh -c 'ps | grep Sh'
1 1 oraccha 0:00.0 release 74K Sh[$Sys]
81 1 oraccha 0:00.0 release 73K Sh[$Sys]
0l 0.011r 0.011t今回はtime(1)を例にスレッドのspawnとチャネル(channel)によるスレッド間通信について書きたいと思う*1。InfernoはUNIX/Plan9とは異なり、複数スレッド(プロセス)が単一のアドレス空間を共有するOSなので、fork&execは存在しない。新しいコマンドを実行する場合は、モジュールをloadして、スレッドをspawnする手順になる。
LimboのチャネルはHoareのCSP (Communicating Sequential Process)に大きな影響を受けたスレッド間通信手段である。スレッドはチャネルを介してメッセージを同期的に送受信する。使われる構文は、以前書いたNewSqueakとほとんど同じで(「Newsqueak (2): チャネル」)、チャネルが演算子"<-"の左辺値ならば受信、右辺値ならば送信である。
implement Time;
include "sys.m";
include "draw.m";
include "sh.m";
FD: import Sys;
Context: import Draw;
Time: module
{
init: fn(ctxt: ref Context, argv: list of string);
};
sys: Sys;
stderr, waitfd: ref FD;
init(ctxt: ref Context, argv: list of string)
{
sys = load Sys Sys->PATH;
stderr = sys->fildes(2);
waitfd = sys->open("#p/"+string sys->pctl(0, nil)+"/wait", sys->OREAD);
if(waitfd == nil){
sys->fprint(stderr, "time: open wait: %r\n");
return;
}sys->pctlの戻り値は自分のスレッドIDである。"#p/n/wait"は"/prog/n/wait"にmountされているが*2、子スレッドの終了を待ったり、エラーを知るために使われるread onlyのファイルである。後のwaitfor関数で使う。
argv = tl argv;
if(argv == nil) {
sys->fprint(stderr, "usage: time cmd ...\n");
return;
}
file := hd argv;
if(len file<4 || file[len file-4:]!=".dis")
file += ".dis";引数からロードするファイル名(*.dis)を生成している。「len 文字列」は文字列の長さを返す。文字列、配列はスライス([m:n])が使える。
t0 := sys->millisec();
c := load Command file;
if(c == nil) {
err := sys->sprint("%r");
if(1){
c = load Command "/dis/"+file;
if(c == nil)
err = sys->sprint("%r");
}
if(c == nil) {
sys->fprint(stderr, "time: %s: %s\n", hd argv, err);
return;
}
}「load Command ファイル名」でコマンドをloadする。CommandはShモジュールで定義されている。sprint関数で使われる"%r"はエラーメッセージに展開される。
t1 := sys->millisec(); pidc := chan of int; spawn cmd(ctxt, c, pidc, argv); waitfor(<-pidc);
ここからが肝心な箇所。まず、チャネル(pidc)を作り、cmdをspawnしている。spawnはPOSIXスレッドのpthread_createのイメージ。チャネルは何のために使われるかというと、子スレッド(cmd)の終了を待ち合わせるためである。"<-pidc"と書くことで、親スレッドの実行は(子スレッドから)チャネルに書き込みがあるまでブロックされる。
t2 := sys->millisec();
f1 := real (t1 - t0) /1000.;
f2 := real (t2 - t1) /1000.;
sys->fprint(stderr, "%.4gl %.4gr %.4gt\n", f1, f2, f1+f2);
}
cmd(ctxt: ref Context, c: Command, pidc: chan of int, argv: list of string)
{
pidc <-= sys->pctl(0, nil);
c->init(ctxt, argv);
}cmd関数の一行目でチャネルに子スレッドIDを送信している。これで親スレッドはwaitfor関数の実行を開始する。init関数は前回のcatで説明した通りで、ここからtimeの引数で与えられたコマンドの実行が始まる。
waitfor(pid: int)
{
buf := array[sys->WAITLEN] of byte;
status := "";
for(;;){
n := sys->read(waitfd, buf, len buf);
if(n < 0) {
sys->fprint(stderr, "sh: read wait: %r\n");
return;
}
status = string buf[0:n];
if(status[len status-1] != ':')
sys->fprint(stderr, "%s\n", status);
who := int status;
if(who != 0) {
if(who == pid)
return;
}
}
}read(waitfd)はUNIX/Plan9のwaitpidだと思えばよい。子スレッドが終了するとスレッドIDとエラーメッセージがreadできる。プロセスに対する/progインタフェースの利用はより徹底されているようだ。
あと、"n := read(fd, buf, len buf); s = string buf[0:n];" というのはイディオム。
まとめ。
- チャネルによってスレッドの待ち合わせが簡単に書ける。
- スレッド(プロセス)のファイルインタフェースの利用はPlan9より徹底している。
*1:InfernoではLimboスレッド、Infernoカーネルプロセスという用語が使われるが、ユーザから見える(spawnで生成する)のは前者のLimboスレッドで、通常は単にスレッドと呼ばれる。JavaのグリーンスレッドやErlangのプロセスに近い。スレッドスケジューリング方式はラウンドロビンでDis内部のスケジューラが行う。一方、カーネルプロセスはホストOSやネイティブOSが提供するスレッドのことで、disは複数のカーネルプロセスによって実行される。emuでの実装はPOSIXスレッドである。LimboスレッドとInfernoカーネルプロセスはm:nマッピングで、Limboスレッドが複数のカーネルプロセスを渡り歩いて実行されることもある。というようにIPWLには書かれているが、manを読むとLimboプロセス、Limboスレッドが混ざって記載されているなぁ。
*2:/porgは/procのInferno版。