tkokamoの日記

HPCの研究開発なのでそんなことをかきたい

FUSEでfilesystemを使ってみる。

仕事で分散ファイルシステムを作っているんだけども、カーネルレイヤでのコーディングなので、非常に神経使うし面倒(楽しいけれども)。
Filesystem in userspace(FUSE)というものの存在はずいぶん前から認知していて学生時代にsshfsなどで使っていた。
FUSEは実行パスが長いのでカーネル内でに比べるとソフトオーバーヘッドがでかいとか欠点もあるけれども、
イデアを形にするときには重宝すると思ったので、まずは Writing a FUSE Filesystem: a Tutorial に沿って動かしてみる。

fuseのinstall

tutorialは2.9.Xを想定しているみたいだったが、手元のCentOS 7.2でyum使ってinstallしたら問題なくinstallされた。

[root@localhost fuse-tutorial-2018-02-04]# uname -r
3.10.0-693.17.1.el7.x86_64
[root@localhost fuse-tutorial-2018-02-04]# yum install fuse fuse-devel
...
Updated:
  fuse.x86_64 0:2.9.2-11.el7                                                 fuse-devel.x86_64 0:2.9.2-11.el7

Dependency Updated:
  fuse-libs.x86_64 0:2.9.2-11.el7

Complete!

触ってみる

Compiling and Runningに書かれていることをやるだけ。
bbfsのコードはhttp://www.cs.nmsu.edu/~pfeiffer/fuse-tutorial.tgz

# rootではやらせてくれなかったので通常ユーザになって続行

takuya@localhost:~/fuse_tutorial/fuse-tutorial-2018-02-04/example$ ../src/bbfs rootdir/ mountdir/
Fuse library version 2.9
about to call fuse_main
takuya@localhost:~/fuse_tutorial/fuse-tutorial-2018-02-04/example$ ls -lR
.:
total 8
-rw-r--r-- 1 takuya takuya  185 Jul 11 20:31 Makefile
-rw-r--r-- 1 takuya takuya 2003 Jul 11 20:32 bbfs.log
drwxr-xr-x 2 takuya takuya   22 Jul 11 20:31 mountdir
drwxr-xr-x 2 takuya takuya   22 Jul 11 20:31 rootdir

./mountdir:
total 4
-rw-r--r-- 1 takuya takuya 11 Jul 11 20:31 bogus.txt

./rootdir:
total 4
-rw-r--r-- 1 takuya takuya 11 Jul 11 20:31 bogus.txt
takuya@localhost:~/fuse_tutorial/fuse-tutorial-2018-02-04/example$ touch rootdir/hoge ★rootdirにhogeを作った。
takuya@localhost:~/fuse_tutorial/fuse-tutorial-2018-02-04/example$ ls -lR
.:
total 12
-rw-r--r-- 1 takuya takuya  185 Jul 11 20:31 Makefile
-rw-r--r-- 1 takuya takuya 6639 Jul 11 20:32 bbfs.log
drwxr-xr-x 2 takuya takuya   33 Jul 11 20:32 mountdir
drwxr-xr-x 2 takuya takuya   33 Jul 11 20:32 rootdir

./mountdir:
total 4
-rw-r--r-- 1 takuya takuya 11 Jul 11 20:31 bogus.txt
-rw-r--r-- 1 takuya takuya  0 Jul 11 20:32 hoge ★mountdirにもできている!

./rootdir:
total 4
-rw-r--r-- 1 takuya takuya 11 Jul 11 20:31 bogus.txt
-rw-r--r-- 1 takuya takuya  0 Jul 11 20:32 hoge

とりあえず触るのは問題なさそう。

レイテンシはいかに示す通り、FUSEを通すとかなり大きくなっている。
open(create含む)で約9倍、writeで約15倍。操作的にディスクアクセスがないのでソフトオーバーヘッドがもろに聞いている様子。

takuya@localhost:~/fuse_tutorial/fuse-tutorial-2018-02-04/example$ strace -T touch rootdir/a 2>&1 | grep "rootdir/a"
execve("/usr/bin/touch", ["touch", "rootdir/a"], [/* 34 vars */]) = 0 <0.000195>
open("rootdir/a", O_WRONLY|O_CREAT|O_NOCTTY|O_NONBLOCK, 0666) = 3 <0.000063>
takuya@localhost:~/fuse_tutorial/fuse-tutorial-2018-02-04/example$ strace -T touch mountdir/b 2>&1 | grep "mountdir/b"
execve("/usr/bin/touch", ["touch", "mountdir/b"], [/* 34 vars */]) = 0 <0.000290>
open("mountdir/b", O_WRONLY|O_CREAT|O_NOCTTY|O_NONBLOCK, 0666) = 3 <0.000533>

