Plan 9は、ほとんどの操作をファイルを通して行います。リソースの参照だけではなく、シグナルの送信*1などカーネルに対して命令を送る場合も、ある程度はファイルを通して行えます。初見では、これらの習慣は馴染みがないと思うので、普段使いそうなものをいくつか紹介します。
Plan 9はUnixよりeverything is a fileを徹底していると言われていますが、Rob Pike氏によるとeverything has the same interfaceとのことです。ファイルというインターフェイスを通して全てのことを行う様子は、以下の例からも理解してもらえるんじゃないでしょうか。
In Plan 9, it's not "everything is a file", it's "everything has the same interface". And that interface is "file *system*". Very different.
— Rob Pike (@rob_pike) 2014年4月30日
それでは、まずは簡単なところから。
コピー・ペースト
カーネルが提供する/dev/snarfを読み書きするだけです。read(2)すると、エディタなどでコピーしたデータを読み出せます。write(2)したデータは、エディタなど他のプログラムから読み出せます。
% echo hello world >/dev/snarf % cat /dev/snarf
CopyではなくSnarfというあまり見ない名前を使っている理由は、データを複製しているわけではないから、だそうです。
プロセス操作
プロセス関連の操作は/proc以下のファイルを扱います。/proc/<pid>/statusでプロセスの状態を確認したり、/proc/<pid>/noteで外部からシグナル(Note)を送ったりできます。
% sleep 100 & % ps | grep sleep glenda 43551 0:00 0:00 8K Sleep sleep % cat /proc/43551/status
シグナル(Note)を送る場合は/proc/<pid>/noteファイルへ文字列を書き込みます。例えばPlan 9のkill(1)はコマンドを出力するだけで、実際にkillするためにはrc(1)へパイプさせる必要があります。
% kill sleep echo kill>/proc/43551/note # sleep % kill sleep | rc
シグナルと異なり、Noteは文字列なので送る側と受け取る側で合意できていれば、文字列の内容はなんでも構いません。プログラム側では書き込まれた文字列が届くので、テキストをみてどう振る舞うかを決めます。
void catchnote(void *, char *msg) { if(strstr(msg, "alarm")) noted(NCONT); else noted(NDFLT); } void main(void) { notify(catch); }
システムが送ってくるNoteはnotify(2)でドキュメント化されています。
プロセスの強制終了
ハンドラの有無に関わらず強制終了させたい場合は/proc/<pid>/ctlにkillと書き込みます。これはUnixのkill -9
に相当します。noteと違って、こちらは決まったコマンドしか受け付けません。
% slay sleep echo kill>/proc/43532/ctl # sleep % slay sleep | rc
GUI関連
スクリーンショット
rioが動作しているなら、/dev/screenファイルがrioによって提供されているので、これをread(2)するとその時点のスクリーン全体に相当するビットマップが読めます。
% cp /dev/screen screen.bmp
/dev/wsys以下にはウインドウ毎のファイルが用意されているので、/dev/wsys/<n>/windowを読むと画像を取得できます。
% cat /dev/winid 2 % lc /dev/wsys 1 2 % cp /dev/wsys/2/window win.bmp
スクロールさせる
rioが提供するターミナル(と呼んでもいいのか定かではないけど)は、コマンドの出力が表示範囲を超えてもデフォルトではスクロールしません。代わりに一定量溢れてしまうとプロセスが一時中断します。マウスの中ボタンメニューからscrollを選んでもいいのですが、/dev/wsys/<n>/wctlにscrollと書き込むと、該当するウィンドウではプログラムの出力に従ってスクロールするようになります。
# winidから自身のwindow idが読める % cat /dev/winid 2 % echo scroll >/dev/wsys/2/wctl
ネットワーク設定
ネットワーク関連の操作は/net以下を扱います。
IPアドレス設定
自身のIPアドレスを設定する場合は、/net/ipifc/cloneにadd ipaddr ipmask
と書き込むことで行います。
% echo add 192.168.1.3 255.255.255.0 >/net/ipifc/clone
静的ルーティングは/net/iprouteにadd ipnet ipmask ipgw
と書きます。
% echo add 192.168.10.0 255.255.255.0 192.168.1.1 >/net/iproute
設定の確認
/net/ndbを読むと、設定されている情報の一部が読めます。ネットワーク関連のプログラムは、このファイルを読んで必要な情報を得ています。
% cat /net/ndb ip=192.168.1.3 ipmask=255.255.255.0 ipgw=192.168.1.1 sys=cpu dom=cpu.local.internal dns=192.168.1.1 ntp=192.168.1.1
/net/iprouteからはルーティングテーブルを読めます。
% cat /net/iproute 0.0.0.0 /96 192.168.1.1 4 none - 192.168.1.0 /120 192.168.1.0 4i ifc - 192.168.1.0 /128 192.168.1.0 4b ifc - 192.168.1.3 /128 192.168.1.3 4u ifc 0 192.168.1.255 /128 192.168.1.255 4b ifc - 127.0.0.0 /104 127.0.0.0 4i ifc - 127.0.0.0 /128 127.0.0.0 4b ifc - 127.0.0.1 /128 127.0.0.1 4u ifc - 127.255.255.255 /128 127.255.255.255 4b ifc - 255.255.255.255 /128 255.255.255.255 4b ifc -
/net/ipselftabはホスト自身だと判断するIPアドレスのテーブルです。受信したパケットの宛先がこのテーブルに該当すると、カーネルは/net/tcpや/net/udp以下のファイルを通してパケットをプロセスへ伝えます。
cpu% cat /net/ipselftab 127.0.0.0 01 6b 192.168.1.0 01 6b 127.0.0.1 01 6u 192.168.1.3 01 6u 127.255.255.255 01 6b 255.255.255.255 02 6b 192.168.1.255 01 6b
ところで普通は、これらの設定はip/ipconfigで行いますが、ip/ipconfigはDHCPなどから構成を取得して、以下のファイルを更新しているだけです。上記以外にも、多くの設定やコマンドが用意されているので、興味があればip(3)を読むと参考になるでしょう。また、パケットルーティングに関連するカーネルの動作は以下の記事にも書きました。
ネットワークプログラミング
Plan 9にはdial(2)やannounce(2)など高レベルの関数が用意されていますが、これらはシステムコールではなく、ただのライブラリ関数です。*2
TCPクライアント
どのようにOSがファイルを扱うか見るため、高レベル関数は使わずに直接/net/tcpや/net/udpなどのディレクトリを扱って通信を行ってみましょう。まずTCPクライアントは、/net/tcp/cloneへconnectコマンドを書き込んで、cloneを開いたまま/net/tcp/<n>/dataを読み書きすることになります。以下のコードはTCPのポート9000へ接続して、サーバからデータを受け取るものです。
#include <u.h> #include <libc.h> void main(void) { int ctl, fd, n, c; char buf[1<<10]; /* ctlを閉じるとコネクションも閉じるので開いたままにする */ ctl = open("/net/tcp/clone", ORDWR); if(ctl < 0) sysfatal("open: %r"); /* 新しく割り当てられたコネクション番号を読む */ n = read(ctl, buf, sizeof(buf)-1); if(n < 0) sysfatal("read: %r"); buf[n] = '\0'; c = atoi(buf); fprint(ctl, "connect 127.1!9000"); snprint(buf, sizeof buf, "/net/tcp/%d/data", c); fd = open(buf, ORDWR); if(fd < 0) sysfatal("open: %r"); n = read(fd, buf, sizeof(buf)-1); if(n < 0) sysfatal("read: %r"); buf[n] = '\0'; print("%s", buf); close(fd); close(ctl); }
/net/tcp/cloneをopen(2)すると、OSによって新しいコネクションのctl(/net/tcp/<n>/ctl)へリダイレクトされたファイルディスクリプタを得ますが、プログラムはまだコネクション番号を知らないので/net/tcp/<n>/dataをopen(2)できません。コネクション番号はctlを読むことにより取得しています。
TCPサーバ
サーバのコードは、Listenするポート番号をannounceコマンドで登録しておきます。/net/tcp/<n>/listenファイルをopen(2)すると、登録したポート番号へ新しい接続があるまでブロックされて、アクセスがあった時にopen(2)から戻ります。このときクライアント毎に新しくコネクションが作成されるので、listenファイルをopen(2)したファイルディスクリプタからコネクション番号を読んでdataファイルを読み書きします。
#include <u.h> #include <libc.h> int netopen(int n, char *name, int mode); int readint(int fd); void main(void) { int ctl, cfd, fd; int c, c1; char buf[1<<10]; ctl = open("/net/tcp/clone", ORDWR); if(ctl < 0) sysfatal("open: %r"); c = readint(ctl); fprint(ctl, "announce 9000"); for(;;){ cfd = netopen(c, "listen", OREAD); /* * クライアントへのコネクション番号を取得する。 * このコードでは1クライアントしか同時に処理できないので、 * 本当はfork(2)した方が良いけどサンプルなので手抜き.... */ c1 = readint(cfd); fd = netopen(c1, "data", OWRITE); fprint(fd, "hello world\n"); close(fd); close(cfd); } close(ctl); } int netopen(int n, char *name, int mode) { int fd; char buf[100]; snprint(buf, sizeof buf, "/net/tcp/%d/%s", n, name); fd = open(buf, mode); if(fd < 0) sysfatal("open: %r"); return fd; } int readint(int fd) { int n; char buf[100]; n = read(fd, buf, sizeof(buf)-1); if(n < 0) sysfatal("read: %r"); buf[n] = '\0'; return atoi(buf); }
サーバの方は少し複雑な動きをしますが、やっていることは一般的なファイルの読み書きだけなので、シェルスクリプトなどでネットワークプログラムも書けますし、他ホストの/netをimportして、それを読み書きすると簡易なプロキシのように振る舞えます。また、プログラム開発などでは、/netと同じインターフェイスを9Pファイルサーバとして実装して、bind(1)で差し替えてしまうと、プログラムに何の変更も行わずに通信をモックしたりできますね。これらの特徴はファイルというインターフェイスで統一している強みだと思います。
関連マニュアル
Plan 9では上で紹介したもの以外にも、多くのものをファイル経由で扱います。これらは、多くのものがマニュアルの3章または4章に書かれているので困ったら探してみると良いでしょう。