Kernel/VM Advent Calendar 52日目 : Brainf**kファイルサーバでもつくってみるか on Plan 9

はじめに

この記事はKernel/VM Advent Calendarのための記事です。

さてPlan 9です. そしてBrainf**kです.
ただ単純に二つを掛け合わせても, Plan 9でのプログラミングの解説にしかならないので,
Plan 9っぽさとして, Brainf**kのインタプリタサービスをファイルとしてアクセス出来るようにしてみました.

作ったモノ

今回は以下の二つをつくってみました.

皆さんご存知のように, Plan 9では全てのリソース, サービスをファイルとして扱うことができます.
逆に, サービスを提供する側のひとたちはファイルとして扱えるようにサービスを実装する必要があります.
ユーザ空間ではファイルサーバ, カーネル空間ではバイスとして, サービスを実装することでこれを行います.
ファイルサーバは, Linuxなどで言えばデーモンに相当するプロセスとして動作します. デバイスは, デバイスでいいんだろうか・・・


今回, インタプリタは実行プログラムとしてではなくファイルとしてユーザから扱えます.
このため, そもそもプログラムを実行させる方法や, brainfuckの命令である"."(画面出力)や","(入力)を受け取る方法を変える必要があります.
ここでは以下のようにしました.

  • インタプリタのファイルサーバはcmddataと名付けられたファイルを提供
  • cmdにプログラムを書き込むと, その結果がdataに溜まる
  • 画面出力の"."は, dataにデータを溜めるために用いる
  • 入力の","は, 後続する一文字をテープの所定の位置に突っ込む

このため, echoでcmdにプログラム文字列を突っ込んでから, catでdataを読み込むと結果が得られる, といった形になります.

bffileserver

bffileserverは, 9Pファイルサーバとして実装されています.
ファイルサーバは, ファイルをユーザに提供するデーモンのようなモノと言えます.
もともとただのプログラムなので起動は, bfとコマンドを入力するだけです.
起動すると, このプロセスは/srv/bfにファイルを作成します.
ユーザはこのファイルをmountコマンドで, 好きな位置にマウントすることで, インタプリタのサービスを扱うためのファイルを参照することができます.

ここではbfsrvディレクトリにマウントしてみました. cmdとdataという名前のファイルが見えているのが分かると思います.

さて, 実際にプログラムを流し込んでみましょう. brainfuckでのHelloWorldはこんな感じでした.

+++++++++[>++++++++>+++++++++++>+++++<<<-]>.>++.+++++++..+++.>-.------------.<++++++++.--------.+++.------.--------.>+.

ここではechoコマンドでこの文字列をcmdに流し込んでみます.

見事になにも起こりませんね. ファイルは叫んだりしないのであたりまえといえばあたりまえですが.
それでは, dataの中身をcatで呼んでみましょう.

するときちんとここに, 本来画面に出力すべきであったデータがとってあるのが見えます.
(なおここは, 単なるバッファなので一回読み出すとそれっきりです. 二回目にcatコマンドを読んでもなにも出ません. )

ファイルサーバの作り方

ファイルサーバの実装は以下の手順が必要です.

  1. Srv構造体を定義
  2. 仮想的なディレクトリツリーの定義とファイルの作成・追加
  3. ディレクトリツリーの登録&サービスの提供

以上がbfのmain関数で行うべき全てです.

void main(int argc, char** argv)
{
	initfs();  // 1, 2

	postmountsrv( &bffs, "bf", nil, MREPL|MCREATE ); // 3
}

Srv構造体(9p(2))は, そのファイルサーバの挙動と, 提供するツリーの情報を保持します.
前者は, ファイル操作に対応した関数テーブルと, ディレクトリツリーを表すTreeという構造体から成ります.
今回, このファイルサーバは1階層に二つのファイルしかない単純な構造です.
基本的にはread/write時に行うべき事を定義するだけでやりたいことができるので, 実装した関数は以下のように非常に少ないです.

Srv bffs = {
	.open	= fsopen,
	.read	= fsread,
	.write	= fswrite,
};

もっと複雑だったり, 動的に編成されるツリーを提供するようなファイルサーバを実装する場合には,
他にもいろいろと関数を追加する必要があります. が, まぁ手抜きなのでこんなもんでしょう...

このSrv構造体にツリーを割り当てて, ファイルを追加していきます.
これは, メモリ上の仮想的なツリーです. こういったものを形成するための関数がいくつか用意されています(9pfile(2)).

void initfs(void)
{
	bffs.tree = alloctree( nil, nil, DMDIR|077, fsdestroyfile );  // ディレクトリツリーの作成
	createfile( bffs.tree->root, "cmd", nil, 0777, nil );  // cmdファイルの追加
	createfile( bffs.tree->root, "data", nil, 0777, nil ); // dataファイルの追加

	return;
}