takuya@localhost:~/fuse_tutorial/fuse-tutorial-2018-02-04/example$ strace -T dd if=/dev/zero of=rootdir/file00 bs=1M count=1 2>&1 | grep "write"
write(1, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 1048576) = 1048576 <0.001646>
write(2, "1+0 records in\n1+0 records out\n", 311+0 records in
write(2, "1048576 bytes (1.0 MB) copied", 291048576 bytes (1.0 MB) copied) = 29 <0.000037>
write(2, ", 0.00409826 s, 256 MB/s\n", 25, 0.00409826 s, 256 MB/s
takuya@localhost:~/fuse_tutorial/fuse-tutorial-2018-02-04/example$ strace -T dd if=/dev/zero of=mountdir/file01 bs=1M count=1 2>&1 | grep "write"
write(1, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 1048576) = 1048576 <0.023698>
write(2, "1+0 records in\n1+0 records out\n", 311+0 records in
write(2, "1048576 bytes (1.0 MB) copied", 291048576 bytes (1.0 MB) copied) = 29 <0.000032>
write(2, ", 0.0261279 s, 40.1 MB/s\n", 25, 0.0261279 s, 40.1 MB/s

ディスクから読む場合も3倍程度かかっている。

# 読み込む前にecho 3 > /prco/sys/vm/drop_cachesでキャッシュを破棄する
takuya@localhost:~/fuse_tutorial/fuse-tutorial-2018-02-04/example$ strace -T dd of=/dev/null if=rootdir/file00 bs=1M count=1 2>&1 | grep "read.*1048576"
read(0, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 1048576) = 1048576 <0.031173>
takuya@localhost:~/fuse_tutorial/fuse-tutorial-2018-02-04/example$ strace -T dd of=/dev/null if=mountdir/file01 bs=1M count=1 2>&1 | grep "read.*1048576"
read(0, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 1048576) = 1048576 <0.092250>

少し中を見てみる

普通ファイルシステムはmountコマンド(mountシステムコール)で特定のディレクトリにマウントする。
その中でファイルシステムごとの初期化処理が行われるが、fuseの場合はmountのifはなくプログラムのmain関数で初期化を行う。

# 以下でマウント相当の初期化処理が行われる。
takuya@localhost:~/fuse_tutorial/fuse-tutorial-2018-02-04/example$ ../src/bbfs rootdir/ mountdir/

初期化としてやっているのは、以下の二つの認識
* ファイルシステム固有の初期化処理 * operationの登録 大雑把に言ってしまえばカーネルレイヤで作るFSも基本的には同じことをやっている。

ファイルシステム固有の初期化処理

bbfsは単にファイルアクセスをリダイレクトする(別ディレクトリ上で同じものを見せる)だけなので、大層な初期化は行っていない。

871 int main(int argc, char *argv[])
872 {
873     int fuse_stat;
874     struct bb_state *bb_data;
875
... // ここは入力チェックなど
901     bb_data = malloc(sizeof(struct bb_state)); // bbfsの固有データ構造の獲得
902     if (bb_data == NULL) {
903         perror("main calloc");
904         abort();
905     }
906
907     // Pull the rootdir out of the argument list and save it in my
908     // internal data
909     bb_data->rootdir = realpath(argv[argc-2], NULL); ★

ここでやっているのはrootdir(rootdir/絶対パス)の登録。
ファイルの実態はrootdir/にあるので、mountdir/に対するアクセスがあったらrootdir/にアクセスするために内部でその情報をひかえている。

// 続き
906
907     // Pull the rootdir out of the argument list and save it in my
908     // internal data
909     bb_data->rootdir = realpath(argv[argc-2], NULL);
910     argv[argc-2] = argv[argc-1]; ★
911     argv[argc-1] = NULL;
912     argc--;
913
914     bb_data->logfile = log_open();
915
916     // turn over control to fuse
917     fprintf(stderr, "about to call fuse_main\n");
918     fuse_stat = fuse_main(argc, argv, &bb_oper, bb_data); // operationの登録
919     fprintf(stderr, "fuse_main returned %d\n", fuse_stat);
920
921     return fuse_stat;
922 }

★ではargvargcを少しいじっている。この処理はいらないのでは?と思っている。
あまりfuseの内部構造については知らないがおそらくargv[argc-1]fuseでアクセスするマウントポイント(mountdir/)となることがfuseの仕様で決まっているんだと思う。
最後にoperationの登録、その他を行っている。

operationの登録

個人的にはfuseの一つの大きな利点は、実装するべき関数群がカーネルレイヤで作る場合に比べてとても少ないことだと思っている。
bbfsでは34個の関数を実装しているが、カーネル内で作るのに比べてこれでもかなり少ないはず。
そして、それぞれの関数群が割とユーザが使うIFと一致しているので、カーネル内部の仕組みを理解する必要がほとんどない。 bbfsで登録されているのはbb_operでそのリストは以下の通り。

822 struct fuse_operations bb_oper = {
823   .getattr = bb_getattr,
824   .readlink = bb_readlink,
825   // no .getdir -- that's deprecated
826   .getdir = NULL,
827   .mknod = bb_mknod,
828   .mkdir = bb_mkdir,
829   .unlink = bb_unlink,
830   .rmdir = bb_rmdir,
831   .symlink = bb_symlink,
832   .rename = bb_rename,
833   .link = bb_link,
834   .chmod = bb_chmod,
835   .chown = bb_chown,
836   .truncate = bb_truncate,
837   .utime = bb_utime,
838   .open = bb_open,
839   .read = bb_read,
840   .write = bb_write,
841   /** Just a placeholder, don't set */ // huh???
842   .statfs = bb_statfs,
843   .flush = bb_flush,
844   .release = bb_release,
845   .fsync = bb_fsync,
822 struct fuse_operations bb_oper = {
823   .getattr = bb_getattr,
824   .readlink = bb_readlink,
825   // no .getdir -- that's deprecated
826   .getdir = NULL,
827   .mknod = bb_mknod,
828   .mkdir = bb_mkdir,
829   .unlink = bb_unlink,
830   .rmdir = bb_rmdir,
831   .symlink = bb_symlink,
832   .rename = bb_rename,
833   .link = bb_link,
834   .chmod = bb_chmod,
835   .chown = bb_chown,
836   .truncate = bb_truncate,
837   .utime = bb_utime,
838   .open = bb_open,
839   .read = bb_read,
840   .write = bb_write,
841   /** Just a placeholder, don't set */ // huh???
842   .statfs = bb_statfs,
843   .flush = bb_flush,
844   .release = bb_release,
845   .fsync = bb_fsync,

試しにopen処理を見てみる。

281
282 /** File open operation
283  *
284  * No creation, or truncation flags (O_CREAT, O_EXCL, O_TRUNC)
285  * will be passed to open().  Open should check if the operation
286  * is permitted for the given flags.  Optionally open may also
287  * return an arbitrary filehandle in the fuse_file_info structure,
288  * which will be passed to all file operations.
289  *
290  * Changed in version 2.2
291  */
292 int bb_open(const char *path, struct fuse_file_info *fi)
293 {
294     int retstat = 0;
295     int fd;
296     char fpath[PATH_MAX];
297
298     log_msg("\nbb_open(path\"%s\", fi=0x%08x)\n",
299             path, fi);
300     bb_fullpath(fpath, path); (1)
301
302     // if the open call succeeds, my retstat is the file descriptor,
303     // else it's -errno.  I'm making sure that in that case the saved
304     // file descriptor is exactly -1.
305     fd = log_syscall("open", open(fpath, fi->flags), 0); (2)
306     if (fd < 0)
307         retstat = log_error("open");
308
309     fi->fh = fd;
310
311     log_fi(fi);
312
313     return retstat;
314 }

やっていることは、(1)実際に読み込むrootdir/配下のパス名の作成、(2)open(2)の実行だけ。
(bbfsは言ってしまえば何もやっていないので、実際に独自のファイルシステムを作ろうとするとこの程度ではすまない)

fuseを使ってやろうとしていること

ファイルシステムの研究開発をしていているので、既存研究のシステムを使ってみたいことはある。
が、まぁやはり研究用のソースなのでまともに動かなくて萎えてしまうことが多い。
fuseをちょろっと見た感じ非常に簡単にPoCが作れそうなので、動かないなら余暇につくってしまおう、というのが魂胆。