Logo address

Play with FUSE

2017/05/01
2017/05/03 追記
2017/05/26 書き換え

最初の一歩が難しい

FUSE が話題になってから10年ほどになる。今回改めて FUSE を調べる必要があって、10年前に比べて詳しく研究することとなった。

FUSE を試したことがない、あるいは試したがコンパイルもできなくて挫折した人は僕のをサンプル(maxim.c)を試してみたらよい。コンパイルから実行までの流れが解説されているので(多分)挫折はしないと思う。

FUSE の配布サンプルの中の hello.c は本当に詰まらない。FUSE の面白みが全く伝わって来ないのだ。最初の一歩は "Hello World" から始めるのがプログラミングの習慣なので、まあ仕方がないだろう。詰まらないと言ってもよく研究すると FUSE の基本がよく分かる。

僕のパッケージ maxim.tgz の中に maxim.c が含まれている。これは初心者にとっても十分に易しいサンプルプログラムだと思っている。それにも関わらず FUSE の面白みが十分に伝わって来る。maxim とは金言(諺) の意味である。maxim を実行すると magic.txt がマウントポイントに作成される。これは擬似的なファイルであることが直ちに分かる。すなわち cat コマンドでこのファイルを見ると、金言が1つ表示される。表示される金言は見るたびに変わる!

maxim は次の代表的な OS で動作することが確認されている。

準備

FUSE は現在では主要な unix 系 OS(MacOS,Linux,FreeBSD) の kernel ではサポートされているはずである。他方、殆どのプログラマは FUSE API(Application Programming Interface) (簡単に言えばライブラリーの事だと思ってよい)を通じて kernel 内の FUSE のコードを利用する。FUSE API は標準インストールされていないので、自分でインストールしなくてはならない。

unix 系の主要 3 OS (MacOS、Linux、FreeBSD) の現在の FUSE API バージョンを調べてみると 2.x である。3.x 系の API が github.com に上がっているが、初心者は敬遠した方がよい。以下では 2.x を前提として解説する。
以下 2.x 系を FUSE2、3.x 系を FUSE3 と言う。この2つはかなり違う。

なお、ちょっと難しい話になるのだが、FUSE のバージョンの話を正確に理解するには、FUSE の構成要素を理解しておく必要がある。各々の構成要素ごとにバージョンがあるのだから。
FUSE は以下の要素から構成されている。
(a) FUSE kernel extension
(b) FUSE kernel loader
(c) FUSE mounter
(d) FUSE programming library&header
カーネルに (a) が組み込まれて配布されている場合には (b) は要らない。Mac の場合には loader によって後から
kernel に extension が組み込まれる。loader は mounter が起動する1
Mac の場合、例えば osxfuse-3.5.8.dmg のようなパッケージで配布されるのであるが、この中には上記の全てが含まれている。FUSE の開発は Linux が大元になっていて、その成果が FreeBSD や MacOS に降りて来ている様である。番号 3.5.8 はパッケージに対するものであり、Linux の libfuse の番号と独立している様である。
(a)、(b)、(c) の事は、配布されたライブラリが吸収しているはずである。osxfuse-3.5.8.dmg の場合には (d) は FUSE2 のレベルである。


注1: 突然 Fuse が動かなくなったことがある。 そして /dev から osxfuse が消えた。この原因は、MacOS では(ダイナミックリンクのため) Fuse を使うアプリ、例えば sshfs が実行されて初めて Fuse のカーネル拡張がロードされるからである。であるから、再起動の直後は Fuse がインストールされていないかのように見えるであろう。
僕のシステムでは問題が発生した原因は sshfs と osxfuse の不適合からであった。

API version の調べ方

MacOS

sysctl -a | grep fuse.version

Linux

fusermount -V

FreeBSD

kldstat
または
sysctl -a | grep fuse.version

なお、この方法による FreeBSD の fuse.version の値は(筆者のは) 0.4.4 と表示された。これは何を意味しているだろう。実際には 2.6 を前提とした僕のサンプルは動くのだ。ネットを調べるとまだ問題があるらしく、フルにサポートされていないとのことか?

コンパイラは fuse.h を参照している。このファイルを調べると妥当な情報が得られるかも知れない。
fuse.h の場所は(3 OS 共に)

find /usr/local/include | grep fuse
を実行すれば分かる。

MacOS

Mac の場合には次の URL から手に入る。
[1] http://osxfuse.github.io/

正式名称は "FUSE for OS X" なのであるが、osxfuse と呼ばれることが多い。

現在(2017/05/26)における最新版は3.5.8である。
バージョン番号からして FUSE3 だと考えられるが、(一部に FUSE3 のコードが使用されているものの)ライブラリーは FUSE2 のままである。

これをダウンロードして

./configure && make
インストールして
/usr/local/include/osxfuse/fuse/fuse.h
を調べると ver.2.9.1 までサポートされていることが分かる。従って FUSE2 のスタイルでプログラムする必要がある。

Linux

次の URL から手に入る。
[2] https://github.com/libfuse/libfuse/releases

ここには ver.3.0.1 もあるが敬遠する。筆者は ver.2.9.7 をインストールしている。
ダウンロードして

./configure && make

インストールして

