panda's tech note

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_getattradvfs_readdiradvfs_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プロセスをフォアグランド実行するため DockerfileCMD を以下のように書き換えます。

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 コマンドを実行すると以下のように /mnt4096 ブロックの空き領域を持ったファイルシステムが確認できます。

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もどきを実装しようと思います。