2005/05/20 追加 (7.0.0 8.0.0)
2005/05/07 更新
2005/05/05
ここでは Plan 9 のパイプの機能を紹介する。
プログラムからの使い方は通常の Unix の方法がそのまま使えるが、BSD系のような片方向のパイプではなく、SYSTEM-V 系の双方向パイプである。その仕組みから言えば Plan 9 のパイプは Unix のパイプとかなり異なる。パイプもまたファイル指向である。そのためにシェルからも容易にパイプを生成し利用できる。以下にシェルを使って簡単に実験できるパイプの使い方を紹介する。
term% cd /tmp term% mkdir X term% bind '#|' X term% ls X X/data X/data1 term%この中に現れる '#|' はカーネルが提供するパイプである。これはプロセス毎に提供されている。これを適当なディレクトリ X に bind すると、X を通じてパイプを利用できる。X/data と X/data1 は完全に等価である。X/data への書き込みは X/data1 から読み取れる。逆に X/data1 への書き込みは X/data から読み取れる。
この例では X をわざわざ作成しているが、既存のものを用いても構わない。既存のものとしては /n/temp が適当であろう。ここに bind しても、名前空間の異なる他のプロセスには影響を与えない。
term% window -mを実行する。するとウィンドウが生成される。サイズや生成位置を指定したい場合にはマニュアルを見るが良い。生成されたウィンドウで ls X を実行してみよう。
term% ls X X/data X/data1 term%となるはずである。window コマンドの -m オプションによって、このコマンドを打ち込んだウィンドウと生成されたウィンドウの環境が保たれようとする。特にワーキングディレクトリと名前空間が保たれている。読者はこのオプションが無い場合と比較するが良い。
cat > X/dataを実行し、他方のウィンドウで
cat X/data1を実行してみるがよい。すると例えば次のようになる。
term% cat > X/data alice bob carol term%carol まで入力し ctl-D を打つ。
term% cat X/data1 alice bob carol term%先に述べたように data と data1 は等価である。2つのウィンドウの関係もそうである。読者は自ら確認してみるがよい。
window -mを実行すれば相互通信可能な合計4つのウィンドウができあがる。これらを A0, A1, B0, B1 としよう。各々のウィンドウで
cat > X/dataウィンドウ A1:
cat X/dataウィンドウ B0:
cat > X/data1ウィンドウ B1:
cat X/data1を実行しておく。
読者は A0 で打ち込んだデータが B1 で表示され、B0 で打ち込んだデータが A1 で表示されるのが確認できるであろう。
もう1つウィンドウを生成する。普通に行うようにマウスで適当に生成してもよいし、-m オプションなしの window コマンドを使ってもよい。生成されたウィンドウ(これを C とする)で /tmp に移動し
ls Xを実行してみる。X/data や X/data1 は存在しないのが分かるはずである。
そこで、さらに
bind '#|' Xを実行する。
cat X/data1を実行し、A0 でデータを打ち込んでみる。もはや C は A0 からのデータを受け取れないことが確認できるであろう。つまりプロセス間通信の秘密は保たれ、また邪魔もされない。UNIX の名前付きパイプとの根本的な違いである。
cat X/dataウィンドウBで
cat >X/data1を実行すれば、他の(名前空間を共有する)ウィンドウで
echo 'blah blah' >X/data1としてもパイプは閉じない。このメカニズムは何かに応用できるかもしれない。
他のディレクトリ、例えば Y に bind を実行すれば X とは独立なパイプが生成される。つまり bind によって新しいキャネルを作成してくれているのである。読者は実際にそのことを確認してみるが良い。
Plan 9 のドキュメントや bind の使用例を見ていると、bind は名前空間の部分木の張り付けを行っているかのような印象を受けるが、カーネルディバイス '#|' の bind に関する限り
bind '#|' Xは単に '#|' の別名 X を作成しているのではない事がこの実験から分かる。実際、直接
cat '#|/data'と
cat >'#|/data1'を実行しても通信はできない。
tr a-z A-Z <X/data >X/dataウィンドウB
cat >X/data1ウィンドウC
cat X/data1ウィンドウD
con -C $home/tmp/X/data1
もしもウィンドウ A0, A1 が動くコンピュータと、ウィンドウ B0, B1 が動くコンピュータが異なったらどうか? 異なるコピュータを結ぶチャットが実現するのである。チャットの相手はアメリカの友人かもしれない。素晴らしいではないか?
ここでは Plan 9 に標準的に含まれているツールだけを使ってチャット環境をどこまで構築できるか考えてみる。
以下ではアクセスされるのを待っているコンピュータをサーバー、他方のコンピュータをクライアントと呼ぶ。
netstat -nを実行する事によって知る事ができる。
term% netstat -n ... tcp 27 arisawa Listen 445 0 :: tcp 28 arisawa Established 24467 564 204.178.31.8 ... term%第5フィールド(この例では 445 や 24467)が現在使用されているポートである。第6フィールドは接続相手方のポートであり、0のものは接続を待っている状態である。接続中であれば相手の IP アドレスが表示されている。
aux/listen1 [-tv] addr cmd args ...である。addr は
'tcp!*!9000'のように指定する。この例では TCP の 9000 ポートからのメッセージを聞いている。この中のアステリスク(*) は listen1 を起動したコンピュータが持っている全ての IP アドレスについて聞く事を意味している。
cmd args ...は指定ポートからの接続要求に対して実行されるコマンドと引数である。コマンドは名前ではなくパスで与える。即ち / または ./ で始まる必要がある。
listen1 は v フラグの下で冗長なメーセージを出すが、この実験ではこのフラグは使わない。t フラグのの下では listen1 が実行された名前空間のままコマンドがが起動される。この事は listen1 を実行したユーザと同じ権限でコマンドが実行される事をも意味している。我々の実験では t フラグが要求されるので、安全な環境で実験して欲しい。
prompt='* ' aux/listen1 -t 'tcp!*!9000' /bin/rc -iすると、UNIX クライアントからは
telnet venus 9000Plan 9 クライアントからは
telnet -r tcp!venus!9000で rc が起動されている事が確認できるが、動作がおかしい。例えば
-bash$ telnet venus 9000 * ls : bad character in file name: 'ls 'のようになる。これは telnet の仕様として、改行の前に CR コードを付加するからであり、そのために rc はクライアントが投入するコマンドを正しく解釈できない。そこで rc にリクエストを渡す前に CR コードを削除する事とし、次のように変更する。即ち、ファイル chat1 を作成し、それをコマンドとして実行する。
#!/bin/rc tr -d 0x0d | {prompt='*' /bin/rc -i}
chat1 の内容。許可ビットを 755 に設定しておく。
その下でサーバ側の実行をaux/listen1 -t 'tcp!*!9000' ./chat1とする。
特に
ls Xで
X/data X/data1が見える事を確認しよう。
まるで telnet でログインしたかのように、クライアント側からサーバに対する任意のコマンドが実行できるのを確認できる。通常の telnet ログインと異なるのは認証が伴わない点だけである。
なおクライアントが接続を終了するには、クライアントが
bind '#|' X aux/listen1 -t 'tcp!*!9000' ./chat1を実行し、さらにサーバ側の2つのウィンドウで
cat X/dataと
cat >X/dataを実行しているとせよ。クライアントは2つの仮想ターミナルで
cat X/data1と
cat >X/dat1を実行する。すると2つのコンピュータ間のチャットが実現する。
* cat X/data1 cat: error reading X/data1: i/o on hungup channel *
今回は2つのポートを使う事にする。クライアントには 9000 とポート 9001 に telnet でアクセスしてもらう事にし、クライアントはポート 9000 を受け取ったメーセージの表示に使い、ポート 9001 に書き込む。この場合のサーバ側での listen1 は次のようになる。
aux/listen 'tcp!:!9000' /bin/cat X/data1 & aux/listen 'tcp!:!9001' /bin/rc -c 'tr -d \x0d>X/data1' &サーバ側は、一方のウィンドウで
cat >X/data他方のウィンドウで
cat X/dataを実行する。
バイプのニーズはユーザが異なる2つのプロセス間だけではなく、分散配置されている2つの異なるコンピュータでも発生する。Plan 9 の /srv ディレクトリはそうした問題を解決するために存在する。後に見るように /srv は正確にはパイプではなく、単にファイル記述子を渡す仕組みである。機能的にはパッファの無いパイプとも考えられる。
/srv は C プログラムで扱うのが普通である。C を使わないで /srv に関する面白い実験を見つけるのは難しい。以下の /srv に関する記事は Mike Haertel の 9fans への投稿をヒントにしている。この投稿の内容は http://tinyurl.com/arnnd にアクセスすればわかる。
cat | echo 0 >/srv/foo他のウインドウ B で
cat /srv/fooを実行する。
ウィンドウ A で入力したデータがウィンドウ B で表示されるのを確認する。
echo 0 >/srv/foo他のウインドウ B で
cat /srv/fooを実行する。
ウィンドウ A で入力したデータがウィンドウ B で表示されることもあれば、シェルコマンドとして認識される事もあるのを確認する。
rm /srv/fooで削除できる。
/srv/foo をオープンしたプロセスはファイル記述子をカーネルから渡される。このファイル記述子が指しているファイルの実体は /srv/foo の中に書き込まれているファイル記述子が指している実体と同じものである。(この事は後の実験で示される。)
echo 0 >/srv/fooで、このウィンドウのファイル記述子0のファイル(標準入力)が、この後に /srv/foo を開くプロセスにパイプされる。それはウィンドウBで実行した
cat /srv/fooである。
cat | echo 0 >/srv/fooは標準入力が2重に読み取られる問題を巧みに避けている。
cat |を添える事によって/srv/foo が生成された時の標準入力を cat の出力に限定しているのである。これを考えついたやつは凄い。
Plan 9 のパイプは双方向パイプなので con を生かした実験をしてみよう。1つのウインドウで
prompt='* ' rc -i <[0=1] >[2=1]| echo 0 >/srv/fooあるいは
prompt='* ' rc -i >[0=1] >[2=1] | echo 0 >/srv/fooを実行する。この中に現れる <[0=1] も >[0=1] もファイル記述子1の内容をファイル記述子0の所にコピーする*。これによって I/O が切り替わる。>[2=1] も同様に考えれば良い。
他のウィンドウで
con -C /srv/fooを実行すると
*のプロンプトが表示される。ここで ls を打ち込んでみよう。ファイル一覧が表示されるはずである。con の -C はローカルエコーを行うフラグである。これがないと打ち込んだコマンドが表示されない。
srv tcp!venus!9000 fooここに venus はアクセスするサーバの名称であり、9000 はそのポートである。これによって /srv/foo が作成される。
srv コマンドを実行すると
post...のメッセージが出てくる。これは /srv/foo にファイル記述子が書き込まれた事を意味している。
サーバ venus ではリスナーが 9000 ポートを監視していなくてはならない。でないと
srv: dial tcp!venus!9000: connection refusedと言われるであろう。そこで実験のために venus で listen1 を使って
term% prompt='* ' aux/listen1 -t 'tcp!*!9000' /bin/rc -iを実行して srv コマンドの実験をする事にしよう。
クライアントは
term% srv tcp!venus!9000 foo term% con -C /srv/fooを実行する。すると
*のプロンプトが出て、任意の rc コマンドが con の下に実行できる事が分かる。
我々は良く似た事を telnet を使って行った。telnet の実験と比較するとサーバ側で CR コードの削除の必要性がなくなった。srv と con の組み合わせは、改行コードの前に CR コードを付加する事無しに、率直にサーバとデータを交換する。
netstat -nを実行すると、例えば(筆者の場合には)
term% netstat -n ... tcp 32 arisawa Listen 9000 0 :: tcp 33 network Closed 14067 9000 192.168.1.2 tcp 34 arisawa Established 14068 9000 192.168.1.2 tcp 35 network Established 9000 14068 192.168.1.2 ... term%この実験ではサーバとクライアントが同一のマシンである。第4フィールドの Closed の行は古い実験の残り滓である。14068 がクライアントのポートであり、クライアントは /net/tcp/34 を、サーバは /net/tcp/35 を使っている事が分かる。
また
term% ps ... arisawa 1995 0:00 0:00 60K Open listen1 arisawa 1999 0:00 0:00 248K Await rc arisawa 2000 0:00 0:00 52K Pread con arisawa 2001 0:00 0:00 52K Pread con ...から con の実行に2つのプロセスが使われており、各々の pid は 2000 と 2001 である事が分かる。各々、キーボードからのデータ待ちとサーバからのデータ待ちのはずである。各プロセスが使用しているファイル記述子とその実体は次のように判明する。
term% cat /proc/2000/fd /usr/arisawa 0 r M 66 (0000000000000001 0 00) 8192 1777 /dev/cons 1 w M 66 (0000000000000001 0 00) 8192 112344 /dev/cons 2 w M 66 (0000000000000001 0 00) 8192 112344 /dev/cons 3 rw I 0 (000000000002044d 0 00) 0 21760 /net/tcp/34/data term% cat /proc/2001/fd /usr/arisawa 0 r M 66 (0000000000000001 0 00) 8192 1795 /dev/cons 1 w M 66 (0000000000000001 0 00) 8192 112618 /dev/cons 2 w M 66 (0000000000000001 0 00) 8192 112618 /dev/cons 3 rw I 0 (000000000002044d 0 00) 0 22052 /net/tcp/34/data term%con は /srv/foo をオープンした時にファイル記述子 3 を受け取ったが、その実体は /net/tcp/34/data である事がここから分かる。
srv -e command srvnameのように使う。command には任意の rc コマンドを与える事ができ、srvname は /srv の中に作成するファイル名である。これによって他のプログラムは /srv/srvname にアクセスする事によって command と会話する事が可能になる。
例を挙げよう。
srv -e 'prompt=''* '' >[2=1] rc -i' fooこれは
prompt='* ' rc -i >[0=1] >[2=1] | echo 0 >/srv/fooと内容的に等価である。
これを実行し、他のウィンドウで
con -C /srv/fooを実行してみよう。すると
*のプロンプトが表示され、rc と会話できる事が分かる。
srv コマンドの -e オプシヨンが何時から付加されたか筆者は知らない。Bell-labs の過去の Plan 9 のソースは sources.plan9.bell-labs.com で見る事ができる*。2002/12/12 のソースが最も古い。Mike Haertel の 9fans の投稿は 2002/05/01 なので、この投稿に触発された可能性が高い。
srv -e 'tr a-z A-Z' fooそして他のウインドウで
con /srv/fooさらに他のウィンドウでも
con /srv/fooを動かす。con のウィンドウで alice を打ち込んでみる。その結果の ALICE は必ずしも同じウィンドウの con で受け取られるわけではない事が分かる。
この結果は /srv/foo を使うプログラムが複数になる場合には何らかの統制が必要である事を意味している*。つまり1つのプロセスだけが /srv/foo を使うようにユーザ側で気をつけるか、読み書きに対して排他制御を行うかである。
既に見たように /srv の中の名前、例えば /srv/foo は名前の付いたパイプのように振る舞う。その片方の端は /srv/foo を生成したプロセス P に繋がっている。mount は
mount /srv/foo /n/fooのように適当なディレクトリ /n/foo にプロセス P のサービスをファイルとして見せる口を作る。このように旨く働くためには、もちろん、P のプログラムは mount を許すための必要な形式を踏まえなくてはならない。このような形式を備えたプログラムをシェルスクリプトのように複数のコマンドを組み合わせて作成する事は難しく、C 言語や Python などに頼らざるを得ないと思う。
u9fs は UNIX のリスナー(inetd や xinetd) の下でサービスを実行するものとしてデザインされていた。しかしリスナーの下で実行すると UNIX システムの管理者権限が必要となる。筆者はユーザレベルのリスナーを作成し、この問題をクリアしてきたが、いずれにせよリスナーが必要であると誰もが思い込んでいた。Mike Haertel の投稿までは...
彼の投稿は
% ssh myname@remotehost u9fs -a none -u myname <[0=1] | echo 0 > /srv/remotehost % mount /srv/remotehost /n/kremvaxであるが、現在の srv の仕様では
% srv -e 'ssh myname@remotehost u9fs -a none -u myname' remotehost % mount /srv/remotehost /n/kremvaxの方が率直である事を読者は理解するであろう*。
srvssh myname@remotehost mount /srv/remotehost /n/kremvax
term% cd tmp term% bind '#|' X term% window -m
term% cpu ar% ls /mnt/term/usr/arisawa/tmp/X /mnt/term/usr/arisawa/tmp/X/data /mnt/term/usr/arisawa/tmp/X/data1 ar% con /mnt/term/usr/arisawa/tmp/X/datacpu が引数無しに起動されると環境変数 cpu で指定されたサーバにアクセスする。ここでは ar がサーバである。
term% con /usr/arisawa/tmp/X/data1
ctl-\つまりコントロールキーを押しながら '\' キーを打つ。すると
>>>が表示され、ここで 'q' を打つ。
ar% con /mnt/term/usr/arisawa/tmp/X/data bob >>> q ar%
rx host command arg ... cpu -h host -c commans arg ...である。
cpu はいかにも Plan 9 らしい高度な機能をユーザに提供してくれる。すなわち
ここではリモートで実行されるプログラムを端末側からプログラムによって制御することを想定し*、それを実現するのに役立ちそうな実験をしてみる事にする。
以下の実験では端末側に 2 つのウィンドウが登場する。
erm% srv -e 'rx ar tr a-z A-Z' foo post... term%ウィンドウ B
term% con /srv/foo ALICE小文字の alice を打ち込んで、リモートホスト ar で実行される tr によって ALICE が表示されているのである。
erm% srv -e 'cpu -h ar -c tr a-z A-Z' foo post... term%でも良さそうに思われるが実際には旨く行かない。原因は cpu コマンドは標準入出力を端末の /dev/cons に割り振るからである。この親切はこのケースにおいてはアダになる。
cpu コマンドがよけいな事をしないように改造する考えもあろうが、次のようにすれば問題を回避できる。
ウィンドウ A で
term% bind '#|' /n/temp; bind /n/temp/data /dev/cons; term% window -m term% cpu -c tr a-z A-Z & term%ウィンドウ B で
term% con /n/temp/data1 ALICE小文字の alice を打ち込んで、リモートホスト ar で実行される tr によって ALICE が表示されているのである。