Rustで学ぶFUSE (3) ファイル作成と削除

# ファイルの作成と削除
RustでFUSEを利用したファイルシステムを作成する記事の3回目です。
最新のソースコードは[こちら](https://github.com/narumatt/sqlitefs)にあります。

## 記事一覧
[Rustで学ぶFUSE (1) リードオンリーなファイルシステムの実装](https://qiita.com/narumatt/items/2dfc3aedf3aafd459e81)
[Rustで学ぶFUSE (2) ファイルの書き込み](https://qiita.com/narumatt/items/5b15dbabfeef32bfaf5c)
**Rustで学ぶFUSE (3) ファイル作成と削除**
[Rustで学ぶFUSE (4) ディレクトリ作成と削除](https://qiita.com/narumatt/items/bec85dd5218a85a089b8)
[Rustで学ぶFUSE (5) 名前の変更と移動](https://qiita.com/narumatt/items/293a1c1f30b1aa9a8270)

## 概要
ファイルの作成と削除が行えるようにします。

必要なのは以下の関数です。

```rust
fn init(&mut self, _req: &Request<'_>) -> Result<(), c_int> {
    ...
}
fn destroy(&mut self, _req: &Request<'_>) {
    ...
}
fn lookup(&mut self, _req: &Request, parent: u64, name: &OsStr, reply: ReplyEntry){
    ...(既存のコードに追加)
}
fn forget(&mut self, _req: &Request<'_>, _ino: u64, _nlookup: u64) {
    ...
}
fn unlink(&mut self, _req: &Request<'_>, _parent: u64, _name: &OsStr, reply: ReplyEmpty) {
    ...
}
fn create(&mut self, _req: &Request<'_>, _parent: u64, _name: &OsStr, _mode: u32, _flags: u32, reply: ReplyCreate) {
    ...
}
```

ファイルを作成する関数は `create` 、削除する関数は `unlink` なので本来必要な関数は2つですが、 `lookup count` の都合で追加でいろいろ実装する必要があります。
以下ではファイル削除の仕組みと `lookup count` について説明していきます。

今回も実装する関数と同名のシステムコールの区別をつけるために、 システムコールは `write(2)` のように、末尾に`(2)`を付けた表記をします。

## ハードリンクとファイル削除
Linuxのファイルシステムはディレクトリエントリとinodeに分かれています。
例えば、以下の図では `~/text/memo.txt` というディレクトリエントリが、330番のinodeのファイルを指しています。

![Untitled Diagram.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/360504/345c015e-a17a-0339-9a30-b55d4ec06dae.png)

ここにシンボリックリンクとハードリンクを追加したものが以下の図になります。
シンボリックリンク( `~/text/symlink.txt` )はあくまで特定のファイルパスへのリンクで、リンク先のファイルとは主、従の関係になります。元のファイルが移動したり削除されれば、データにアクセスする事ができなくなります。
一方ハードリンク( `~/text/hardlink.txt` )はinodeに対するリンクで、元のファイルとは対等な存在です。 `~/text/memo.txt` が移動しようが削除されようが、データにアクセスする事ができます。

![hardlink.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/360504/845fc346-f593-a3eb-f9fc-51ac76292bf6.png)

よって、ファイル削除時には、ファイルシステムは実際のデータを消す前に、他のファイルからハードリンクされていないかチェックする必要があります。
データがいくつのファイルからハードリンクされているかを本書では `参照カウント` と呼びます。(一般的には「ハードリンク数」という表記が多いようです。)

この `参照カウント` は、 `ls -l` コマンドでも確認する事ができます。パーミッションの右の数字です。通常ファイルは作成時点では1で、意図してハードリンクしないと1のままです。ディレクトリの場合、 `.` や `..` でサブディレクトリから参照されているので、数が増えます。

```
-rw-r--r--  1 matt  staff  3389 12 16  2018 memo.txt
```

## lookup count
ここまで参照カウントについて説明してきましたが、ファイル削除に絡む関数を実装する場合、更に `lookup count` に注意する必要があります。
大まかに説明すると、lookup countは **いくつのプロセスがファイルを開いているか(または開く予定か)**を示します。


`lib.rs`(fuse-rsの説明) や `fuse_lowlevel.h`(libfuseの説明) によると、
「**lookup count が0でない内は、`unlink`, `rmdir`, `rename(上書きされる場合)` などの関数で参照カウントが0になってもinodeを削除しないでね**」という事になっています。
lookup countは最初は0で、関数の戻り値に `ReplyEntry` と `ReplyCreate` がある全ての関数が呼ばれるたびに、1ずつ増やしていきます。
具体的には、 `lookup`, `mknod`, `mkdir`, `symlink`, `link`, `create` が実行されると1増えます。

`forget` はlookup countを減らすために呼ばれる関数です。 `forget` が実行されてlookup countが0になるまでは、ファイルシステムは削除を遅らせる必要があります。
例えば、カーネルはまだファイルをOpenしているプロセスがあると、 `forget` の実行をファイルが閉じられるまで遅延させます。
これにより、「別の誰かがファイルを削除して `ls` 等でファイルを見つけられなくなるが、削除前からファイルを開いていた場合は読み込み続ける事ができる」という機能が実現できます。
ちなみに、 `forget` でlookup countがマイナスになる場合はどこかに増やし忘れがある可能性が高いので、先述した増やすべき6つの関数をチェックしてください。

lookup countを実装するために、ファイルシステムの構造体に次の変数 `lookup_count` を追加します。

```rust
pub struct SqliteFs{
    /// DBとやり取りする
    db: Sqlite,
    /// lookup countを保持する。 key: inode番号, value: lookup count
    lookup_count: Mutex<HashMap<u32, u32>>
}
```

今回はkeyがinode番号、valueがlookup count、であるHashMapを作成します。

## 追加したDB関数
今回は以下のようなDB関数を追加しました。

```rust
/// ファイル/ディレクトリのinodeを追加し、新しく割り振ったinode番号を返す。 引数attrのinoは無視される。
fn add_inode(&mut self, parent: u32, name: &str, attr: &DBFileAttr) -> Result<u32, Error>;
/// inodeをチェックし、参照カウントが0なら削除する
fn delete_inode_if_noref(&mut self, inode: u32) -> Result<(), Error>;
/// 親ディレクトリのinode番号、ファイル/ディレクトリ名で指定されたディレクトリエントリを削除し、
/// 該当のinodeの参照カウントを1減らす
/// 削除したファイル/ディレクトリのinode番号を返す
fn delete_dentry(&mut self, parent: u32, name: &str) -> Result<u32, Error>;
/// 参照カウントが0である全てのinodeを削除する
fn delete_all_noref_inode(&mut self) -> Result<(), Error>;
```

## lookup
第一回で作成した、ファイルのメタデータを取得する関数 `lookup` 更新します。  
関数が実行されるたびに、lookupで情報を持ってくる対象のファイル/ディレクトリのlookup countに1を足すようにします。

コードは以下のようになります。

```rust
fn lookup(&mut self, _req: &Request, parent: u64, name: &OsStr, reply: ReplyEntry) {
    // 既存のコード
    let parent = parent as u32;
    let child = match self.db.lookup(parent, name.to_str().unwrap()) {
        Ok(n) => {
            if n.ino == 0 {
                reply.error(ENOENT);
                return;
            }
            reply.entry(&ONE_SEC, &n.get_file_attr() , 0);
            n.ino
        },
        Err(err) => {reply.error(ENOENT); debug!("{}", err); return;}
    };

    // ここから追加内容
    // lookup countに1を足す。HashMapにkeyが無い場合は追加する
    let mut lc_list = self.lookup_count.lock().unwrap();
    let lc = lc_list.entry(child).or_insert(0);
    *lc += 1;
}
```

## create

```rust
fn create(
    &mut self,
    _req: &Request<'_>,
    parent: u64,
    name: &OsStr,
    mode: u32,
    flags: u32,
    reply: ReplyCreate
);
```

引数の `parent` のinode番号で指定されたディレクトリ内の、 `name` で指定されたファイル名を持つファイルを作成します。

`creat(2)` または `O_CREAT` を指定した `open(2)` 実行時に呼ばれます。

指定されたファイルが存在しない場合、引数の `mode` で指定されたパーミッションでファイルを作成し、ファイルを開きます。
ファイルのオーナーに設定するユーザ、グループは、引数の `req` から `req.uid()` `req.gid()` で取得できます。
ただし、マウントオプションで `–o grpid` または `–o bsdgroups` が指定されている場合や、親ディレクトリにsgidが設定されている場合(親ディレクトリのパーミッションと `libc::S_ISGID` でANDを取って判定)は、親ディレクトリと同じグループを設定しないといけません。

ファイルが既に存在する場合、openと同じ動作を行います。

ファイルを作成する以外は `open` と同じ動作のため、open時のフラグが `flags` で渡されます。

`creat(2)` や `open(2)` ではフラグに `O_EXCL` が指定されている場合、指定した名前のファイルが既に存在するとエラーにしなければなりませんが、この処理はカーネルがやってくれているようです。

`create` が実装されていない場合、カーネルは `mknod` と `open` を実行します。
`create` が実装されている場合、libfuseは通常ファイルの `mknod` が実行されると、代わりに `create` を呼び出しますが、fuse-rsは呼び出してくれません。

実装したコードは以下のようになります。

```rust
fn create(
    &mut self,
    req: &Request<'_>,
    parent: u64,
    name: &OsStr,
    mode: u32,
    _flags: u32,
    reply: ReplyCreate
) {
    let ino;
    let parent = parent as u32;
    let name = name.to_str().unwrap();
    // ファイルが既にあるかチェックする
    let mut attr = match self.db.lookup(parent, name) {
        Ok(n) => n,
        Err(err) => {reply.error(ENOENT); debug!("{}", err); return;}
    };
    if attr.ino == 0 {
        // ファイル作成
        let now = SystemTime::now();
        attr = DBFileAttr {
            ino: 0, // 無視されるので0にする
            size: 0,
            blocks: 0,
            atime: now,
            mtime: now,
            ctime: now,
            crtime: now,
            kind: FileType::RegularFile,
            perm: mode as u16,
            nlink: 0,
            uid: req.uid(),
            gid: req.gid(),
            rdev: 0,
            flags: 0
        };
        ino = match self.db.add_inode(parent, name, &attr) {
            Ok(n) => n,
            Err(err) => {
                reply.error(ENOENT);
                debug!("{}", err);
                return;
            }
        };
        attr.ino = ino;
    } else {
        ino = attr.ino;
    }
    // createもlookup countを+1する
    let mut lc_list = self.lookup_count.lock().unwrap();
    let lc = lc_list.entry(ino).or_insert(0);
    *lc += 1;
    reply.created(&ONE_SEC, &attr.get_file_attr(), 0, 0, 0);
}
```

## unlink

```rust
fn unlink(
    &mut self,
    _req: &Request<'_>,
    parent: u64,
    name: &OsStr,
    reply: ReplyEmpty
);
```

親ディレクトリのinode番号が引数の `parent`, 削除対象のファイル/ディレクトリの名前が `name` で指定されるので、ファイルまたはディレクトリを削除します。

削除対象はディレクトリエントリと、該当のinodeのメタデータです。
ただし、inodeは複数のディレクトリエントリから参照されている可能性があるので、参照カウント(メタデータの `nlink` の項目) を1減らし、0になった場合に削除します。
また、 lookup count をチェックし、0になっていない場合は即座に削除を行いません。

```rust
fn unlink(&mut self, _req: &Request<'_>, parent: u64, name: &OsStr, reply: ReplyEmpty) {
    // ディレクトリエントリを削除しつつ、対象のinode番号を得る
    let ino = match self.db.delete_dentry(parent as u32, name.to_str().unwrap()) {
        Ok(n) => n,
        Err(err) => {reply.error(ENOENT); debug!("{}", err); return;}
    };
    // lookup countのチェック
    let lc_list = self.lookup_count.lock().unwrap();
    if !lc_list.contains_key(&ino) {
        // 参照カウントが0の場合削除する。そうでない場合、unlink 内では削除しない
        match self.db.delete_inode_if_noref(ino) {
            Ok(n) => n,
            Err(err) => {reply.error(ENOENT); debug!("{}", err); return;}
        };
    }
    reply.ok();
}
```

## forget

```rust
fn forget(&mut self, _req: &Request<'_>, _ino: u64, _nlookup: u64);
```

lookup countを減らします。

引数の `ino` で対象のinode番号、 `nlookup` で減らす数が指定されます。
`lookup count` が原因でinodeの削除が遅延されている場合、この関数が実行されてlookup countが0になったタイミングで削除します。

```rust
fn forget(&mut self, _req: &Request<'_>, ino: u64, nlookup: u64) {
    let ino = ino as u32;
    // lookup countのチェック
    let mut lc_list = self.lookup_count.lock().unwrap();
    let lc = lc_list.entry(ino).or_insert(0);
    *lc -= nlookup as u32;
    if *lc == 0 {
        // 0になった場合、lookup countの一覧から削除する
        lc_list.remove(&ino);
        // 参照カウントが0でinodeの削除が遅延されていた場合、改めて削除する
        match self.db.delete_inode_if_noref(ino) {
            Ok(n) => n,
            Err(err) => debug!("{}", err)
        }
    }
}
```

## destroy

```rust
fn destroy(&mut self, _req: &Request<'_>);
```

ファイルシステムの終了時に呼ばれる関数です。

ファイルシステムのアンマウント時には、全ての lookup count が0になる事が期待されます。
一方、 `forget` が呼ばれる事は保証されていないので、ファイルシステムが自分でチェックする必要があります。

```rust
fn destroy(&mut self, _req: &Request<'_>) {
    let lc_list = self.lookup_count.lock().unwrap();
    // lookup countが残っている全てのinodeをチェック
    for key in lc_list.keys() {
        // 参照カウントが0でinodeの削除が遅延されていた場合、改めて削除する
        match self.db.delete_inode_if_noref(*key) {
            Ok(n) => n,
            Err(err) => debug!("{}", err)
        }
    }
}
```

## init

```rust
fn init(&mut self, _req: &Request<'_>) -> Result<(), c_int>;
```

ファイルシステムのマウント時に最初に呼ばれる関数です。

何らかの事情で `destroy` が呼ばれずにファイルシステムが突然終了した場合、参照カウントが0のままのinodeが残り続ける事になるので、チェックして削除します。

```rust
fn init(&mut self, _req: &Request<'_>) -> Result<(), c_int> {
    match self.db.delete_all_noref_inode() {
        Ok(n) => n,
        Err(err) => debug!("{}", err)
    };
    Ok(())
}
```

## 実行結果
### ファイル作成

- コマンド

```bash
# ファイル作成
$ touch ~/mount/touch.txt
# ファイル作成 + 書き込み
$ echo "created" > ~/mount/test.txt
# ファイル作成の確認
$ ls ~/mount
hello.txt  test.txt  touch.txt
# 書き込み内容の確認
$ cat ~/mount/test.txt
created
```

- FUSEログ

```
[2019-10-30T11:21:59Z DEBUG fuse::request] INIT(2)   kernel: ABI 7.31, flags 0x3fffffb, max readahead 131072
[2019-10-30T11:21:59Z DEBUG fuse::request] INIT(2) response: ABI 7.8, flags 0x1, max readahead 131072, max write 16777216
// touch ~/mount/touch.txt によるファイル作成のログ
[2019-10-30T11:22:14Z DEBUG fuse::request] LOOKUP(4) parent 0x0000000000000001, name "touch.txt"
[2019-10-30T11:22:14Z DEBUG fuse::request] CREATE(6) parent 0x0000000000000001, name "touch.txt", mode 0o100664, flags 0x8841
[2019-10-30T11:22:14Z DEBUG fuse::request] FLUSH(8) ino 0x0000000000000003, fh 0, lock owner 16194556409419452441
[2019-10-30T11:22:14Z DEBUG fuse::request] SETATTR(10) ino 0x0000000000000003, valid 0x1b0
[2019-10-30T11:22:14Z DEBUG fuse::request] RELEASE(12) ino 0x0000000000000003, fh 0, flags 0x8801, release flags 0x0, lock owner 0
// echo "created" > ~/mount/test.txt による書き込みログ
[2019-10-30T11:22:28Z DEBUG fuse::request] LOOKUP(14) parent 0x0000000000000001, name "test.txt"
[2019-10-30T11:22:28Z DEBUG fuse::request] CREATE(16) parent 0x0000000000000001, name "test.txt", mode 0o100664, flags 0x8241
[2019-10-30T11:22:29Z DEBUG fuse::request] GETXATTR(18) ino 0x0000000000000004, name "security.capability", size 0
[2019-10-30T11:22:29Z DEBUG fuse::request] WRITE(20) ino 0x0000000000000004, fh 0, offset 0, size 8, flags 0x0
[2019-10-30T11:22:29Z DEBUG fuse::request] RELEASE(22) ino 0x0000000000000004, fh 0, flags 0x8001, release flags 0x0, lock owner 0
```

### ファイル削除

- コマンド

```
$ rm ~/mount/test.txt
$ ls ~/mount/test.txt
ls: cannot access '/home/jiro/mount/test.txt': No such file or directory
```

- FUSEログ

```
[2019-10-30T05:32:26Z DEBUG fuse::request] LOOKUP(48) parent 0x0000000000000001, name "test.txt"
[2019-10-30T05:32:26Z DEBUG fuse::request] ACCESS(50) ino 0x0000000000000004, mask 0o002
[2019-10-30T05:32:26Z DEBUG fuse::request] UNLINK(52) parent 0x0000000000000001, name "test.txt"
[2019-10-30T05:32:26Z DEBUG fuse::request] FORGET(54) ino 0x0000000000000004, nlookup 4
```

## まとめ
ファイルの作成、削除が問題なくできるようになりました。
「削除した時点でファイルのデータなんて消してしまえ！」というならもう少しシンプルになりますが、ある程度ファイルシステムを使う側から期待されている動作なので、きちんと実装しました。

ここまでのコードは[github](https://github.com/narumatt/sqlitefs/tree/create-delete-file) に置いてあります。
次回は、ディレクトリの作成/削除ができるようにします。

**次の記事**
[Rustで学ぶFUSE (4) ディレクトリ作成と削除](https://qiita.com/narumatt/items/bec85dd5218a85a089b8)