FUSE が気になっていたので触ってみたメモ。
FUSE
FUSE(Filesystem in Userspace) とは、ざっくり言えばユーザーランドで独自のファイルシステムを作れる機能を提供するソフトウェア。
人生において自作OSや自作言語、自作静的解析器等々、車輪の再開発を行いたいものは様々である。
自分が何も自作していないことに気づいて「人生とは…」となる。
そんな何も自作したことがない人でも FUSE を使うことでお手軽に自作ファイルシステムを作ることができる。
(まぁ後述するように実際はカーネルへの橋渡し的な実装を行うものなのでファイルシステムと呼んでいいかは微妙だが…)
FUSE はどこで使われているか
SSH先のディレクトリをマウントする SSHFS やGMailをファイルシステムとして扱える GMailfs などで利用されている。
ディレクトリやファイルの読み書きを実装するので、例えば /tweet.txt
に文字列を書き出すことでツイートすることや Twitter のタイムラインを /tl.txt
に書き込むといったことがファイルシステムという形で実現できる。
Twitter の例はモチベーションがないかもしれないが、例えばS3専用のファイルシステムとか作ると面白いかもしれない。
とはいえ、速度は遅いので用途は限られる。
FUSE のアーキテクチャ
見ての通り FUSE 自体はカーネルランドで動作し、glibc と VFS の橋渡し役となっている。
FUSE による自作ファイルシステムは libfuse を利用することで動作し、glibc における open()
や read()
を libfuse がいい感じに置き換えるという感じ。
FUSE でファイルシステムを作る
まずは特定のファイル( /file
)を読み込めるだけの機能を持ったファイルシステムを作ってみる。
また、簡単のため、 file
はメモリ上に存在させる。
fuse_main
FUSE は fuse_main
に fuse_operations
構造体を登録することで動作する。これだけでコマンドオプションをパースしてくれたり、プログラム終了時にファイルシステムを unmount してくれたりする。
fuse_main(argc, argv, op, private_data)
op
には fuse_operations
構造体を登録する。
この構造体にはいわゆる UNIX でいう open
や read
にあたる operation を登録する。
ファイルを読み込むだけならば最低限必要なのは次の4つ。
open
read
- http://libfuse.github.io/doxygen/structfuse__operations.html#a272960bfd96a0100cbadc4e5a8886038
- ファイルの内容を返すときの callback 関数
readdir
getattr
- http://libfuse.github.io/doxygen/structfuse__operations.html#ac39a0b7125a0e5001eb5ff42e05faa5d
stat()
みたいなもの。ファイルの属性を取得する callback 関数
static struct fuse_operations fuse_my_operations = {
.open = open_callback,
.read = read_callback,
.readdir = readdir_callback,
.getattr = getattr_callback,
};
int main(int argc, char *argv[])
{
return fuse_main(argc, argv, &fuse_my_operations, NULL);
}
open_callback
ファイルオープンを要求した際に実行される callback 関数。
今回は /file
以外であれば -ENOENT
を返すだけの処理にする。
static int open_callback(const char *path, struct fuse_file_info *fi) {
if (strcmp(path, filepath) != 0) {
return -ENOENT;
}
printf("open %s\n", path);
return 0;
}
read_callback
FUSE が開いたファイルを読み込むときに実行される callback 関数。
次のことを実装すれば良い。
- 戻り値はファイルのサイズ
- ファイルがない場合は
-ENOENT
をきちんと返してあげる - 第2引数の
buf
にファイルの内容を書き込む
static int read_callback(const char *path, char *buf, size_t size, off_t offset, struct fuse_file_info *fi) {
printf("read %s\n", path);
size_t len;
if (strcmp(path, filepath) != 0) {
return -ENOENT;
}
len = strlen(filecontent);
// 読み込み開始位置がファイルの長さを超えていないかチェック
if (offset >= len) {
return 0;
}
// ファイルの長さよりバッファサイズが大きければ
// ファイルの終わりまでの長さにする
if (offset + size > len) {
size = (len - offset);
}
memcpy(buf, filecontent + offset, size);
return size;
}
readdir_callback
ディレクトリを読み込み、構造を FUSE に伝えるための callback 関数。
- filler は fuse_fill_dir_t 関数
- 単一のディレクトリのみ扱うには filler の offset に 0 は渡すだけでもよい
static int readdir_callback(const char *path, void *buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info *fi) {
filler(buf, ".", NULL, 0);
filler(buf, "..", NULL, 0);
filler(buf, filename, NULL, 0);
return 0;
}
getattr_callback
ファイルの属性を読み取るときに呼ばれる callback 関数。
stat()
みたいなものだがst_dev
とst_blksize
はない。- 第3引数の
stbuf
にstat
構造体を書き込む
ここの処理を変えれば、例えば ls -al
したときにファイルサイズを全然別のサイズに見せる、といったことができる。
static int getattr_callback(const char *path, struct stat *stbuf) {
memset(stbuf, 0, sizeof(struct stat));
printf("attr read %s\n", path);
if (strcmp(path, "/") == 0) {
stbuf->st_mode = S_IFDIR | 0755;
stbuf->st_nlink = 2;
stbuf->st_uid = getuid();
stbuf->st_gid = getgid();
printf("attr read %s\n", path);
return 0;
}
if (strcmp(path, filepath) == 0) {
stbuf->st_mode = S_IFREG | 0777;
stbuf->st_nlink = 1;
stbuf->st_size = strlen(filecontent);
stbuf->st_uid = getuid();
stbuf->st_gid = getgid();
return 0;
}
return -ENOENT;
}
コード全体
#define FUSE_USE_VERSION 26
#include <fuse.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
static const char *filepath = "/file";
static const char *filename = "file";
static const char *filecontent = "wei\n";
static int getattr_callback(const char *path, struct stat *stbuf) {
memset(stbuf, 0, sizeof(struct stat));
printf("attr read %s\n", path);
if (strcmp(path, "/") == 0) {
stbuf->st_mode = S_IFDIR | 0755;
stbuf->st_nlink = 2;
stbuf->st_uid = getuid();
stbuf->st_gid = getgid();
printf("attr read %s\n", path);
return 0;
}
if (strcmp(path, filepath) == 0) {
stbuf->st_mode = S_IFREG | 0777;
stbuf->st_nlink = 1;
stbuf->st_size = strlen(filecontent);
stbuf->st_uid = getuid();
stbuf->st_gid = getgid();
return 0;
}
return -ENOENT;
}
static int readdir_callback(const char *path, void *buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info *fi) {
filler(buf, ".", NULL, 0);
filler(buf, "..", NULL, 0);
filler(buf, filename, NULL, 0);
return 0;
}
static int open_callback(const char *path, struct fuse_file_info *fi) {
if (strcmp(path, filepath) != 0) {
return -ENOENT;
}
printf("open %s\n", path);
return 0;
}
static int read_callback(const char *path, char *buf, size_t size, off_t offset, struct fuse_file_info *fi) {
printf("read %s\n", path);
size_t len;
if (strcmp(path, filepath) != 0) {
return -ENOENT;
}
len = strlen(filecontent);
// 読み込み開始位置がファイルの長さを超えていないかチェック
if (offset >= len) {
return 0;
}
// ファイルの長さよりバッファサイズが大きければ
// ファイルの終わりまでの長さにする
if (offset + size > len) {
size = (len - offset);
}
memcpy(buf, filecontent + offset, size);
return size;
}
static struct fuse_operations fuse_my_operations = {
.getattr = getattr_callback,
.open = open_callback,
.read = read_callback,
.readdir = readdir_callback,
};
int main(int argc, char *argv[])
{
return fuse_main(argc, argv, &fuse_my_operations, NULL);
}
上記のコードをコンパイルし実行する。
$ mkdir /tmp/example
$ ./bin/fuse-sample -s -f /tmp/example
$ cd /tmp/example
$ ls -la file
-rwxrwxrwx 1 kohei.morita 2033490572 4 1 1 1970 file
$ cat file
wei
FUSE は Go や Python など著名な言語での Binding が存在するため、もっと気軽にオレオレ FS を作ることができる。
例えば、次のようなものを作ると面白いかもな、と思っている。
- ファイルが作成された段階で libclamav を用いてマルウェアスキャン
- HTTP リクエストのテキストを作成すると送信してレスポンスを書き込んでくれる
- 書き込んだファイルを private gist として作成してくれるもの