これはRust Advent Calendar 2017 11日目の記事です。
今回はRustを用いてファイルシステムを作ることができるrust-fuseを紹介します。
FUSE(Filesystem in Userspace)とは?
FUSEは、通常カーネル空間で提供するファイルシステムの機能をユーザ空間上で定義し動作させることができるものです。
ユーザ空間とカーネル空間の橋渡しをするインターフェースとカーネル内での処理を提供しています。実装者はカーネルのコードをいじることなくファイルシステムを作成できます。
リファレンス実装はlibfuseとして提供されています。
FUSEを使ったファイルシステムとしては、SSHFSやGlusterFS、gitfsなどがあります。
rust-fuse
rust-fuseはRustでFUSEを実装できることを目的としたcrateで、単純なlibfuseのバインディングではなくRustに最適化した形で機能を提供しています。
rust-fuseの内部実装について日本語で説明されているページが以下にあります。
興味ある方は見てみるとよいのではないでしょうか。
- http://akiradeveloper.hatenadiary.com/entry/2015/01/12/142641
- http://akiradeveloper.hatenadiary.com/entry/2015/01/12/161501
ちなみに現時点でのrust-fuseの安定版はFUSE2.6(FUSE Kernel ABI 7.8)ベースとなっており、Linuxでは2.9.1、macOS(OSXFUSE)では3.0(FUSE Kernel ABI 7.19)への対応をおこなっている最中のようです。
最小構成で動かしてみる
前置きが長くなりましたが、手始めに何もしないファイルシステムを作ってみましょう。
[dependencies]
fuse = "0.3"
env_logger = "0.3"
env_loggerは必須ではありませんが、rust-fuseにもloggerでログが埋め込まれているので、env_logger::init()
して環境変数RUST_LOG
にdebug
あたりを渡しておくと動作確認が楽になります。
extern crate env_logger;
extern crate fuse;
use std::env;
use fuse::Filesystem;
struct EmptyFS;
impl Filesystem for EmptyFS {}
fn main() {
env_logger::init().expect("fail logger init");
let mountpoint = env::args_os().nth(1).expect("usage: emptyfs MOUNTPOINT");
fuse::mount(EmptyFS, &mountpoint, &[]).expect("fail mount()");
}
何もないfuse::Filesystem
トレイトを定義します。
fsroot
ディレクトリをマウントポイントにして起動してみます。
なお、動作環境はArch Linuxを使っています。
$ cargo build --release
$ mkdir fsroot
$ RUST_LOG=debug ./target/release/emptyfs fsroot
INFO:fuse::session: Mounting fsroot
DEBUG:fuse::request: INIT(1) kernel: ABI 7.26, flags 0x1ffffb, max readahead 131072
DEBUG:fuse::request: INIT(1) response: ABI 7.8, flags 0x1, max readahead 131072, max write 16777216
アンマウントはfusermount
コマンドで行えます。
$ fusermount -u fsroot
$ RUST_LOG=debug ./target/release/emptyfs fsroot
:
INFO:fuse::session: Unmounted /vagrant/emptyfs/fsroot
$
初期処理と終了処理
init()
とdestroy()
を定義することで、初期処理と終了処理を行うことができます。
use std::os::raw::c_int;
use fuse::{Filesystem, Request};
struct EmptyFS;
impl Filesystem for EmptyFS {
fn init(&mut self, _req: &Request) -> Result<(), c_int> {
println!("my init");
Ok(())
}
fn destroy(&mut self, _req: &Request) {
println!("my destroy");
}
}
「できます」と書きつつ、初期処理はうまく動いたのですが、destroy()
はアンマウントしたりプロセス終了しただけでは呼ばれませんでした。
トリガをご存知の方教えてください。
メモリFS
FUSEでできることを知るために、もう少し大きな例を見ていきましょう。
ここでは、FUSEプロセス(メモリ)上だけでファイル管理するメモリFSもどきを作ってみます。
ちなみに、こちらのリポジトリに今回のメモリFSもどきのコードを置いていますので、興味ある方はみてみてください。
ディレクトリ参照
まずはルートディレクトリをls
コマンドで参照できるようにしてみましょう。
ファイルシステム用の構造体としてMemFS
を定義します。
iノード番号をキーに、 親のiノード番号とパス名、rust-fuseでファイル属性を定義するfuse::FileAttr
にアクセスできるようにしておきます。(inodes
)
struct MemFS {
// <ino, (parent_ino, path, fileattr)>
inodes: HashMap<u64, (u64, String, FileAttr)>,
}
impl Filesystem for MemFS {
fn getattr(&mut self, _req: &Request, ino: u64, reply: ReplyAttr) {
for (&inode, f) in self.inodes.iter() {
if ino == inode {
reply.attr(&TTL, &f.2);
return;
}
}
// 何も返すものがない場合はlibc::ENOENTを返しておく
reply.error(ENOENT);
}
fn readdir(&mut self, _req: &Request, _ino: u64, _fh: u64, offset: i64, mut reply: ReplyDirectory) {
// offset?
// see also: https://github.com/libfuse/libfuse/blob/master/include/fuse.h#L535
// readdirには2つのモードがあるらしく、offsetが 0 or non-0 でモードが分かれる。
// ここではoffset==0の時のみ処理させる
if offset == 0 {
// 今は固定でカレントディレクトリと親ディレクトリの情報のみを返しておく
reply.add(1, 0, FileType::Directory, ".");
reply.add(2, 1, FileType::Directory, "..");
}
reply.ok();
}
}
該当iノード番号のファイル属性を取得するためにgetattr()
コールバックが呼ばれるのと、
ディレクトリ読み出しするためにreaddir()
コールバックが呼ばれるので、それぞれ定義しておきます。
rust-fuseではコールバックに対する応答(ReplyXX
)でファイルシステム上の情報を返すようになっています。
getattr()
ではReplyAttr.attr()
で該当ファイルのファイル属性を返します。
readdir()
ではReplyDirectory.add()
で該当エントリの情報を追加しReplyDirectory.ok()
で情報を確定します。
reply.attr()
の第1引数TTLはgetattr()
の結果をキャッシュする時間を指定します。後ほど説明するlookup()
にも同様の結果をキャッシュする仕組みがあります。
MemFS
ではマウント前にルートディレクトリ用のデータを作成してからマウントします。
fn file_create(ino: u64, size: u64, ftype: FileType) -> FileAttr {
let t = time::now().to_timespec();
FileAttr {
ino: ino,
size: size,
blocks: 0,
atime: t,
mtime: t,
ctime: t,
crtime: t,
kind: ftype,
perm: match ftype {
FileType::Directory => 0o755,
_ => 0o644,
},
nlink: 2,
uid: 501,
gid: 20,
rdev: 0,
flags: 0
}
}
fn main() {
env_logger::init().expect("fail logger init");
let mountpoint = env::args_os().nth(1).expect("usage: memfs MOUNTPOINT");
let mut inodes = HashMap::new();
// ルートディレクトリを作っておく
inodes.insert(0, ("/".to_string(), file_create(1, 0, FileType::Directory)));
// マウント
fuse::mount(MemFS{inodes: inodes}, &mountpoint, &[]).expect("fail mount()");
}
$ RUST_LOG=debug ./target/release/memfs fsroot
INFO:fuse::session: Mounting fsroot
DEBUG:fuse::request: INIT(1) kernel: ABI 7.26, flags 0x1ffffb, max readahead 131072
DEBUG:fuse::request: INIT(1) response: ABI 7.8, flags 0x1, max readahead 131072, max write 16777216
ls
コマンドを実行し参照できるか試してみます。
最初の何もしないEmptyFSの場合は
$ ls -al fsroot
ls: cannot access 'fsroot': Function not implemented
となってしまいますが、MemFSの場合は
$ ls -a fsroot
total 0
drwxr-xr-x 2 501 utmp 0 Dec 11 00:05 .
drwxr-xr-x 1 vagrant vagrant 306 Dec 10 22:49 ..
となり、無事ディレクトリが読み出せました。
ファイル作成
次にtouch
コマンドを実行し、ファイルを作成できるようにしてみましょう。
ファイル作成時は、ファイルの存在をチェックするlookup()
コールバックと、ファイル作成するcreate()
コールバック、ファイル属性を設定するsetattr()
コールバックが呼ばれるのでこれらを定義しておきます。
impl Filesystem for MemFS {
fn lookup(&mut self, _req: &Request, parent: u64, name: &OsStr, reply: ReplyEntry) {
for (_, f) in self.inodes.iter() {
if f.0 == parent && name.to_str().unwrap() == f.1.as_str() {
reply.entry(&TTL, &f.2, 0);
return;
}
}
reply.error(ENOENT);
}
fn create(&mut self, _req: &Request, parent: u64, name: &OsStr, _mode: u32, _flag: u32, reply: ReplyCreate) {
let inode = time::now().to_timespec().sec as u64;
let f = file_create(inode, 0, FileType::RegularFile);
self.inodes.insert(inode, (parent, name.to_str().unwrap().to_string(), f));
reply.created(&TTL, &f, 0, 0, 0);
}
fn setattr(&mut self, _req: &Request, ino: u64, _mode: Option<u32>, _uid: Option<u32>,
_gid: Option<u32>, _size: Option<u64>, _atime: Option<Timespec>,
_mtime: Option<Timespec>, _fh: Option<u64>, _crtime: Option<Timespec>,
_chgtime: Option<Timespec>, _bkuptime: Option<Timespec>, _flags: Option<u32>,
reply: ReplyAttr) {
match self.inodes.get(&ino) {
Some(f) => reply.attr(&TTL, &f.2),
None => reply.error(EACCES),
}
}
}
$ touch fsroot/foo # 変更前
touch: cannot touch 'fsroot/foo': Function not implemented
$
$ touch fsroot/foo # 変更後
$
ファイル作成ができましたと言いたいところですが、このままだとls
コマンドでファイル自体を確認することができません。
readdir()
を変更して作成したファイルが見えるようにしてみます。
impl Filesystem for MemFS {
fn readdir(&mut self, _req: &Request, ino: u64, _fh: u64, offset: i64, mut reply: ReplyDirectory) {
if offset > 0 {
reply.ok();
return;
}
reply.add(1, 0, FileType::Directory, ".");
reply.add(2, 1, FileType::Directory, "..");
let mut reply_add_offset = 2;
for (_, f) in self.inodes.iter() {
if ino == f.0 {
let attr = f.2;
let name = &f.1;
reply.add(attr.ino, reply_add_offset, attr.kind, name);
reply_add_offset += 1;
}
}
reply.ok();
}
}
$ touch fsroot/foo
$ ls fsroot -al
total 0
drwxr-xr-x 2 501 utmp 0 Dec 11 13:35 .
drwxr-xr-x 1 vagrant vagrant 306 Dec 11 02:49 ..
-rw-r--r-- 1 501 utmp 0 Dec 11 13:35 foo
無事、作成したファイルが参照できました。
ファイルの読み書き
続いて、ファイルの読み書きをできるようにしてみます。
echo
コマンドリダイレクトでファイル書き込み、そのファイルをcat
コマンドで読み出してみます。
$ echo "hogehoge" > fsroot/bar
-bash: echo: write error: Function not implemented
$ cat fsroot/bar
cat: fsroot/bar: Function not implemented
ファイル内のデータを格納するエリアとしてMemFSにdatas
メンバを追加します。
iノード番号をキーにデータを保持します。雑にString
にしています。
struct MemFS {
// <ino, (parent_ino, path, fileattr)>
inodes: HashMap<u64, (u64, String, FileAttr)>,
// <ino, file_data>
datas: HashMap<u64, String>,
}
ファイル読み書き時のコールバック関数はwrite()
とread()
です。
以下のように定義します。
impl Filesystem for MemFS {
fn write(&mut self, _req: &Request, ino: u64, _fh: u64, _offset: i64, data: &[u8], _flags: u32, reply: ReplyWrite) {
// 書き込み対象のデータ長を保持
let length: usize = data.len();
// Stringにしてdatasに格納する
let x = String::from_utf8(data.to_vec()).expect("fail to-string");
self.datas.insert(ino, x);
if let Some(f) = self.inodes.get_mut(&ino) {
let parent_ino = f.0;
let name = f.1.clone();
*f = (parent_ino, name, file_create(ino, length as u64, FileType::RegularFile));
}
// 書き込みサイズを伝える
reply.written(length as u32);
}
fn read(&mut self, _req: &Request, ino: u64, _fh: u64, _offset: i64, _size: u32, reply: ReplyData) {
match self.datas.get(&ino) {
Some(x) => reply.data(x.as_bytes()), // 読み出しデータ内容を伝える
None => reply.error(EACCES), // 雑にlibc::EACCES返す
}
}
}
実行結果は以下の通りです。
$ echo "hogehoge" > fsroot/bar
$ cat fsroot/bar
hogehoge
$ ls -la fsroot/
total 0
drwxr-xr-x 2 501 utmp 0 Dec 11 20:55 .
drwxr-xr-x 1 vagrant vagrant 306 Dec 11 02:49 ..
-rw-r--r-- 1 501 utmp 9 Dec 11 20:55 bar
$ ls -la fsroot/bar
-rw-r--r-- 1 501 utmp 9 Dec 11 20:55 fsroot/bar
$
ファイルを読み書きできました。
その他のコールバック関数
今回紹介しなかった主なコールバック関数を紹介します。
コールバック関数 | 概要 |
---|---|
symlink() | シンボリックリンク作成時(ln -s等) |
link() | ハードリンク(ln等) |
unlink() | 削除(rm等) |
mkdir() | ディレクトリ作成(mkdir等) |
rmdir() | ディレクトリ削除(rmdir等) |
rename() | リネーム(mv等) |
詳しくはrust-fuseのドキュメントを参照するとよいでしょう。
Rustによるファイルシステム実装あれこれ
RustによるFUSEなプロジェクトは以下のようなものがあります。
- anowell/netfuse rust-fuseを使ったネットワークバックエンドなファイルシステム
- kahing/catfs 適度にキャッシュしてくれるファイルシステム?
- wfraser/fuse-mt マルチスレッドなrust-fuse
おわりに
かなり早足でしたがRustでFUSEをさわる手順を説明しました。
みなさんもRustで自分のファイルシステムを実装してみてはいかがでしょうか?