Advent Calendar 2019: advfs
Day 2: FUSE と最小構成
今日はこれからファイルシステムの開発に使用するコアライブラリである FUSE について説明します。FUSE は Filesystem in Userspace の略と言われています。ファイルシステムは通常のUNIX系システムでは,カーネルまたはカーネルモジュールに実装されます。そのため,従来のUNIX系のシステム(MINIXなどの純粋なマイクロカーネルなOSは除く)では独自のファイルシステムを作成するには,カーネルまたはカーネルモジュールのシステムプログラミングが必要でした。
FUSEは,ファイルシステムユーザ空間で実装するためのカーネルモジュールおよびライブラリです。カーネルまたはカーネルモジュールとして実装されるファイルシステムでは,共通APIとしてVFS (Virtual Filesystem)を用いることが多いですが、FUSEでは,ファイルシステム操作APIを struct fuse_operations
という構造体で定義します。VFSはOSごとに定義が異なるので説明を省略しますが,ファイル名(パス)の代わりに inode または vnode により識別するオブジェクトとしてファイルまたはディレクトリを扱い,ファイル名(パス)から inode または vnode への変換を行った後に,それらに対する操作を定義します。FUSEではファイル名(パス)に対する操作を定義します。
この struct fuse_operations
の具体的な中身は fuse_operations Struct Reference にまとめられています。
今回は最低限のオペレーションとして,ファイルまたはディレクトリの属性を取得する getattr
,ディレクトリの内容リストを取得する readdir
,ファイルシステムの状態を取得する statfs
を以下のように定義します。
static struct fuse_operations advfs_oper = {
.getattr = advfs_getattr,
.readdir = advfs_readdir,
.statfs = advfs_statfs,
};
advfs_getattr
,advfs_readdir
,advfs_statfs
はそれぞれ関数名です。つまり, struct fuse_operations
の各メンバは関数ポインタとなっています。
getattr
特定のパスで指定されたファイルまたはディレクトリの属性を取得する関数です。API定義は以下の通りです。
int getattr(const char *path, struct stat *stbuf);
第一引数には,ファイルまたはディレクトリへのパス,第二引数には struct stat
のポインタ型の変数です。第二引数は,このパスのファイルまたはディレクトリの属性を返すために使用します。
今回は以下のコードのようにマウントした最上位ディレクトリ /
のみを正常なディレクトリとし,それ以外は存在しないエントリとします。最上位ディレクトリ /
に対しては権限を 0777
(全てのユーザ・グループ・その他ユーザに読み書きおよび実行権限を与える)設定にしています。また,このディレクトリに含まれるディレクトリ・ファイル(st_nlink
)はこのディレクトリ .
と一段階上の親ディレクトリ ..
があるので 2
とします。また,uid
および gid
はFUSEを実行したコンテキストとします。
if ( strcmp(path, "/") == 0 ) {
stbuf->st_mode = S_IFDIR | 0777;
stbuf->st_nlink = 2;
stbuf->st_uid = ctx->uid;
stbuf->st_gid = ctx->gid;
status = 0;
} else {
status = -ENOENT;
}
readdir
次に readdir
API を定義します。 readdir
APIのプロトタイプ宣言は
int readdir(const char *path, void *buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info *fi)
で定義されています。第一引数は対象のディレクトリ,第二引数はディレクトリ内のファイル・ディレクトリリストを返す(保存する)バッファです。また第三引数はこのバッファにファイル・ディレクトリ情報を保存するための関数ポインタ(使用方法については下記参照)です。第四引数は今回は 0
として扱います。マニュアルには
The filesystem may choose between two modes of operation:
1) The readdir implementation ignores the offset parameter, and passes zero to the filler function's offset. The filler function will not return '1' (unless an error happens), so the whole directory is read in a single readdir operation.
2) The readdir implementation keeps track of the offsets of the directory entries. It uses the offset parameter and always passes non-zero offset to the filler function. When the buffer is full (or an error happens) the filler function will return '1'.
とありますが,1)
の方針で実装します。
今回は最上位ディレクトリ /
のみを定義するため,これ以外のパスについては, ENOENT
エラーを返すようにします。そして,上述した通り,このディレクトリ .
と一段階上の親ディレクトリ ..
を設定するため,FUSEの filler
関数で .
および ..
を当該ディレクトリからのリンクとして定義します。
if ( strcmp(path, "/") == 0 ) {
filler(buf, ".", NULL, 0);
filler(buf, "..", NULL, 0);
return 0;
}
return -ENOENT;
statfs
最後にファイルシステム全体の情報を取得する statfs
APIを実装します。以下の通り,ブロックサイズ f_bsize
を 4 KiB に設定します。また,フラグメントサイズ f_frsize
をブロックサイズと同じ 4096
として,ブロック数 f_blocks
(フラグメントサイズ単位)で 1024
,空きブロック数 f_bfree
および(非特権ユーザの)利用可能ブロック数 f_bavail
をそれぞれ 1024
とします。この値は実装するファイルシステムと実際の利用領域に対して変更すべきですが,今回はこの値を利用します。
FUSEもカーネルモジュールとして実装されているため,「パス」と書いていたオブジェクトも実際には inode 単位で扱われます。f_files
はこのファイルシステム上で扱う inode 数,f_ffree
は空き inode 数,f_favail
は(非特権ユーザの)利用可能 inode 数を定義しています。 f_fsid
はファイルシステムIDを表し,f_flag
はマウントフラグを保持します。また, f_namemax
はファイル名の最大長を定義します。今回は特にファイルシステムの中身を考えずに適当に値を割り当てています。次回以降に適宜説明する予定です。
memset(buf, 0, sizeof(struct statvfs));
buf->f_bsize = 4096;
buf->f_frsize = 4096;
buf->f_blocks = 1024; /* in f_frsize unit */
buf->f_bfree = 1024;
buf->f_bavail = 1024;
buf->f_files = 1000;
buf->f_ffree = 100;
buf->f_favail = 100;
buf->f_fsid = 0;
buf->f_flag = 0;
buf->f_namemax = 255;
return 0;
main関数とDockerfile
上述の3関数を advfs_getattr
, advfs_readdir
, advfs_statfs
として実装した段階で,main関数から以下のように fuse_main()
を呼びます。fuse_main()
の引数に argc
および argv
があることからも分かると思いますが,例えば,今回 Dockerfile
内で指定するプロセスのフォアグラウンドオプション -f
など引数の処理も fuse_main()
内で行われるため,特に自分で実装することはありません。
static struct fuse_operations advfs_oper = {
.getattr = advfs_getattr,
.readdir = advfs_readdir,
.statfs = advfs_statfs,
};
int
main(int argc, char *argv[])
{
return fuse_main(argc, argv, &advfs_oper, NULL);
}
具体的な実装は src/main.c を参照してください。
昨日は Docker コンテナで bash
を実行しましたが,今日は直接今回実装したFUSEプロセスをフォアグランド実行するため Dockerfile
の CMD
を以下のように書き換えます。
CMD ["./advfs", "/mnt", "-f"]
なお,fuse_main()
内と扱われるオプションは以下の通りです。
$ ./advfs --help-[master]
usage: ./advfs mountpoint [options]
general options:
-o opt,[opt...] mount options
-h --help print help
-V --version print version
FUSE options:
-d -o debug enable debug output (implies -f)
-f foreground operation
-s disable multi-threaded operation
fuse: no mount point
実行
今回実装したプログラムをビルド・実行するために
$ docker-compose build && docker-compose up -d
を実行します。これで,FUSEで実装したファイルシステムを /mnt
にマウントしたコンテナが立ち上がります。
次に
$ docker exec -it advfs_advfs_1 bash
と実行することで, /mnt
に実装したファイルシステムをマウントしたコンテナで bash
を実行できます。例えばこのシェルの中で, df
コマンドを実行すると以下のように /mnt
に 4096
ブロックの空き領域を持ったファイルシステムが確認できます。
root@f6d4a1717635:/usr/src# df
Filesystem 1K-blocks Used Available Use% Mounted on
overlay 18745336 5148944 12605668 30% /
tmpfs 65536 0 65536 0% /dev
tmpfs 2022544 0 2022544 0% /sys/fs/cgroup
/dev/sda1 18745336 5148944 12605668 30% /etc/hosts
shm 65536 0 65536 0% /dev/shm
tmpfs 2022544 0 2022544 0% /proc/acpi
tmpfs 2022544 0 2022544 0% /proc/scsi
tmpfs 2022544 0 2022544 0% /sys/firmware
advfs 4096 0 4096 0% /mnt
また,わかりにくいですが, /mnt
に対して ls
コマンドを実行すると,以下の通り, .
および ..
ディレクトリが列挙されます。
root@f6d4a1717635:/usr/src# ls -al /mnt/
total 4
drwxrwxrwx 2 root root 0 Jan 1 1970 .
drwxr-xr-x 1 root root 4096 Dec 2 14:31 ..
今日のまとめと明日の予定
今日は最低限の3 APIを実装しました。明日はメモリ上のファイルシステムramfsもどきを実装しようと思います。