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 }
★ではargv
、argc
を少しいじっている。この処理はいらないのでは?と思っている。
あまり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が作れそうなので、動かないなら余暇につくってしまおう、というのが魂胆。