ls -l /usr/local/lib/*fuse*
を実行すると version が確認できる。

FreeBSD

僕はいつインストールしたのか、あるいは FreeBSD の標準インストールだったのか?

ls -l /usr/local/lib/*fuse*
で確認すると、ver.2.9.4 がインストールされていた。
もしも、まだインストールされていないなら、[1] から ver.2.9.7 をダウンロードする。
./configure && make
で無事コンパイルできる。

Hello.c

サンプルコードの中に hello.c が含まれている。筆者の maxim パッケージの中にも hello.c が含まれてる。筆者のは osxfuse に含まれていたのと同じものである。

コンパイルして(インストールしなくてもよい)、
適当なマウントポイントを作り(以下これを mtpt とする)

mkdir mtpt # need only once
./hello mtpt
ls -l mtpt
mtpt の中に hello.txt が見える。
cat mtpt/hello.txt
で "Hello World!" が表示される。
何の変哲もない普通のファイルのように見える。しかしこれはメモリの中にあるファイルである。

普通のファイルではないことは

mount
を実行すれば、マウントされたファイルの一覧に含まれていることから分かる。たったそれだけである!

ちょっぴり変なのは

ls -l mtpt
における日付で 1970/01/01 になっていることである。これは hello.c の手抜きのためで、マウントされた日付になるのが正しい考え方であろう。(この問題は maxim.c で改善される。)

Maxim パッケージ

maxim.c は maxim パッケージの中に含まれている。パッケージ maxim-1.0.tgzここからダウンロードできる。

パッケージの中には以下のものが含まれている。

Makefile

OS ごとに Makefile がある。
OSX で実行するときには

cp Makefile_osx makefile
としておけば後が楽である。(BSD や Linux も同様)

OS ごとに Makefile を作ったために内容は非常にシンプルである。
僕は configure から作られた Makefile は嫌いである。Makefile

の条件を満たす必要があると思う。近頃配布される Makefile は変に利口で、必要以上に膨れ上がっていて、中を見ても頭が痛くなるばかりで、上記の条件を満たさないものも多い。

Makefile_bsdMakefile_linux は実は同じである。多分、MacOS を除く全ての unix 系の OS で同じで構わない。

最初に述べた準備(Fuse API)が整っていればコンパイルできるはずである。

Makefile の中では Fuse API version を 2.6 に設定したが、内容が初等的なだけに、もっとバージョンを下げても大丈夫かも知れない。
なお、FUSE3 は API がかなり変更されているので、添付の hello.cmaxim.c もコンパイルできない。

久々に unix の Makefile を書いてみると

.o:
	$(CC) $(CFLAGS) -o $@ $< -L$(LIBRARY_DIR) $(LIBS) misc.o
の部分に手間取ったなあ...

osxfuse に添付されていた Makefile では ".o" ではなく、".c" になっていた。(misc.o は僕のファイルだから勿論入っていない。)
Makefile の意味を考えるに ".c" は明らかにおかしい。問題は ".o" も何だか変だと感じたことである。オブジェクトファイルを作るだけならリンカーオプションは要らないはずである。この違和感は、unix の C コンパイラ cc がコンパイラーとリンカーを兼ねていることから発生する。(Plan9 では分離されている。)

トライ&エラーを繰り返し、ようやく何だか少し分かってきた。cc はリンカーオプション(-L)が最初に現れた場所から後の引数はリンカーに渡しているようだ。".o" の次の行は、".o" の生成規則ではなく、".o" が必要であるにも関わらず存在しない時に実行されるコマンドで、cc の場合には(リンカーを兼ねているために)一挙に実行ファイルの生成にまで進んでしまうと言うことらしい。

maxim.c

maxim の使い方

usage: maxim maxims.txt mtpt

以下に実行例を示す。

-bash$ mkdir mtpt 	# need only once
-bash$ maxim maxims.txt mtpt
-bash$ ls -l mtpt	# then you will find mtpt/magic.txt

# try "cat mtpt/magic.txt". the outputs below are example
-bash$ cat mtpt/magic.txt
You can’t eat your cake and have it.
-bash$ cat mtpt/magic.txt
Even a worm will turn.
-bash$ cat mtpt/magic.txt
Nothing comes of nothing.

# unmount
-bash$ umount mtpt	# for osx, bsd and linux
-bash$ fusermount -u mtpt	# for linux

金言(諺) がランダムに選ばれて

cat mtpt/magic.txt
で表示されていることを見て欲しい。

ファイルの日付は

ls -l mtpt
で見ることができる。maxim.c では hello.c と違って、マウントした日付にされている。

このような仮想的なファイルの場合には読み取って初めてファイルサイズが確定するのであって、ls -l で表示されるファイルサイズは何になるのが正しいのだろうか? 0 にする以外にはなさそうに思える。

通常のファイルは静的である。そのような場合にはファイルのキャシングが上手く働く。しかし、このケースではキャシングは邪魔である。maxim.c には「キャシングするな」の指示が含まれている。

今回気が付いたが MacOS と他(FreeBSD と Linux)ではディレクトリのリンクカウントの考え方が違うんだね...

misc.c

misc.c の中には次の関数が含まれている。

このうち readlines() はファイルを読み取り、行の配列を作る。
ランダムに行が表示されるために、行の内容を配列に入れることが求められる。
readlines() の使い方は test_misc.c に示されている。

int n,i;
char **lines;
n = readlines(*argv,&lines);
for(i = 0; i < n; i++)
	puts(lines[i]);
free(lines);

工夫されているのは free(lines) でメモリーが解放されていることである。
readlines() 専用の free 例えば free_readlines(lines) のようなものを使うのであればコーディングは簡単である。unix にはそのような「発明」が多いのであるが、プログラマに優しいとは言えない。

補助的に使われている mread1()mread2() もファイルから読み取った内容をメモリーに保存する。mread1() はメモリーを準備する前にファイルのサイズを調べ必要なメモリーを確保する。mread2() はファイルを読み取りながら必要なメモリーのサイズを調整する。コーディングは mread1() の方が簡単でありメモリーの無駄がない。これで良さそうに思えるかも知れないが、問題がある。ディスク上のファイルを読み取る場合には問題は無い。しかし、パイプからのデータや仮想ファイルを読む場合には、この方法は使えない。