はじめに
3行で
この記事は、RustのFUSEインターフェースである fuse-rs
を用いてFUSEを使ったファイルシステムの実装に挑戦し、得られた知見などを記録したものです。
複数回に分けて、徐々に関数を実装していきます。今回は、あらかじめ用意されたファイルをreadできるようになる所までいきます。
作成したファイルシステムの最新版は こちらに置いてあります。
記事一覧
Rustで学ぶFUSE (1) リードオンリーなファイルシステムの実装
Rustで学ぶFUSE (2) ファイルの書き込み
Rustで学ぶFUSE (3) ファイル作成と削除
Rustで学ぶFUSE (4) ディレクトリ作成と削除
Rustで学ぶFUSE (5) 名前の変更と移動
概要
Filesystem in Userspace (FUSE) はLinuxのユーザ空間でファイルシステムを実現する仕組みです。
一般的にファイルシステムを作るというと、カーネルモジュールを作成しなければならないのでいろいろと苦労が多いですが、FUSEを使えば比較的楽に実装できます。
また、HDDなどの実デバイスに直接読み書きするだけでなく、ネットワークストレージを利用した仮想的なファイルシステムを作るのにも都合がよいです。
そんな訳で、FUSEを使ったSSH接続先をマウントできるソフトウェア や AWS S3をマウントできるソフトウェアといった「読み書きできる何かをファイルシステムとしてマウント出来るようにするソフトウェア」があれこれと存在します。
上記のようなソフトウェアの代表例である sshfs
や s3fs
は使った事のある人もいるのではないでしょうか。
元々はLinuxの一部でしたが、MacOSやFreeBSDでも使用できます。最近ではWindowsのWSL2でも使えるようになるようです。WSLの導入の手間が要るとはいえ、WindowsでもFUSEが使えるのは嬉しいですね。
ちなみにWindowsには、Fuseに似た仮想ファイルシステムである Dokan もあります。
ただし、カーネルモジュールを作るより楽とはいえ、FUSEを使ったソフトウェアを作成するのは大変です。
ある程度ファイルシステムの知識は必要ですし、何か調べようとしてもドキュメントが少なく、チュートリアルを見てもほどほどの所で終わってしまい、「あとはサンプルとsshfsの実装などを見てくれ!」とコードの海に投げ出されます。
そこで、各所の情報をまとめつつ、自分で0からファイルシステムを実装して気をつける点などを見つけていきます。
ついでにRustも学べればいいな…という欲を出して、実装はRustを使っていきます。
参考資料
Rust FUSE : Rust版Fuseインターフェースのプロジェクト
libfuse : C版のFuseインターフェースライブラリ
osxfuse : MacOS向けのFuseモジュール
FUSEプロトコルの説明 : カーネルモジュール <-> Fuseライブラリ間のプロトコル
VFSの説明
fuse_lowlevel.h(libfuseのヘッダ): lowlevel関数の説明
fuse_common.h(libfuseのヘッダ)
Linuxプログラミングインターフェース(書籍) : システムコールがどう動くべきかは大体ここを見て判断する
libfuseのメーリングリストのアーカイブ : fuseの使い方についてはここが一番参考になる
gcsf : fuse-rsを使ったファイルシステムの例
実験環境
プログラムは全て次の環境で実験しています。
- Linux: 5.3.11
- ディストリビューション: Fedora 31
- Rust: 1.39.0
- fuse-rs: 0.3.1
FUSEの仕組み(概要)
FUSE本体はLinuxカーネルに付属するカーネルモジュールで、大抵のディストリビューションではデフォルトで有効になっています。
FUSEを使ったファイルシステムがマウントされたディレクトリ内に対してシステムコールが呼ばれると、以下のように情報がやりとりされます。
システムコール <-> VFS <-> FUSE <-(FUSE ABI)-> FUSEインターフェース <-(FUSE API)-> 自作のファイルシステム <-> デバイスやネットワーク上のストレージ
Wikipediaの図 を見ると分かりやすいです。
一般的なファイルシステムであればVFSの先に各ファイルシステムのカーネルモジュールがあるのですが、FUSEは受け取った情報をユーザ空間に横流ししてくれます。
FUSEインターフェース
FUSEはデバイス /dev/fuse
を持ち、ここを通じてユーザ空間とやりとりを行います。
前項の FUSE <-> FUSEインターフェース
の部分です。
規定のプロトコル(FUSE ABI)を用いて /dev/fuse
に対してデータを渡したり受け取ったりするのがFUSEインターフェースです。
有名なライブラリとして、C/C++用の libfuse があります。
このlibfuseが大変強力なので、大抵の言語でのFUSEインターフェースはlibfuseのラッパーになっています。
libfuseを使うと、 open
, read
, write
等の関数を決められた仕様通りに作成して登録するだけで、ファイルシステムとして動作するようになっています。
例えば、 read(2)
のシステムコールが呼ばれると、最終的に自作のファイルシステムの read
関数が呼ばれます。
以下の例は、自作の read
関数です。引数や戻り値の仕様はlibfuseによって決められていますが、内部をどう実装するかは自由です。
// read関数を、常にランダムな内容を返すようにした例
int my_read(const char *path, char *buf, size_t size, off_t offset, struct fuse_file_info *fi) {
ssize_t res;
res = getrandom(buf, size, 0);
return (int)res;
}
登録すべき関数は、 libfuse
のヘッダーである fuse.h
内で定義されている通常のものと、 fuse_lowlevel.h
内で定義されている低級なものがあります。
ファイルシステムを作成する場合、どちらの関数群を実装するか選択する必要があります。
fuse.h
の方はおおよそシステムコールと1:1で対応しています。 fuse_lowlevel.h
の方は FUSE ABI
と1:1になるように作られています。
fuse-rs
Rustには(ほぼ)独自のFUSEインターフェースの実装 Rust FUSE(fuse-rs) があります。ありがたいですね。
プロトコルが同じなので、インターフェースの関数(FUSE API)はlibfuseのlowlevel関数と大変似ています。そのため、何か困った時にはlibfuseの情報が流用できたりします。
現時点(2020/1) の最新版は0.3.1で、2年ぐらい更新されていませんが、次バージョン(0.4.0)が開発中です。
0.3.1と0.4.0では仕様が大きく異なるので注意してください。
また、0.3.1では対応するプロトコルのバージョンが7.8で、最新のものと比較していくつかの機能がありません。
libfuseはマルチスレッドで動作し、並列I/Oに対応していますが、fuse-rsはシングルスレッドのようです。
使用するためには、 Cargo.toml
に以下のように記述します。
[dependencies]
fuse = "0.3.1"
データの保存先
今回自分でファイルシステムを実装していく上で、HDDの代わりになるデータの保存先としてsqliteを使用します。ライブラリは rusqlite を使用します。
FUSEの実装方法について調べるのがメインなので、こちらについてはざっくりとしか説明しませんが、ご容赦ください。
実装の話が見たい方は 次章までスキップ してください。
sqliteは可変長のバイナリデータを持てるので、そこにデータを書き込みます。トランザクションがあるので、ある程度アトミックな操作ができます。DBなので、メタデータの読み書きも割と簡単にできるでしょう。
fuse-rsが扱う整数の大半は u64
ですが、sqliteはunsignedの64bit intに対応していないので、厳密にやろうとするといろいろと面倒になります。
とりあえず全部 u32
にキャストする事にしますが、気になる場合は i64
にキャストして、大小比較を行うユーザ定義関数をsqlite上に作成したり、u32
2個に分割したりしてください。
以下ではDBの構造についてざっくりと説明していきます。
データベース構造
テーブルはメタデータテーブル(MDT)とディレクトリエントリテーブル(DET)とブロックデータテーブル(BDT)の3つに分けます。
今後拡張ファイル属性が必要になってきた場合、拡張属性データテーブル(XATTRT)を追加します。
以下では各テーブルについて説明していきます。
メタデータ用テーブル(MDT)
ファイルのinode番号をキーとして検索するとメタデータが返ってくるような、メタデータ用のテーブルを作ります。
メタデータは一般的なファイルシステムのメタデータと同じような形式です。
fuse-rsが関数の引数で渡してきたり、戻り値として要求したりするメタデータ構造体は以下のように定義されています。
// fuse::FileAttr
pub struct FileAttr {
/// inode番号
pub ino: u64,
/// ファイルサイズ(バイト単位)
pub size: u64,
/// ブロックサイズ *Sparse File に対応する場合、実際に使用しているブロック数を返す
pub blocks: u64,
/// Time of last access. *read(2)実行時に更新される
pub atime: Timespec,
/// Time of last modification. *write(2)またはtruncate(2)実行時に更新される
pub mtime: Timespec,
/// Time of last change. *メタデータ変更時に更新される。 write(2)またはtruncate(2)でファイル内容が変わるときも更新される
pub ctime: Timespec,
/// Time of creation (macOS only)
pub crtime: Timespec,
/// ファイル種別 (directory, file, pipe, etc)
pub kind: FileType,
/// パーミッション
pub perm: u16,
/// ハードリンクされている数
pub nlink: u32,
/// User id
pub uid: u32,
/// Group id
pub gid: u32,
/// Rdev *デバイスファイルの場合、デバイスのメジャー番号とマイナー番号が入る
pub rdev: u32,
/// Flags (macOS only, see chflags(2)) *非表示などmac特有の属性が入ります。
pub flags: u32,
}
これに合わせて、以下のようなテーブルを作ります。
列名 | 型 | 概要 |
---|---|---|
id | integer primary | ファイルのinode番号 (pkey) |
size | int | ファイルサイズ |
atime | text | アクセス時刻 |
atime_nsec | int | アクセス時刻(小数点以下) |
mtime | text | 修正時刻 |
mtime_nsec | int | 修正時刻(小数点以下) |
ctime | text | ステータス変更時刻 |
ctime_nsec | int | ステータス変更時刻(小数点以下) |
crtime | text | 作成時刻(mac用) |
crtime_nsec | int | 作成時刻(小数点以下) |
kind | int | ファイル種別 |
mode | int | パーミッション(ファイル種別含む) |
nlink | int | ハードリンク数 |
uid | int | uid |
gid | int | gid |
rdev | int | デバイスタイプ |
flags | int | フラグ(mac用) |
idをinteger primary keyにします。これがinode番号になります。
kindはファイル種別です。
通常ファイル・キャラクターデバイス・ブロックデバイス・FIFO・ソケット・ディレクトリ・シンボリックリンク の7種類があります。
FUSEでは stat(2)
と同様に、 mode
にファイル種別のビットも含まれているので、
ビット操作する必要があります。
cのlibfuseでは libc::S_IFMT
(該当ビットのマスク) libc::S_IFREG
(通常ファイルを示すビット) 等を用いて
if((mode & S_IFMT) == S_IFREG)
のようにして判別する事ができます。
fuse-rsの場合はメタデータを返す時はenumで定義されたファイル種別を使い、ビット操作はライブラリ側で処理してくれるので、
実際のビットがどうなっているかを気にするケースはあまりありませんが、 mknod
の引数で mode
が生の値で渡ってくるので、 mknod
を実装する場合は気をつける必要があります。
ファイルデータ用テーブル(BDT)
ファイルのinode番号とファイル内のブロック番号を指定するとデータが返ってくるような、ブロックデータテーブルを作成します。
ブロックデータテーブル(BDT)のblobにデータを格納します。
BDTはファイルのinode番号, 何番目のブロックか、の列を持ちます。具体的には以下のようになります。
列名 | 型 | 概要 |
---|---|---|
file_id | int | ファイルのinode番号 (pkey)(foreign key) |
block_num | int | データのブロック番号(1始まり)(pkey) |
data | blob | データ(4kByte単位とする) |
外部キー foreign key (file_id) references metadata(id) on delete cascade
を指定する事で、ファイルのメタデータが消えたらデータも削除されるようにします。
「あるファイルのあるブロック」は一意なので、主キーとして (file_id, block_num)
を指定します。
ディレクトリ構造用テーブル(DET)
ディレクトリ構造を表現する方法は、以下の2つの候補があります。
- オブジェクトストレージのように、各ファイルがフルパスを記憶していて、文字列操作で各ディレクトリの情報を得る方法
- 一般的なファイルシステムのように、ディレクトリエントリを作る方法
今回はfuse-rsの関数とも相性のいい後者のディレクトリエントリ方式で行います。
ディレクトリのinode番号を指定すると、ディレクトリ内の全てのエントリ(ファイル名、ファイルタイプ、inode番号のセット)を返すようなテーブルを作成します。
必要なのは以下のデータです。
列名 | 型 | 概要 |
---|---|---|
parent_id | int | 親ディレクトリのinode番号 (pkey)(foreign key) |
child_id | int | 子ファイル/子ディレクトリのinode番号 (foreign key) |
file_type | int | 子のファイルタイプ |
name | text | 子のファイル/ディレクトリ名 (pkey) |
Hello
概要
第一段階として、fuse-rsに付属する、サンプルプログラムの HelloFS
と同じ機能を実装します。
HelloFS
は以下の機能があります。
- ファイルシステムはリードオンリー
- ルート直下に
hello.txt
というファイルがあり、"Hello World!\n"
という文字列が書き込まれている
fuse::Filesystem
トレイトが用意されているので、必要な関数を実装していきます。
HelloFS
の機能を実現するのに必要なのは以下の4つの関数です。
use fuse::{
Filesystem,
ReplyEntry,
ReplyAttr,
ReplyData,
ReplyDirectory,
Request
};
impl Filesystem for SqliteFs {
fn lookup(&mut self, _req: &Request, parent: u64, name: &OsStr, reply: ReplyEntry){
...
}
fn getattr(&mut self, _req: &Request, ino: u64, reply: ReplyAttr) {
...
}
fn read(&mut self, _req: &Request, ino: u64, _fh: u64, offset: i64, _size: u32, reply: ReplyData) {
...
}
fn readdir(&mut self, _req: &Request, ino: u64, _fh: u64, offset: i64, mut reply: ReplyDirectory) {
...
}
}
ファイルやディレクトリをopen/closeする関数を実装せずにread関数やreaddir関数を実装していますが、libfuse
や fuse-rs
は全ての関数にデフォルトの実装があり、今回のようにreadonlyで状態を持たないファイルシステムの場合、自分で実装しなくても動作します。
これらの関数については今後実装する必要が出てきた時に説明します。
DB関数
データベースを読み書きする関数です。
今回作成した関数は以下になります。
pub trait DbModule {
/// ファイルのメタデータを取得する。見つからない場合は0を返す
fn get_inode(&self, inode: u32) -> Result<DBFileAttr, Error>;
/// ディレクトリのinode番号を指定して、ディレクトが持つディレクトリエントリを全て取得する
fn get_dentry(&self, inode: u32) -> Result<Vec<DEntry>, Error>;
/// 親ディレクトリのinode番号と名前から、ファイルやサブディレクトリのinode番号とメタデータを得る
/// inodeが存在しない場合、inode番号が0の空のinodeを返す
fn lookup(&self, parent: u32, name: &str) -> Result<DBFileAttr, Error>;
/// inode番号とブロック数を指定して、1ブロック分のデータを読み込む
/// ブロックデータが存在しない場合は、0(NULL)で埋められたブロックを返す
fn get_data(&self, inode: u32, block: u32, length: u32) -> Result<Vec<u8>, Error>;
/// DBのブロックサイズとして使っている値を得る
fn get_db_block_size(&self) -> u32;
}
// メタデータ構造体
pub struct DBFileAttr {
pub ino: u32,
pub size: u32,
pub blocks: u32,
pub atime: SystemTime,
pub mtime: SystemTime,
pub ctime: SystemTime,
pub crtime: SystemTime,
pub kind: FileType,
pub perm: u16,
pub nlink: u32,
pub uid: u32,
pub gid: u32,
pub rdev: u32,
pub flags: u32,
}
// ディレクトリエントリ構造体
pub struct DEntry {
pub parent_ino: u32,
pub child_ino: u32,
pub filename: String,
pub file_type: FileType,
}
ルートディレクトリ
あらゆるディレクトリは .
(自分自身)と ..
(親ディレクトリ)のエントリを持ちます。
ルートの ..
は自分自身を指すようにします。
何も入っていないファイルシステムでも、初期データとしてルートディレクトリの情報が必要です。FUSEでは、ルートディレクトリのinode番号は1です。
ルートディレクトリは必ず存在する必要があります。
fuseの関数全般の話
fuseの関数
ファイルシステムなので、関数は基本的に受け身です。システムコールに応じて呼び出されます。
fuse-rsでは、 Filesystem
トレイトが定義されているので、必要な関数を適宜実装していきます。
引数
どの関数にも Request
型の引数 req
が存在します。
req.uid()
で実行プロセスのuidが、 req.gid()
でgidが、 req.pid()
でpidが取得できます。
戻り値
init
以外の各関数に戻り値は存在せず、引数の reply
を操作して、呼び出し元に値を受け渡します。
ReplyEmpty, ReplyData, ReplyAttr
のように、関数に応じて reply
の型が決まっています。
reply.ok()
reply.error(ENOSYS)
reply.attr(...)
等 reply
の型に応じたメソッドを実行します。
エラーの場合、 libc::ENOSYS
libc::ENOENT
のような定数を reply.error()
の引数に指定します。
lookup
fn lookup(&mut self, _req: &Request, parent: u64, name: &OsStr, reply: ReplyEntry);
引数の parent
で親ディレクトリのinode番号、 name
で当該ディレクトリ/ファイルの名前が与えられるので、メタデータを返します。
lookup実行時には lookup count
をファイルシステム側で用意して、増やしたりしなければなりませんが、今回はreadonlyのファイルシステムなので無視します。
lookup count
については unlink
実装時に説明します。
replyに必要なデータは以下になります。
//正常
reply.entry(&TTL, &ATTR, &GENERATION);
エラーの場合
reply.error(ENOENT);
Replyの引数
reply.entry()
の3つの引数について説明します。
TTL
time::Timespec
で期間を指定します。
TTLの間はカーネルは再度問い合わせに来ません。
今回は、以下のような ONE_SEC
という定数を作って返しています。
const ONE_SEC: Timespec = Timespec{
sec: 1,
nsec: 0
};
ATTR
対象のメタデータ。 fuse::FileAttr
を返します。
GENERATION
inodeの世代情報を u64
で返します。削除されたinodeに別のファイルを割り当てた場合、前のファイルと違うファイルである事を示すために、generationに別の値を割り当てます。
ただし、この値をチェックするのは(知られているものでは)nfsしかありません。
今回は常時 0
に設定します。
libfuseの説明 も参考にしてください。
エラー
対象のディレクトリエントリが存在しない場合、 reply.error(ENOENT)
でエラーを返します。
実装
実装は以下のようになります。
fn lookup(&mut self, _req: &Request, parent: u64, name: &OsStr, reply: ReplyEntry) {
match self.db.lookup(parent as u32, name.to_str().unwrap()) {
Ok(attr) => {
reply.entry(&Timespec{sec: 1, nsec: 0}, &attr.get_file_attr() , 0);
},
Err(_err) => reply.error(ENOENT)
};
}
getattr
fn getattr(&mut self, _req: &Request, ino: u64, reply: ReplyAttr);
引数の ino
でinode番号が指定されるので、ファイルのメタデータを返します。
メタデータの内容については lookup
で返す ATTR
と同じです。
fn getattr(&mut self, _req: &Request, ino: u64, reply: ReplyAttr) {
match self.db.get_inode(ino as u32) {
Ok(attr) => {
reply.attr(&ONE_SEC, &attr.get_file_attr());
},
Err(_err) => reply.error(ENOENT)
};
}
read
fn read(&mut self, _req: &Request, ino: u64, _fh: u64, offset: i64, size: u32, reply: ReplyData);
引数の ino
のinode番号で指定されたファイルを、 offset
で指定されたバイトから size
で指定されたバイト分読み込みます。
読み込んだデータは reply.data(&data)
を実行して返します。
EOFまたはエラーを返す場合を除いて、 read
関数は引数の size
で指定されたサイズのデータを返さないといけません。
例えば、長さ200byteのファイルに対して、4096byteの要求が来ることがあります。
この場合EOFなので、200byte返す事が許されます。また、200byte以上返しても切り捨てられます。
それ以外の場合で要求された長さのデータを用意できない場合は、エラーを返さないといけません。
libfuseやfuse-rsの説明では、「もし要求されたサイズより短いサイズのデータを返した場合、0埋めされる」と書いてありますが、
手元の環境では0埋めされず、短いサイズが read(2)
の結果として返ってきました。
カーネルのこのコミット で仕様が変わっているように見えます。
例外として、 direct_io
をマウントオプションとして指定していた場合、または direct_io
フラグを open
の戻り値として指定した場合、
カーネルは read(2)
システムコールの戻り値としてファイルシステムの戻り値を直接使うので、ファイルシステムは実際に読み込んだ長さを返します。
諸事情(ストリーミングしてる等の理由)でファイルサイズと実際のデータの長さが異なる場合に、このオプションが利用できます。
引数の fh
は open
時に戻り値としてファイルシステムが指定した値です。同じファイルに対して複数の open
が来たときに、
どの open
に対しての read
かを識別したり、ファイルオープン毎に状態を持つことができます。
今回は open
を実装していないので常に0が来ます。
fn read(&mut self, _req: &Request, ino: u64, _fh: u64, _offset: i64, _size: u32, reply: ReplyData) {
let mut data: Vec<u8> = Vec::with_capacity(_size as usize);
let block_size: u32 = self.db.get_db_block_size();
let mut size: u32 = _size;
let mut offset: u32 = _offset as u32;
// sizeが0になるまで1ブロックずつ読み込む
while size > 0 {
let mut b_data = match self.db.get_data(ino as u32, offset / block_size + 1, block_size) {
Ok(n) => n,
Err(_err) => {reply.error(ENOENT); return; }
};
// ブロックの途中から読み込む、またはブロックの途中まで読む場合の対応
let b_offset: u32 = offset % block_size;
let b_end: u32 = if (size + b_offset) / block_size >= 1 {block_size} else {size + b_offset};
if b_data.len() < b_end as usize {
b_data.resize(b_end as usize, 0);
}
data.append(&mut b_data[b_offset as usize..b_end as usize].to_vec());
offset += b_end - b_offset;
size -= b_end - b_offset;
}
reply.data(&data);
}
readdir
fn readdir(&mut self, _req: &Request, ino: u64, _fh: u64, offset: i64, mut reply: ReplyDirectory);
指定されたinodeのディレクトリのディレクトリエントリを返します。
ディレクトリ内の全てのファイルまたはディレクトリの、「名前」、「ファイル種別(ファイル/ディレクトリ/シンボリックリンク/etc.)」、「inode番号」を返します。
ls
コマンドの結果を返すイメージです。
一定サイズのバッファが渡されるので、一杯になるまでディレクトリエントリを入れて返します。
引数の fh
は opendir
でファイルシステムが渡した値です。今回は opendir
を実装していないので0です。
cでは fuse_add_direntry()
という関数を使用してバッファを埋めますが、rustでは引数で渡された reply: ReplyDirectory
を使用します。
具体的には以下のように使います。
let target_inode = 11; // inode番号
let filename = "test.txt"; // ファイル名
let fileType = FileType.RegularFile; //ファイル種別
result = reply.add(target_inode, offset, fileType, filename);
reply.add()
でバッファにデータを追加していき、最終的に reply.ok()
を実行すると、データが返せます。
バッファが一杯の時、 ReplyDirectory.add()
は true
を返します。
reply.add()
の引数の offset
はファイルシステムが任意に決めたオフセットです。
大抵はディレクトリエントリ一覧内のインデックスや次のエントリへのポインタ(cの場合)が使われます。
同じディレクトリエントリ内で offset
は一意でなければなりません。また、offsetは決まった順番を持たなければなりません。
カーネルがreaddir
の 引数として offset
に0でない値を指定してきた場合、
該当の offset
を持つディレクトリエントリの、次のディレクトリエントリを返さなければならないからです。
readdir
の引数に 0
が来た場合「最初のディレクトリエントリ」を返さないといけないので、ファイルシステムは offset
に0を入れてはならないです。
厳密に実装する場合、 opendir
時の状態を返さないといけないので、 opendir
の実装と状態の保持が必要になります。
. (カレントディレクトリ)
と .. (親ディレクトリ)
の情報は返さなくともよいですが、返さなかった場合の処理は呼び出し側のプログラムに依存します。基本的には返すようにするといいです。
fn readdir(&mut self, _req: &Request, ino: u64, _fh: u64, offset: i64, mut reply: ReplyDirectory) {
let db_entries: Vec<DEntry> = match self.db.get_dentry(ino as u32) {
Ok(n) => n,
Err(err) => {reply.error(ENOENT); debug!("{}", err); return;}
};
for (i, entry) in db_entries.into_iter().enumerate().skip(offset as usize) {
let full = reply.add(entry.child_ino as u64, (i + 1) as i64, entry.file_type, &entry.filename);
if full {
break;
}
}
reply.ok();
}
マウント
main関数で fuse::mount()
を実行すると、マウントできます。
fuse-rsは env_logger に対応しているので、最初に有効にしておきます。
RUST_LOG=debug [コマンド]
のように、環境変数でレベルを設定できます。
DEBUG
レベルにすると各関数の呼び出しを記録してくれます。
引数は雑に取得していますが、マウントオプションの処理などがあるので、後々 clap などを使って解析することにします。
fn main() {
// ログを有効にする
env_logger::init();
// 引数からマウントポイントを取得
let mountpoint = env::args_os().nth(1).unwrap();
// 引数からDBファイルのパスを取得
let db_path = env::args_os().nth(2).unwrap();
// マウントオプションの設定
let options = ["-o", "ro", "-o", "fsname=sqlitefs"]
.iter()
.map(|o| o.as_ref())
.collect::<Vec<&OsStr>>();
// ファイルシステムの初期化
let fs: SqliteFs = match SqliteFs::new(db_path.to_str().unwrap()) {
Ok(n) => n,
Err(err) => {println!("{:?}", err); return;}
};
// マウント
fuse::mount(fs, &mountpoint, &options).unwrap();
}
初期データ登録
自動でテーブルを作成する機能をまだ実装していません。初期化用の init.sql と、 hello.txt追加用の hello.sql を作成して、実行してデータベースを作成します。
init.sql
ではテーブルの作成とルートディレクトリのメタデータ、ディレクトリエントリの登録、
hello.sql
では hello.txt
のメタデータ作成、ルートディレクトリへの追加、データの追加を行っています。
$ sqlite3 ~/filesystem.sqlite < init.sql
$ sqlite3 ~/filesystem.sqlite < hello.sql
ビルド及び実行
[プログラム名] [マウント先] [データベースファイル名]
で実行できます。
# バックグラウンドで実行
$ ./sqlite-fs ~/mount ~/filesystem.sqlite &
# lsしてhello.txtがあるのを確認
$ ls ~/mount
hello.txt
# hello.txtの内容が読み込めることを確認
$ cat ~/mount/hello.txt
Hello World!
また、 $ RUST_LOG=debug cargo run ~/mount
でビルドと実行( ~/mount
にマウントして、デバッグログを出力)ができます。
試しに cat ~/mount/hello.txt
を実行すると、以下のようなログが出力されます。 env_logger
のおかげで各関数に対する呼び出しが記録されています。
[2019-10-25T10:43:27Z DEBUG fuse::request] INIT(2) kernel: ABI 7.31, flags 0x3fffffb, max readahead 131072
[2019-10-25T10:43:27Z DEBUG fuse::request] INIT(2) response: ABI 7.8, flags 0x1, max readahead 131072, max write 16777216
[2019-10-25T10:43:42Z DEBUG fuse::request] LOOKUP(4) parent 0x0000000000000001, name "hello.txt"
[2019-10-25T10:43:42Z DEBUG fuse::request] OPEN(6) ino 0x0000000000000002, flags 0x8000
[2019-10-25T10:43:42Z DEBUG fuse::request] READ(8) ino 0x0000000000000002, fh 0, offset 0, size 4096
[2019-10-25T10:43:42Z DEBUG fuse::request] FLUSH(10) ino 0x0000000000000002, fh 0, lock owner 12734418937618606797
[2019-10-25T10:43:42Z DEBUG fuse::request] RELEASE(12) ino 0x0000000000000002, fh 0, flags 0x8000, release flags 0x0, lock owner 0
lookup -> open -> read -> close の順で関数が呼び出されている事が分かります。
close
に対応する関数である flush
と release
は実装していませんが、動作しています。
ファイルシステムのアンマウント
ファイルシステムは fusermount -u [マウント先]
でアンマウントできます。アンマウントするとプログラムは終了します。
Ctrl + c
等でプログラムを終了した場合でもマウントしたままになっているので、かならず fusermount
を実行してください。
まとめ
4つの関数を実装するだけで、Readonlyのファイルシステムが作成できました。
次回はファイルにデータの書き込みができるようにします。
ここまでのコードは github に置いてあります。