Linux Advent Calendar 2020 第4日目の記事です。
LinuxにはFUSE(Filesystem in Userspace)という、ユーザランド上に仮想的なファイルシステムを構築するための機能があります。
今日の記事では、このFUSEを利用してちょっとしたサンプルを作成してみる話をしようと思います。
背景
今期のアニメにおちこぼれフルーツタルトがあります。この劇中歌に「ブロ子の歌」という同じフレーズを延々と繰り返す曲があり、なかなかの中毒性があります。
(ちなみに歌詞の繰り返しをグラフ化するアプリもご用意してございます)
「これはファイルを開くたびに歌詞を表示し続けるような振る舞いをさせると面白いのでは」と思い、FUSEを使用してサンプルアプリを作成してみようと考えたのが今日のお話の背景になります。
FUSEを使用する
FUSEカーネルモジュールをビルドする
実験環境としてCentOS-8上にLinux-5.9.3のカーネルをインストールして試してみました。
Linuxの make defconfig
ではFUSEがビルドに含まれないようです。そのため今回はカーネルモジュールとしてFUSEをビルドしてみました。
既にカーネルがビルド済みの場合は、FUSEをカーネルモジュールとしてビルドする設定で対応できます。
具体的には、カーネルコンフィグの画面で"FUSE (Filesystem in Userspace) support"の設定値を「<M>
」にすればOKです。
上記の設定を入れたカーネルソースツリーで、 make fs/fuse/fuse.ko
を実行するとFUSEのカーネルモジュール( fuse.ko
)をビルドできます。
# cd /usr/src
# make fs/fuse/fuse.ko
CALL scripts/checksyscalls.sh
CALL scripts/atomic/check-atomics.sh
DESCEND objtool
MODPOST Module.symvers
CC [M] fs/fuse/fuse.mod.o
LD [M] fs/fuse/fuse.ko
あとは fuse.ko
を読み込めばLinuxカーネル側での準備は完了です。
# lsmod
Module Size Used by
# insmod /usr/src/fs/fuse/fuse.ko
# lsmod
Module Size Used by
fuse 118784 1
libfuseのサンプルを参照してみる
サンプルのコンパイル
ユーザランド側でFUSE(のライブラリ)機能は、libfuse/libfuseを使用します。CentOS-8ではパッケージで提供されており、以下の手順でインストールできます。
$ sudo dnf install -y fuse3 fuse3-libs fuse3-devel
libfuse/libfuseのリポジトリにはサンプルコードも含まれているため、パッケージでFUSEの開発ライブラリをインストールして以下のようにサンプルをコンパイルして試してみるのが理解が進みやすそうです。
サンプルとして提供されている hello.c
をコンパイルして動作させることで、FUSEの使い方を把握してみます。
以下の手順でサンプルのコンパイルが行えます。
$ git clone https://github.com/libfuse/libfuse.git
$ cd libfuse/example
$ gcc -g -o hello hello.c `pkg-config fuse3 --cflags --libs`
どうやら実行時引数にマウント先のディレクトリを指定するようです。ここでは mnt
ディレクトリを作成し、そこにマウントしてみましょう。
$ mkdir mnt
$ ./hello ./mnt
プログラムを実行すると、すぐにプロンプトが返ってきます。 ps
コマンドで確認するとプロセスが存在しており、端末から切り離されて実行されるような挙動になっていることが分かります。
また、 mnt
ディレクトリの中を見ると、 hello
というファイルが生えて(?)きていることも確認できます。
$ ps ax | grep hello
10585 ? Ssl 0:00 ./hello ./mnt
10589 pts/0 S+ 0:00 grep --color=auto hello
$ ls -l ./mnt/hello
-r--r--r-- 1 root root 13 1月 1 1970 ./mnt/hello
ファイル hello
の中身を見ると、"Hello World!"というテキストが入っていました。
$ cat ./mnt/hello
Hello World!
ソースコードと照らし合わせて調べてみる
libfuse/libfuse/example/hello.cを見ると、マウントした時に生えてくるファイルを options.filename
に指定し、ファイルの中身を options.contents
に設定するようです。また、いずれも strdup()
で得られたアドレスを渡していることから、おそらくはデータはヒープ領域に配置しておく、という制約があるのかもしれません。
150 int main(int argc, char *argv[])
151 {
...
155 /* Set defaults -- we have to use strdup so that
156 fuse_opt_parse can free the defaults if other
157 values are specified */
158 options.filename = strdup("hello");
159 options.contents = strdup("Hello World!\n");
そして、 struct fuse_operations
のメンバ変数に関数ポインタを設定しています。名前から推測するに、 open(2)
や read(2)
で呼び出される関数を指定しているのでしょう。
131 static const struct fuse_operations hello_oper = {
132 .init = hello_init,
133 .getattr = hello_getattr,
134 .readdir = hello_readdir,
135 .open = hello_open,
136 .read = hello_read,
137 };
read(2)
に対応しているであろう関数、 hello_read()
の実装を見てみます。 offset
や len
の計算を行っていたりしますが、肝心なのは124行目の memcpy(buf, options.contents + offset, size);
で、単に options.contents
を memcpy()
することにより、 read(2)
等で読みだされたデータとして返しているようです。
112 static int hello_read(const char *path, char *buf, size_t size, off_t offset,
113 struct fuse_file_info *fi)
114 {
115 size_t len;
116 (void) fi;
117 if(strcmp(path+1, options.filename) != 0)
118 return -ENOENT;
119
120 len = strlen(options.contents);
121 if (offset < len) {
122 if (offset + size > len)
123 size = len - offset;
124 memcpy(buf, options.contents + offset, size);
125 } else
126 size = 0;
127
128 return size;
129 }
ということは、適宜 options.contents
が指すデータを変えてゆくことでファイルを読みだすたびに中身が変化するという挙動を実現できそうです。
サンプル「broccofs」を作成してみる
さっそくFUSEのサンプルを作成してみましょう。「ブロ子の歌」の歌詞を延々と返し続けるファイルとしたいので、"broccofs"という名称にしてみます。サンプルコードは以下のgistに置いてあります。
libfuseのサンプルコード( hello.c
)をベースに作成しているので、broccofsでの変更箇所のみを解説します。
データ構造としては単純で、歌詞データを文字列の配列として保持しておきます。
int main(int argc, char *argv[])
{
...
// FUSEマウント時に表示させるファイル名を設定。
options.filename = strdup("brocco.txt");
// 「ブロ子の歌」データ。
char *messages[] = {
"ブロっ子\n",
"ブロっ子\n",
"ブロッコリー♪\n",
"(ブロっ子)\n",
NULL
};
// 歌詞データの行数をカウントする。
for (options.max = 0; messages[options.max] != NULL; options.max++)
;
// 歌詞データをstrdup()したうえで設定する。
options.messages = malloc(sizeof(char *) * options.count);
if (options.messages) {
for (int i = 0; i < options.max; i++) {
options.messages[i] = strdup(messages[i]);
}
}
options.count = 0;
options.contents = options.messages[0];
そして hello_read()
が呼ばれる度に次に表示する歌詞データを差し替えてゆくという挙動になります。
static int hello_read(const char *path, char *buf, size_t size, off_t offset,
struct fuse_file_info *fi)
{
...
len = strlen(options.contents);
memcpy(buf, options.contents, len);
if (++(options.count) >= options.max) {
options.count = 0;
}
// 参照するデータを切り替える。
options.contents = options.messages[options.count];
return size;
}
一点注意する個所として、 struct fuse_config->kernel_cache
の値を 0
に設定しておくというものがあります。具体的な実装までは追えていませんが、構造体のメンバ変数名から推測するに、カーネル内でのデータキャッシュを行わない・行うという設定のようです。
(この値が 1
(0以外?)になっていると、 options.contents
のデータを差し替えても変更が反映されないという挙動になります)
static void *hello_init(struct fuse_conn_info *conn,
struct fuse_config *cfg)
{
(void) conn;
cfg->kernel_cache = 0;
return NULL;
}
上記の実装・注意点を踏まえたうえでサンプルファイルを作成しました。コンパイルは以下の手順で行えます。
$ gcc -g -o brocco brocco.c `pkg-config fuse3 --cflags --libs`
さっそく実行してみましょう。 hello.c
のサンプルと同様に、ファイル( brocco.txt
)が生えてきています。
$ mkdir mnt01
$ ./brocco ./mnt01
$ ls -l ./mnt01/
合計 0
-r--r--r-- 1 root root 13 1月 1 1970 brocco.txt
そして想定通り、ファイルの中身を表示するたびに「ブロ子の歌」歌詞が表示されています。
$ for i in `seq 1 13` ; do echo "`cat ./mnt01/brocco.txt` `echo '(^_^)/'`" ; done
ブロっ子 (^_^)/
ブロっ子 (^_^)/
ブロッコリー♪ (^_^)/
(ブロっ子) (^_^)/
ブロっ子 (^_^)/
ブロっ子 (^_^)/
ブロッコリー♪ (^_^)/
(ブロっ子) (^_^)/
ブロっ子 (^_^)/
ブロっ子 (^_^)/
ブロッコリー♪ (^_^)/
(ブロっ子) (^_^)/
ブロっ子 (^_^)/
Sixel Graphicsと組み合わせてみる
Sixelという、端末にビットマップグラフィックスを表示させるための拡張機能(というか端末シーケンス)があります。broccofsの発展例としておちこぼれフルーツタルトのアイコン画像をFUSE経由で参照するサンプルを作成してみましょう。
アイコン画像の取得とsixelファイル化
Sixelのフォーマットは、ビットマップを端末のエスケープシーケンスの形で表現したものであるため、PNG等の画像をSixelのエスケープシーケンスに変換しておく必要があります。
変換に必要なツールはsaitoha/libsixelリポジトリで提供されており、以下の手順でビルドします。
$ git clone https://github.com/saitoha/libsixel.git
$ cd libsixel
$ ./configure
$ make
$ file converters/img2sixel
converters/img2sixel: POSIX shell script, ASCII text executable, with very long lines
また、アイコン画像を縮小しておきたいので、ImageMagickも併せてインストールしておきます。
$ sudo dnf install epel-release
$ sudo dnf install ImageMagick
以下のスクリプトでアイコンのダウンロードとサイズ縮小、Sixelフォーマットへの変換を行っておきます。準備はこれで完了です。
#!/bin/sh
BASE_URL='http://ochifuru-anime.com/images/special/003/icon/'
L="
OF_icon_1.png
OF_icon_2.png
OF_icon_3.png
OF_icon_4.png
OF_icon_5.png
OF_icon_8.png
OF_icon_7.png
OF_icon_6.png
"
export LD_LIBRARY_PATH=../libsixel/src/.libs
for i in $L
do
if [ ! -f $i ]; then
echo "===> download $i"
curl -sLO $BASE_URL/$i
convert $i -resize 40% $i.resized
../libsixel/converters/img2sixel $i.resized > $i.resized.sixel
fi
done
ソースコードは以下のgistに置いてあります。
基本的な構造は先述のサンプルと同じで表示する文字列をSixelデータに置き換えた形になります。
int main(int argc, char *argv[])
{
...
options.filename = strdup("icon.txt");
char *icons[] = {
"OF_icon_1.png.resized.sixel",
"OF_icon_2.png.resized.sixel",
"OF_icon_3.png.resized.sixel",
"OF_icon_4.png.resized.sixel",
"OF_icon_5.png.resized.sixel",
"OF_icon_6.png.resized.sixel",
"OF_icon_7.png.resized.sixel",
"OF_icon_8.png.resized.sixel",
NULL
};
for (options.max = 0; icons[options.max] != NULL; options.max++)
;
options.icons = malloc(sizeof(char *) * options.count);
if (options.icons) {
FILE *fp;
for (int i = i ; i < options.max; i++) {
size_t sz = sizeof(char) * (BUFSIZ * 20);
FILE *fp = fopen(icons[i], "r");
options.icons[i] = malloc(sz);
if (options.icons[i] != NULL && fp != NULL) {
fread(options.icons[i], sz, 1, fp);
//fclose(fp); // double-freeが発生して原因が特定できないので(ホントはダメだけど)コメントアウト。
}
}
}
options.count = 0;
options.contents = options.icons[0];
以下の手順でコンパイル・実行します。
$ gcc -g -o of_icon of_icon.c `pkg-config fuse3 --cflags --libs`
$ mkdir mnt02
$ ./of_icon ./mnt02
実行してみると端末にアイコン画像が表示されます!
ls -l
の結果をみると、ファイルを開くたびにサイズが変わる(=内部的に次のデータに切り替えられている)ことが確認できます。
その他:FUSEの実行でエラーになった場合の対応方法
FUSEでアプリを作成している際にプログラムが落ちたりすると、以下のようなエラーが出ることがあります。このエラーがでると、別のディレクトリを指定しない限りFUSEでマウントできない状況に陥ってしまいます。
fuse: bad mount point `./mnt': Transport endpoint is not connected
このような場合は、 fusermount
コマンドでディレクトリをアンマウントすることで問題が解消します。
$ fusermount -u ./mnt
まとめ
FUSEを利用するためのカーネル側・ユーザランド側の設定手順と、FUSEライブラリを用いた簡単なサンプルアプリの紹介をしました。
FUSE側で返したいデータを read(2)
等のタイミングで操作できるのと、比較的シンプルな手順で仮想的なファイルシステムを構築できるので、より面白い応用例を考えたいと思います。