Plan 9とGo言語のブログ

主にPlan 9やGo言語の日々気づいたことを書きます。

Plan 9はファイルをどのように使っているか

Plan 9は、ほとんどの操作をファイルを通して行います。リソースの参照だけではなく、シグナルの送信*1などカーネルに対して命令を送る場合も、ある程度はファイルを通して行えます。初見では、これらの習慣は馴染みがないと思うので、普段使いそうなものをいくつか紹介します。

Plan 9Unixよりeverything is a fileを徹底していると言われていますが、Rob Pike氏によるとeverything has the same interfaceとのことです。ファイルというインターフェイスを通して全てのことを行う様子は、以下の例からも理解してもらえるんじゃないでしょうか。

それでは、まずは簡単なところから。

コピー・ペースト

カーネルが提供する/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>/ctlkillと書き込みます。これはUnixkill -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>/wctlscrollと書き込むと、該当するウィンドウではプログラムの出力に従ってスクロールするようになります。

# winidから自身のwindow idが読める
% cat /dev/winid
          2
% echo scroll >/dev/wsys/2/wctl

ネットワーク設定

ネットワーク関連の操作は/net以下を扱います。

IPアドレス設定

自身のIPアドレスを設定する場合は、/net/ipifc/cloneadd ipaddr ipmaskと書き込むことで行います。

% echo add 192.168.1.3 255.255.255.0 >/net/ipifc/clone

静的ルーティングは/net/iprouteadd 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/ipconfigDHCPなどから構成を取得して、以下のファイルを更新しているだけです。上記以外にも、多くの設定やコマンドが用意されているので、興味があればip(3)を読むと参考になるでしょう。また、パケットルーティングに関連するカーネルの動作は以下の記事にも書きました。

ネットワークプログラミング

Plan 9にはdial(2)やannounce(2)など高レベルの関数が用意されていますが、これらはシステムコールではなく、ただのライブラリ関数です。*2

TCPクライアント

どのようにOSがファイルを扱うか見るため、高レベル関数は使わずに直接/net/tcp/net/udpなどのディレクトリを扱って通信を行ってみましょう。まずTCPクライアントは、/net/tcp/cloneconnectコマンドを書き込んで、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章に書かれているので困ったら探してみると良いでしょう。

*1:Plan 9ではNoteと呼ぶ

*2:Plan 9のマニュアル2章はライブラリ関数で、システムコールと区別しません