Rust FUSEいろいろ

# はじめに

[Rust-FUSEで遊んでみる (ver.2019)](https://qiita.com/hakua-doublemoon/items/94df892e54f08bb1b232) の続きです。
2019/10/23現在、ファイルを追加(mknod)して書き込む(write)ところまではできたので、ここまでの作業を振り返ってみます。特に趣旨はなく、ファイルシステムとかRustってこんなもんなんだなという話です。知見などは期待しないでください。
ソースコードは下記です。READMEはfork元のままなのであれです。

https://github.com/hakua-doublemoon/rust-fuse

# やったこといろいろ

## デフォルトのRust-FUSE::hello_fsでできること

ファイルシステムを展開したフォルダにhello.txtを自動で作成して中身が読めるようになります。
読み書きはできません。

## mknod（ファイルの追加）をしてみる
ここで主にやったのは下記です:

0. ファイルの情報（inode）を増やせるようにする
0. フォルダに書き込みをできるようにする
0. mknodのシステムコールの処理を実装する
0. lookupのシステムコールをちゃんと処理するようにする

1番は簡単なので2番から話します。

### フォルダに書き込みをできるようにする

```diff:hello_fs/src/main.rs
-   let options = ["-o", "ro", "-o", "fsname=hello"]
+   let options = ["-o", "fsname=hello"]
```

ファイルシステム作成時にオプションを渡してるのでそれを修正すればOKです。
これをやらないといくらmknodなどの実装をしてもアプリケーションがディレクトリに変更を加えようとしても門前払いされます。

### mknodの実装

まずsrc/lib.rsからmknodにエラー応答してるやつを引っ張ってきます。

```rust:src/lib.rs>>>hello_fs/src/main.rs
    fn mknod(&mut self, _req: &Request<'_>, _parent: u64, _name: &OsStr, _mode: u32, _rdev: u32, reply: ReplyEntry) {
        reply.error(ENOSYS);
    }
```

エラーではなくまともな応答を返すためには`reply: ReplyEntry`の使い方を考えます。これはほかのシステムコールの実装でも同じです。ここでは`ReplyEntry`がlookupでも使用されているのでとりあえずそれと同じにすればOKでした。

```rust:hello_fs/src/main.rs
            reply.entry(&TTL, &(self.file_inode[node_idx]), 0);
```

### lookupの改造

当初のlookupはディレクトリとファイルに1つずつの固定の応答しかなかったので改造します。
lookupはfile名を渡してくるので、file名とinode番号を紐づけるテーブルが必要になります。ここで`BlockInfo`というものを作成しました。適当に図示すると下のようになります。

![fs_usecase.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227756/62240af9-b363-a8dd-9c1e-8c50e0fd779d.png)

実際のファイルシステムではブロック（データ）そのものにファイル名が書いてあったりするようですが、現段階では、ブロック本体とブロックの情報的なものは分離してあります。

## writeしてみる

writeに関しては下記のように実装を進めました。

0. `BlockInfo`に`Block`を割り当てる
1. Writeの情報を受け取ってBlockに書く
2. ReadでBlockの中身を返す

### Blockを実装する

特に術も知らないのでポインターを使います。
メモリの確保は`std::alloc`を使います。はじめはboxなどを検討しましたが扱いが高度だったのでやめました。
定義はこんな感じ:

```rust:hello_fs/src/hello_blocks.rs
pub struct BlockBox {
    pub data: *mut u8,
    pub layout: Layout,
}
```

使い終わったときにリークされると困るのでdropを実装しておきます。

```rust:hello_fs/src/hello_blocks.rs
impl Drop for BlockBox {
    fn drop(&mut self)
    {
        unsafe {
            dealloc(self.data, self.layout);
        }
    }
}
```

### Writeを実装する

mknodと同様に、まず`src/lib.rs`からwriteの元ネタを引っ張ってきます。
書き込むデータは下記のようにスライスで渡されます。またオフセット（シーク位置）も考慮する必要があります。`_fh`はファイルハンドラのようですが差し当たっては無視してても支障ないです。

```rust:hello_fs/src/main.rs
    fn write(&mut self, _req: &Request<'_>, ino: u64, _fh: u64, offset: i64, data: &[u8],
                       _flags: u32, reply: ReplyWrite)
```

offsetがあればポインターをずらし、コピーでデータを書き込みます。

```rust:hello_fs/src/hello_blocks.rs
            if  offset > 0  {
                dst_ptr = dst_ptr.offset(offset as isize) as *mut u8;
                cur_pos = offset as usize;
            }
            std::ptr::copy_nonoverlapping(src_ptr, dst_ptr, src_len);
```

書いたブロックにあまり（余白）があれば0で埋めておきます。

```rust:hello_fs/src/hello_blocks.rs
                std::ptr::write_bytes(dst_ptr, 0, remain_bytes);
```

逆にブロックのサイズが不足していればreallocします。reallocしたとき、データはコピーしてもらえるので心配いらないです。

```rust:hello_fs/src/hello_blocks.rs
                let lay = Layout::from_size_align(new_size, BLOCK_UNIT).ok().unwrap();
                bx.data = realloc(dst_ptr, lay, new_size);
                bx.layout = lay;
```

### 書いたデータをReadで返す

これは特に難しいことないですね。スライスで返します。

```rust:hello_fs/src/hello_blocks.rs
        let p = self.data;
        let mut l = offset + (size as i64);
        let slice = unsafe { std::slice::from_raw_parts(p, l as usize) };
        &slice[(offset as usize)..]
```

### ほかに実装が必要なもの
writeはwriteだけの実装で終わらないです。

* setattr
    * 実際にファイル情報を変更する必要はなく、変更前のファイル情報を返すだけでだいたいOKです。<br>ただし<font color="Red">ファイルサイズは今書かれているデータの最後尾のバイトを渡す必要があります。</font>そうしないと追記みたいなwriteアクセスで指定されるoffsetがおかしくなります。

* ioctl
    * 後述しますがアプリケーションによって必要です

他にreleaseとかflushとかgetxattrとかもコールされますが、これらは実装されてなくても適当に動きます。

# トラブル集
## mknodでいい加減な応答を返すとアプリがおかしくなる
mknodで応答するinodeの情報は適当でも動くことは動きました。しかしちゃんとしたものを渡さないとアプリケーションが誤動作します。
具体的に起こったことは: 作ったファイルシステムをrubyでテストしていたのですが、rubyのプロセスの中でinodeの情報をキャッシュするっぽく、ダミーのinodeを応答するとそのダミーに対して以降のアクセスを行い始めたということでした。bashからechoとcatをするときはinodeのキャッシュみたいのがなく、lookupをいちいちするのでそういう動作が無いようでした。

## rubyでopenするとIoCtlが飛んでくる
bashでcatしてもIoCtlはないのですが、rubyではなぜかやってきます。
IoCtlはRust FUSEのデフォルトでは解放されていないので、featureを指定したうえでsrc/request.rsにIoCtlを実装してやる必要があります。（もう少し深くから必要かも？）

```rust:src/request.rs
            #[cfg(feature = "abi-7-11")]
            ll::Operation::IoCtl { arg: _, data: _ } => {
                println!("[I] -- IoCtl --");
                self.reply::<ReplyEmpty>().error(ENOSYS);
            }
```

……エラー応答させてますね。応答さえできればいいんだろうか……？
ファイルシステム本体に実装する必要はないです。
featureの指定をするのでビルドは下記のようなコマンドで行います。

```bash
> cargo build --features abi-7-11,fuse-abi/abi-7-11
```

ちなみにそもそもどうやってioctlがコールされているかを捕まえたかという話もありますが……printlnでトレースしたわけですね。

# その他
## workspaceの利用
当初は`Cargo.toml`に下記を書いてhello_fsをビルドしていましたが、ワーニングが出てました。

```toml
[[bin]]
name = "hello_fs"
path = "examples/hello.rs"
```

なので「[Cargoのワークスペース](https://doc.rust-jp.rs/book/second-edition/ch14-03-cargo-workspaces.html)」を少し読んで、rust-fuseプロジェクトの中にhello_fsというサブディレクトリを作り、`examples/hello.rs`を`hello_fs/src/main.rs`として移動させ、Cargo.tomlにも下記のようにworkspaceを定義しました。

```toml
[workspace]
members = [
    ".",
    "fuse-abi",
    "fuse-sys",
    "hello_fs",
]
```

workspaceはsrc/main.rsが依存していれば勝手にビルドされるようですが、hello_fsは依存されていないので下記のように`-p`でビルドを指定してやります。

```bash
> cargo build --features abi-7-11,fuse-abi/abi-7-11 -p hello_fs
```

# まとめ
一通り書いてみると、下記のような知見が得られた気がしました。

* FUSEに関して:
    * mknodやwriteなどの実装を追加する方法
    * アプリケーションによってioctlなど想定しないシステムコールがやってくる
* Rustに関して:
    * メモリのヒープとdropトレイトの実装
    * ヒープしたメモリのポインターの扱い
    * ビルド時のfeatureの指定
    * workspaceの利用

そもそもRustのコーディングにおいては文字列を渡すのですら苦労しました。しかしそれも乗り越え……やはりこういったまとまったコーディングがプログラミング言語全体も含むスキルの向上に役立つなあと感じてます。