LoginSignup
31
19

More than 3 years have passed since last update.

Rustで学ぶFUSE (1) リードオンリーなファイルシステムの実装

Last updated at Posted at 2020-01-28

はじめに

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をマウントできるソフトウェアといった「読み書きできる何かをファイルシステムとしてマウント出来るようにするソフトウェア」があれこれと存在します。
上記のようなソフトウェアの代表例である sshfss3fs は使った事のある人もいるのではないでしょうか。

元々は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つの候補があります。

  1. オブジェクトストレージのように、各ファイルがフルパスを記憶していて、文字列操作で各ディレクトリの情報を得る方法
  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 は以下の機能があります。

  1. ファイルシステムはリードオンリー
  2. ルート直下に 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関数を実装していますが、libfusefuse-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) システムコールの戻り値としてファイルシステムの戻り値を直接使うので、ファイルシステムは実際に読み込んだ長さを返します。
諸事情(ストリーミングしてる等の理由)でファイルサイズと実際のデータの長さが異なる場合に、このオプションが利用できます。

引数の fhopen 時に戻り値としてファイルシステムが指定した値です。同じファイルに対して複数の 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 コマンドの結果を返すイメージです。
一定サイズのバッファが渡されるので、一杯になるまでディレクトリエントリを入れて返します。

引数の fhopendir でファイルシステムが渡した値です。今回は 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 に対応する関数である flushrelease は実装していませんが、動作しています。

ファイルシステムのアンマウント

ファイルシステムは fusermount -u [マウント先] でアンマウントできます。アンマウントするとプログラムは終了します。
Ctrl + c 等でプログラムを終了した場合でもマウントしたままになっているので、かならず fusermount を実行してください。

まとめ

4つの関数を実装するだけで、Readonlyのファイルシステムが作成できました。
次回はファイルにデータの書き込みができるようにします。

ここまでのコードは github に置いてあります。

次の記事
Rustで学ぶFUSE (2) ファイルの書き込み

31
19
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
31
19