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 のレベルである。
/dev
から osxfuse
が消えた。この原因は、MacOS では(ダイナミックリンクのため) Fuse を使うアプリ、例えば sshfs
が実行されて初めて Fuse のカーネル拡張がロードされるからである。であるから、再起動の直後は Fuse がインストールされていないかのように見えるであろう。
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
Mac の場合には次の URL から手に入る。
[1] http://osxfuse.github.io/
正式名称は "FUSE for OS X" なのであるが、osxfuse と呼ばれることが多い1。
現在(2017/05/26)における最新版は3.5.8である。
バージョン番号からして FUSE3 だと考えられるが、(一部に FUSE3 のコードが使用されているものの)ライブラリーは FUSE2 のままである。
これをダウンロードして
./configure && make
/usr/local/include/osxfuse/fuse/fuse.h
最近は macFUSE と改名している。現在(2021/08/14)における最新版は macFUSE 4.2.0である。macFUSE になってからソースコードが公開されていない!
次の URL から手に入る。
[2] https://github.com/libfuse/libfuse/releases
ここには ver.3.0.1 もあるが敬遠する。筆者は ver.2.9.7 をインストールしている。
ダウンロードして
./configure && make
インストールして
ls -l /usr/local/lib/*fuse*
僕はいつインストールしたのか、あるいは FreeBSD の標準インストールだったのか?
ls -l /usr/local/lib/*fuse*
./configure && make
サンプルコードの中に hello.c
が含まれている。筆者の maxim パッケージの中にも hello.c
が含まれてる。筆者のは osxfuse
に含まれていたのと同じものである。
コンパイルして(インストールしなくてもよい)、
適当なマウントポイントを作り(以下これを mtpt
とする)
mkdir mtpt # need only once ./hello mtpt ls -l mtpt
mtpt
の中に hello.txt
が見える。cat mtpt/hello.txt
普通のファイルではないことは
mount
ちょっぴり変なのは
ls -l mtpt
1970/01/01
になっていることである。これは hello.c
の手抜きのためで、マウントされた日付になるのが正しい考え方であろう。(この問題は maxim.c
で改善される。)
maxim.c
は maxim パッケージの中に含まれている。パッケージ maxim-1.0.tgz
はここからダウンロードできる。
パッケージの中には以下のものが含まれている。
Makefile_osx
# Makefile for OSXMakefile_bsd
# Makefile for FreeBSDMakefile_linux
# Makefile for Linuxhello.c
# from osxfusemaxim.c
# my filemaxims.txt
# a data file.misc.c
# my fileplist
# package listtest_misc.c
# my fileusage_maxim.txt
# this file
OS ごとに Makefile がある。
OSX で実行するときには
cp Makefile_osx makefile
OS ごとに Makefile
を作ったために内容は非常にシンプルである。
僕は configure
から作られた Makefile
は嫌いである。Makefile
は
Makefile
は変に利口で、必要以上に膨れ上がっていて、中を見ても頭が痛くなるばかりで、上記の条件を満たさないものも多い。
Makefile_bsd
と Makefile_linux
は実は同じである。多分、MacOS を除く全ての unix 系の OS で同じで構わない。
最初に述べた準備(Fuse API)が整っていればコンパイルできるはずである。
Makefile の中では Fuse API version を 2.6 に設定したが、内容が初等的なだけに、もっとバージョンを下げても大丈夫かも知れない。
なお、FUSE3 は API がかなり変更されているので、添付の hello.c
も maxim.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
の場合には(リンカーを兼ねているために)一挙に実行ファイルの生成にまで進んでしまうと言うことらしい。
Apple は頻繁に OS のバージョンアップを繰り返す。そしてそのたびに動かないソフトウェアが発生する。有料ソフトウェアの作者はそれによって利益が転がってくることもあるから文句は言わないだろう。しかし Mac で動くソフトウェアを無償で提供している人たちもいるのだ。彼らは OS のバージョンアップに振り回されることになる。
OS X 改め macOS になって FUSE も osxFUSE から macFUSE になった。そのために以前の Makefile が動かなくなっていた。そこで macFUSE 用の Makefile_mac
を載せておく: ダウンロード
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
の中には次の関数が含まれている。
int mread1(char *file,char **bp);
int mread2(char *file,char **bp);
int readlines(char *file, char ***linesp);
このうち 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()
の方が簡単でありメモリーの無駄がない。これで良さそうに思えるかも知れないが、問題がある。ディスク上のファイルを読み取る場合には問題は無い。しかし、パイプからのデータや仮想ファイルを読む場合には、この方法は使えない。