今回の場合, echoコマンドでfswrite()関数が, catコマンドでfsread()関数が呼び出されると予想できます.
それぞれ, 前者は受け取った文字列を解釈・実行しバッファにデータをため, 後者ではこのバッファからデータを読みます.

devbrainfuck

bffileserverはお手軽ですが, ユーザ空間のサービスの上あんまりカーネルVMっぽくないので, Advent Calendarのネタにするにはもう少しひねりが必要そうです.
ここでは, "とりあえずカーネルかデバドラ弄くれば許されるだろう"という安直な発想の元,
brainfuckバイスのようなものをPlan 9で書いてみました. 基本的な動作は先のファイルサーバ版と変わりません.



Plan 9では多くの機能がファイルサーバとして提供されています.
かのYUREXドライバ for Plan 9も, ファイルサーバの一つです.
これらはファイルとしてのインタフェイスを提供し, 容易に入れ替えたりすることができます.
一方で, Plan 9カーネルのコアの機能はカーネルに静的に埋め込まれています.
この機能を提供するモジュールをバイスと呼びます.
先ほどのYUREXドライバの土台となるUSBデバイスや, プロセスデバイス, ファイルシステムバイスなどがこれに含まれます.
ここでは, このデバイスの一つとして, Brainf**kのインタプリタサービスを追加してみます.


使い方

まずは, カーネルにこのソースコードを組み込む必要があります. ここでは, x86向けのpcfというカーネルの場合のコンパイル, 起動方法を挙げます

  • devbrainfuck.cソースファイルを/sys/src/9/port/以下にコピー
  • このカーネルのコンフィグファイルである/sys/src/9/pc/pcf中に, 組み込むデバイスとしてbrainfuckを追加

% mk 'CONF=pcf'
% ls 9pcf
9pcf

  • 9fat:コマンドで, 9fat領域を/n/9fat以下にマウント

% 9fat:
% ls /n/9fat
...

  • 先の9pcfカーネルをここにコピー(ここではbf9pcfという名前に変更)

% cp 9pcf /n/9fat/bf9pcf

  • /n/9fat/plan9.iniに以下の行を追加

bootfile=sdC0!9fat!bf9pcf

先ほどのファイルサーバの例では, 明示的にプログラムを立ち上げる必要がありましたが, 今回はカーネルが処理を行ってくれるので不要です.
ただし, サービスにアクセスするために, Brainf**kデバイスが提供するファイルツリーをbindする必要があります.
それぞれのデバイスは, 自身が提供するファイルツリーを, #と任意の一文字からなる名前空間に持っています.
たとえば, ルートファイルシステム#/, キーボードマッピング, マウスは#mと言った具合です
devbrainfuckデバイス#b名前空間を使っているので, これを適当な位置にマウントします.
ここでは/mnt/bfにマウントしてみました.

先ほどと同じく, 2つファイルがあります. 今回はbfcmdとbfdataという名前にしました.
これは通常, デバイスの提供するツリーは/dev以下にみんなまとめてbindされることが多いため, 今回作ったファイルを区別するためです.
ファイルサーバの例と同じように, echo/catでプログラムの流し込み, 結果の取り出しができます.

作り方

ファイルサーバではSrv構造体を用いましたが, デバイスの実装ではDev構造体というものがかわりに用意されています.
これもSrv構造体と同じような働きをしますが, ツリーの情報は持っていません.
一方で, デバイス固有の名前空間を定義するために任意の一文字を指定するメンバがあります.
ここではbを割り当てています.(Bはとられていた・・・)

Dev brainfuckdevtab = {
	'b',
	"brainfuck",

	bfreset,
	devinit,
	devshutdown,
	bfattach,
	bfwalk,
	bfstat,
	bfopen,
	devcreate,
	bfclose,
	bfread,
	devbread,
	bfwrite,
	devbwrite,
	devremove,
	devwstat,
};

ファイルサーバの時にも述べましたが, やるべき事はそんなに多くないので大抵の関数はdev~で始まるデフォルトの挙動を行う関数になります.
またその他の関数についても, bfread()とbfwrite()以外は大してお仕事をしていません. しかも, brainfuckの主要部はファイルサーバのモノと共通化できているという...

まとめ

というわけでBrainf**k interpreter as a Fileとして, ユーザ空間のファイルサーバ実装と, カーネル空間のデバイス実装について取り上げてみました.
でっちあげの構造のためいろいろと不備がありますが, 基本的な実装はRAMファイルシステム(/sys/src/lib9p/ramfs.c)やキーボードマップデバイス(devkbmap.c)などといった,
実際のファイルサーバやデバイスを参考にしたものです. Everything is a FileSystemをどのように実現しているのかうかがい知るのにはある程度, 役に立つでしょう.

もっとも, Plan 9の一番面白いところは, デバイスやファイルサーバとユーザの間に横たわるカーネル自体にあります.
これを期にPlan 9ソースコードなぞ探検してみてはいかがですか?