9P
はじめに
9Pはコンピュータ上の色々なものをファイルとして扱うためのプロトコルである。このプロトコルを使えば、ディスクに記録されたデータだけでなく、マウスやキーボードといった入力機器や、ネットワーク、プロセスの情報等もファイルとして扱える。Unixの後継OSとして米AT&Tのベル研究所で開発されたPlan9というOSのために設計された。
Plan9というOSはネットワークを介して複数のコンピュータを繋いで使うように設計されているので、この9Pもローカルだけでなくネットワーク越しに使うことを前提にしている。
プロトコルの概要
9Pの通信ではクライアントがサーバーに対してリクエストを送り、サーバーがそれに対してリプライを返すことを繰り返す。クライアントからのリクエストをTメッセージ、サーバーからのリプライをRメッセージと言う。Tメッセージにはファイルにアクセスするための色々なものが用意されている。例えばファイルを開くためのTopenや、開いたファイルに書き込むためのTwrite等である。それぞれのTメッセージには対応するRメッセージがある。Topenに対してRopen、Twriteに対してRwrite等である。
通信は必要な情報をプロトコルの定める方法でバイト列に変換して行う。2バイト以上のデータはリトルエンディアンになるように配置する。テキストデータはUTF-8にエンコードする。テキストデータは長さの情報も一緒に送るので最後のヌル文字は含めない。
各メッセージは4バイトの整数から始まる。これは、この4バイトを含むメッセージの長さをバイト単位で示したものである。次にメッセージの種類を記述する1バイトのデータが来る。次はメッセージのタグである。クライアントはサーバーからの返事を待たずに別のメッセージを送れるので、サーバーからのRメッセージがどのTメッセージに対する返事なのかを識別する必要がある。そのために使うのがタグである。クライアントがTメッセージを送る際にタグを付け、サーバーはそれに対するRメッセージに同じタグを付ける。その後ろには、メッセージの種類によって固有の情報が付けられる。
以下は実際の9P通信のログである。実際はバイナリの通信だが、ログとして見易いように変換してある。また、先頭にくるメッセージサイズは省略してある。この例では、サーバーのルートディレクトリにあるhello
というファイルの内容を読みこんでいる:
<-- Tversion Tag 65535 msize 8192 version '9P2000'
--> Rversion Tag 65535 msize 8192 version '9P2000'
<-- Tattach Tag 0 fid 0 afid -1 uname kenji aname
--> Rattach Tag 0 qid (0000000000000000 0 d)
<-- Twalk Tag 0 fid 0 newfid 1 nwname 1 0:hello
--> Rwalk Tag 0 nwqid 1 0:(0000000000000001 0 )
<-- Tstat Tag 0 fid 1
--> Rstat Tag 0 stat 'hello' 'kenji' 'kenji' '' q (0000000000000001 0 ) m 0644 at 1730004808 mt 1730004808 l 7 t 0 d 0
<-- Twalk Tag 0 fid 1 newfid 2 nwname 0
--> Rwalk Tag 0 nwqid 0
<-- Topen Tag 0 fid 2 mode 0x0
--> Ropen Tag 0 qid (0000000000000001 0 ) iounit 8169
<-- Tread Tag 0 fid 2 offset 0 count 4096
--> Rread Tag 0 count 7 ' 776f726c 64210a'
<-- Tread Tag 0 fid 2 offset 7 count 4096
--> Rread Tag 0 count 0 ''
<-- Tclunk Tag 0 fid 2
--> Rclunk Tag 0
まず最初にクライアントがサーバーに対してTversionを送り、プロトコルのバージョンと、メッセージの最大サイズを交渉する。Tversionのタグは65535
と決められている。msize
の8192はメッセージの最大サイズ(バイト)で、version
の9P2000がプロトコルのバージョンである。サーバーからRversionで同じmsize
とversion
が返ってきたのでこのサイズとバージョンで以降の通信を行う。
次のTattachで、クライアントがサーバーにルートディレクトリのfid
を要求する。afid
以降は認証用の情報である(後述)。fid
はコネクションにおいてファイルと紐付けられる整数で、Unixにおけるファイルディスクリプタのようなものである。ファイルの読み書きはこのfid
を使って行われる。サーバーからのRattachにはルートディレクトリのqid
が含まれる。これはサーバー上でファイルを一意に識別するもので、Unixにおけるinodeに相当するものである。以上でサーバーに繋いでセッションを確立できた。
次にTwalkで目的のファイル(hello
)までファイルツリーを辿る。ここでは起点として先程得られたルートディレクトリ(fid = 1
)を起点として、このディレクトリ内のhello
ファイルを要求してfid = 2
を割り当てようとしている。サーバーはルートディレクトリ内に要求されたファイルが見付かったので、そのファイルのqid
を返す。
目的のファイルにfid
が割り当てられたので、Tstatでこのファイルの属性を要求している。おそらくファイルのパーミッションを調べるためである。
次にTopenを使ってこのファイルを開き、Treadを使って内容を読む。二回目のTreadに対して0バイトのデータが返ってきたので、ファイルはこれで終りである。
最後に、必要なくなったfid
はTclunkで捨てる。
List of all Message Types
Tメッセージ | Rメッセージ | 説明 |
---|---|---|
Tversion | Rversion | バージョンの交渉 |
Tauth | Rauth | 認証ファイルの取得 |
Rerror | エラー(Rメッセージのみ) | |
Tflush | Rflush | 処理中のリクエストをキャンセル |
Tattach | Rattach | ルートディレクトリの取得 |
Twalk | Rwalk | ファイルツリーを辿る |
Topen | Ropen | ファイルを開く |
Tcreate | Rcreate | ファイルの作成 |
Tread | Rread | ファイルを読む |
Twrite | Rwrite | ファイルへ書き込む |
Tclunk | Rclunk | fidの削除 |
Tremove | Rremove | ファイルを削除 |
Tstat | Rstat | ファイルの属性を取得 |
Twstat | Rwstat | ファイルの属性を変更 |
fid
とqid
fid
はコネクションにおいてファイルを指定するために使われる整数で、Unixのファイルディスクリプタのようなものである。使用する整数はクライアントが指定できる。サーバーに接続する際にTattachにより、サーバーのルートディレクトリのfid
が設定され、Twalkにより追加され、Tclunkにより削除される。
qid
はサーバーがファイルを一意に識別するためのもので、Unixのinodeに相当するものである。ただしinodeはファイルが削除されると再利用される可能性があるのに対し、qid
は再利用できない。qid
はこのファイルがディレクトリかどうか等を示すtype
、ファイルが変更されたときにインクリメントされるvers
、そしてファイルを一意に表すpath
の3つの情報で構成される。
認証
9Pにはクライアントの認証が組込まれていない。もともとはあったようだが、認証のアルゴリズムに欠陥が見付かったりすればいちいちコードを修正してコンパイルしなおさなければいけないうえ、9Pプロトコル自体も変更しないといけないので、外部に切りだすことになった。認証に必要なやりとりは認証サーバーとクライアントが行い、9Pサーバーは認証が成功したかどうかの情報を認証サーバーに確認することでクライアントの確認を行う。
9PサーバーはTattachメッセージを受けとると、認証サーバーとのコネクションを確立する。その後Rattachメッセージでafid
という特殊なfid
をクライアントに返す。クライアントからこのafid
に対して読み書きする命令が届くと、9Pサーバーはこの読み書きを認証サーバーとのコネクションに横流しする。つまりクライアントはこのafid
を通じて認証サーバーと直接やりとりができる。クライアントはユーザー名やパスワード等(パスワード認証の場合)の情報をafid
に書き込んで認証する。このように認証をプロトコル自体から切り離すことで、認証に関するバグが見付かったとしても、認証サーバーを修正するだけでいい。また、より強力な認証システムを組み込むのも楽である。
コネクションの共有
クライアントはサーバーとversion
メッセージを交すことでコネクションを確立する。コネクションの確立からの一連のやりとりをセッションという。ひとつのコネクションは複数のクライアントが共有できる。
クライアントはTauthまたはTattachメッセージによりサーバーにfid
を要求できる。このうちTauthにより得られたfid
は認証以外には使えない。Tattachで得られたfid
は、Twalkメッセージによりファイルツリーを辿るための起点として利用でき、このとき辿った先のファイルを示すfid
が生成される。これ以外の方法でfid
が増えることはない。つまり、サーバーはひとつのコネクションのなかで、ルートディレクトリから派生したfid
を追跡することで、クライアントを区別できる。
メッセージ詳細
それぞれ最初にメッセージの形式を書く。field[n]
はfield
という名前のnバイトのデータを表す(nは整数)。また、括弧の中が整数ではなくs
になっている場合、そのフィールドは文字列のデータで、その前に文字列の長さを示す2バイトの整数が先行する。また、メッセージ名のフィールド(Tversion
等)にはメッセージの種類を表す1バイトのenumが来る。
version
プロトコルのバージョンを交渉すし、コネクションを確立する。
size[4] Tversion tag[2] msize[4] version[s]
size[4] Rversion tag[2] msize[4] version[s]
クライアントは最初にサーバーにこのメッセージを送ってバージョンとメッセージの最大サイズ(msize
)を交渉する。サーバーはクライアントから送られたバージョンとメッセージサイズに対応していれば、同じバージョンとサイズを送り返し、交渉成立である。
サーバーとクライアントはこのメッセージを以ってコネクションを確立する。
attach、auth
コネクションを確立する。
size[4] Tauth tag[2] afid[4] uname[s] aname[s]
size[4] Rauth tag[2] aqid[13]
size[4] Tattach tag[2] fid[4] afid[4] uname[s] aname[s]
size[4] Rattach tag[2] qid[13]
attach
メッセージはサーバーのルートディレクトリを要求し、fid
を割り当てる。認証が必要なサーバーであれば、先にauth
メッセージで認証を済ませておき、attach
メッセージのafid
に、先程使用したafid
をセットする。サーバーはこのafid
に紐付いている認証サーバーにクライアントが認証済みであるかを確認し、認証済みであればルートディレクトリのqid
を返す。サーバーが複数のファイルツリーを公開している場合、aname
で指定する。
auth
メッセージは認証サーバーとやりとりするためのfid
であるafid
を要求する。このafid
を読み書きすることで認証サーバーとやりとりをし、自身の身分を証明する。認証が済んだらこのafid
をセットしたattach
メッセージを送る。
error
エラーを返す。
size[4] Rerror tag[2] ename[s]
クライアントからの要求に対して、なにかエラーがおきたときにそのエラーを返すために使われる。Terror
はない。
flush
リクエストをキャンセルする。
size[4] Tflush tag[2] oldtag[2]
size[4] Rflush tag[2]
実行中の要求をキャンセルする。キャンセルしたいリクエストのtag
をoldtag
にセットする。
walk
ファイルツリーを移動する。
size[4] Twalk tag[2] fid[4] newfid[4] nwname[2] nwname*(wname[s])
size[4] Rwalk tag[2] nwqid[2] nwqid*(qid[13])
fid
を起点に移動する。移動先のファイルをnewfid
にセットする。移動するディレクトリの数をnwname
に、移動するディレクトリの名前を移動する順にwname
にそれぞれセットする。例えばカレントディレクトリから./dir1/dir2/
に移動する場合、nwname
は2
、wname
は{"dir1", "dir2"}
となる。wname
の最後の要素はディレクトリでなくファイルでもいい。
wname
の最初の要素への移動が失敗した場合はRerror
が返される。それ以外の場合はRwalk
が返され、qid
は移動が成功した順番にそのファイルのqid
がセットされる。nwname
とnwqid
が一致した場合は最後まで移動できたことになる。
walk
はfid
を増殖させる唯一の方法である。
open、create
fid
を開いて(作成して)読み書きできる状態にする。
size[4] Topen tag[2] fid[4] mode[1]
size[4] Ropen tag[2] qid[13] iounit[4]
size[4] Tcreate tag[2] fid[4] name[s] perm[4] mode[1]
size[4] Rcreate tag[2] qid[13] iounit[4]
open
はfid
と紐付いたファイルを開く。mode
は読み込み専用のOREAD
、書き込み専用のOWRITE
、読み書きのORDWR
等である。ファイルを開く手順は以下の通り:
attach
でルートディレクトリのfid
を取得- ルートディレクトリの
fid
からwalk
で開きたいファイルのfid
を取得 - その
fid
をopen
で開く
create
はfid
と紐付いたディレクトリに新しいファイルを作成する。creat
ではなくcreate
である。作成が成功したらfid
は新しく作成されたファイルに紐付く。
read、write
ファイルを読み書きする。
size[4] Tread tag[2] fid[4] offset[8] count[4]
size[4] Rread tag[2] count[4] data[count]
size[4] Twrite tag[2] fid[4] offset[8] count[4] data[count]
size[4] Rwrite tag[2] count[4]
read
はfid
のoffset
バイト目からcount
バイト読む。読んだデータと、読めたデータサイズがdata
、count
ととして返される。write
はfid
のoffset
バイト目にcount
バイトのデータdata
を書く。書きこめたサイズがcount
として返される。ファイルを読み書きするためにはあらかじめfid
をopen
する必要がある。
clunk
fid
を忘れる。
size[4] Tclunk tag[2] fid[4]
size[4] Rclunk tag[2]
いらなくなったfid
を忘れる。一度忘れたfid
はwalk
で別のファイルを取得する際に再利用できる。
remove
ファイルを削除する。
size[4] Tremove tag[2] fid[4]
size[4] Rremove tag[2]
fid
と紐付いたサーバー上のファイルを削除する。fid
はclunk
したのと同様に忘れられる。
stat、wstat
ファイルの属性を読み書きする。
size[4] Tstat tag[2] fid[4]
size[4] Rstat tag[2] stat[n]
size[4] Twstat tag[2] fid[4] stat[n]
size[4] Rwstat tag[2]
stat
はファイルの属性を読む。読めた情報はstat
として返される。wstat
はファイルの属性をstat
に変更する。stat
はファイルの名前やqid
、サイズ、作成日、更新日等の情報がバイト列になったものである。