ファイルの作成と削除
RustでFUSEを利用したファイルシステムを作成する記事の3回目です。
最新のソースコードはこちらにあります。
記事一覧
Rustで学ぶFUSE (1) リードオンリーなファイルシステムの実装
Rustで学ぶFUSE (2) ファイルの書き込み
Rustで学ぶFUSE (3) ファイル作成と削除
Rustで学ぶFUSE (4) ディレクトリ作成と削除
Rustで学ぶFUSE (5) 名前の変更と移動
概要
ファイルの作成と削除が行えるようにします。
必要なのは以下の関数です。
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のファイルを指しています。
ここにシンボリックリンクとハードリンクを追加したものが以下の図になります。
シンボリックリンク( ~/text/symlink.txt
)はあくまで特定のファイルパスへのリンクで、リンク先のファイルとは主、従の関係になります。元のファイルが移動したり削除されれば、データにアクセスする事ができなくなります。
一方ハードリンク( ~/text/hardlink.txt
)はinodeに対するリンクで、元のファイルとは対等な存在です。 ~/text/memo.txt
が移動しようが削除されようが、データにアクセスする事ができます。
よって、ファイル削除時には、ファイルシステムは実際のデータを消す前に、他のファイルからハードリンクされていないかチェックする必要があります。
データがいくつのファイルからハードリンクされているかを本書では 参照カウント
と呼びます。(一般的には「ハードリンク数」という表記が多いようです。)
この 参照カウント
は、 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
を追加します。
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関数を追加しました。
/// ファイル/ディレクトリの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を足すようにします。
コードは以下のようになります。
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
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は呼び出してくれません。
実装したコードは以下のようになります。
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
fn unlink(
&mut self,
_req: &Request<'_>,
parent: u64,
name: &OsStr,
reply: ReplyEmpty
);
親ディレクトリのinode番号が引数の parent
, 削除対象のファイル/ディレクトリの名前が name
で指定されるので、ファイルまたはディレクトリを削除します。
削除対象はディレクトリエントリと、該当のinodeのメタデータです。
ただし、inodeは複数のディレクトリエントリから参照されている可能性があるので、参照カウント(メタデータの nlink
の項目) を1減らし、0になった場合に削除します。
また、 lookup count をチェックし、0になっていない場合は即座に削除を行いません。
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
fn forget(&mut self, _req: &Request<'_>, _ino: u64, _nlookup: u64);
lookup countを減らします。
引数の ino
で対象のinode番号、 nlookup
で減らす数が指定されます。
lookup count
が原因でinodeの削除が遅延されている場合、この関数が実行されてlookup countが0になったタイミングで削除します。
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
fn destroy(&mut self, _req: &Request<'_>);
ファイルシステムの終了時に呼ばれる関数です。
ファイルシステムのアンマウント時には、全ての lookup count が0になる事が期待されます。
一方、 forget
が呼ばれる事は保証されていないので、ファイルシステムが自分でチェックする必要があります。
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
fn init(&mut self, _req: &Request<'_>) -> Result<(), c_int>;
ファイルシステムのマウント時に最初に呼ばれる関数です。
何らかの事情で destroy
が呼ばれずにファイルシステムが突然終了した場合、参照カウントが0のままのinodeが残り続ける事になるので、チェックして削除します。
fn init(&mut self, _req: &Request<'_>) -> Result<(), c_int> {
match self.db.delete_all_noref_inode() {
Ok(n) => n,
Err(err) => debug!("{}", err)
};
Ok(())
}
実行結果
ファイル作成
- コマンド
# ファイル作成
$ 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 に置いてあります。
次回は、ディレクトリの作成/削除ができるようにします。