はじめに
Rust-FUSEで遊んでみる (ver.2019) の続きです。
2019/10/23現在、ファイルを追加(mknod)して書き込む(write)ところまではできたので、ここまでの作業を振り返ってみます。特に趣旨はなく、ファイルシステムとかRustってこんなもんなんだなという話です。知見などは期待しないでください。
ソースコードは下記です。READMEはfork元のままなのであれです。
やったこといろいろ
デフォルトのRust-FUSE::hello_fsでできること
ファイルシステムを展開したフォルダにhello.txtを自動で作成して中身が読めるようになります。
読み書きはできません。
mknod(ファイルの追加)をしてみる
ここで主にやったのは下記です:
- ファイルの情報(inode)を増やせるようにする
- フォルダに書き込みをできるようにする
- mknodのシステムコールの処理を実装する
- lookupのシステムコールをちゃんと処理するようにする
1番は簡単なので2番から話します。
フォルダに書き込みをできるようにする
- let options = ["-o", "ro", "-o", "fsname=hello"]
+ let options = ["-o", "fsname=hello"]
ファイルシステム作成時にオプションを渡してるのでそれを修正すればOKです。
これをやらないといくらmknodなどの実装をしてもアプリケーションがディレクトリに変更を加えようとしても門前払いされます。
mknodの実装
まずsrc/lib.rsからmknodにエラー応答してるやつを引っ張ってきます。
fn mknod(&mut self, _req: &Request<'_>, _parent: u64, _name: &OsStr, _mode: u32, _rdev: u32, reply: ReplyEntry) {
reply.error(ENOSYS);
}
エラーではなくまともな応答を返すためにはreply: ReplyEntry
の使い方を考えます。これはほかのシステムコールの実装でも同じです。ここではReplyEntry
がlookupでも使用されているのでとりあえずそれと同じにすればOKでした。
reply.entry(&TTL, &(self.file_inode[node_idx]), 0);
lookupの改造
当初のlookupはディレクトリとファイルに1つずつの固定の応答しかなかったので改造します。
lookupはfile名を渡してくるので、file名とinode番号を紐づけるテーブルが必要になります。ここでBlockInfo
というものを作成しました。適当に図示すると下のようになります。
実際のファイルシステムではブロック(データ)そのものにファイル名が書いてあったりするようですが、現段階では、ブロック本体とブロックの情報的なものは分離してあります。
writeしてみる
writeに関しては下記のように実装を進めました。
-
BlockInfo
にBlock
を割り当てる - Writeの情報を受け取ってBlockに書く
- ReadでBlockの中身を返す
Blockを実装する
特に術も知らないのでポインターを使います。
メモリの確保はstd::alloc
を使います。はじめはboxなどを検討しましたが扱いが高度だったのでやめました。
定義はこんな感じ:
pub struct BlockBox {
pub data: *mut u8,
pub layout: Layout,
}
使い終わったときにリークされると困るのでdropを実装しておきます。
impl Drop for BlockBox {
fn drop(&mut self)
{
unsafe {
dealloc(self.data, self.layout);
}
}
}
Writeを実装する
mknodと同様に、まずsrc/lib.rs
からwriteの元ネタを引っ張ってきます。
書き込むデータは下記のようにスライスで渡されます。またオフセット(シーク位置)も考慮する必要があります。_fh
はファイルハンドラのようですが差し当たっては無視してても支障ないです。
fn write(&mut self, _req: &Request<'_>, ino: u64, _fh: u64, offset: i64, data: &[u8],
_flags: u32, reply: ReplyWrite)
offsetがあればポインターをずらし、コピーでデータを書き込みます。
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で埋めておきます。
std::ptr::write_bytes(dst_ptr, 0, remain_bytes);
逆にブロックのサイズが不足していればreallocします。reallocしたとき、データはコピーしてもらえるので心配いらないです。
let lay = Layout::from_size_align(new_size, BLOCK_UNIT).ok().unwrap();
bx.data = realloc(dst_ptr, lay, new_size);
bx.layout = lay;
書いたデータをReadで返す
これは特に難しいことないですね。スライスで返します。
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です。
ただしファイルサイズは今書かれているデータの最後尾のバイトを渡す必要があります。そうしないと追記みたいなwriteアクセスで指定されるoffsetがおかしくなります。
- 実際にファイル情報を変更する必要はなく、変更前のファイル情報を返すだけでだいたいOKです。
-
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を実装してやる必要があります。(もう少し深くから必要かも?)
#[cfg(feature = "abi-7-11")]
ll::Operation::IoCtl { arg: _, data: _ } => {
println!("[I] -- IoCtl --");
self.reply::<ReplyEmpty>().error(ENOSYS);
}
……エラー応答させてますね。応答さえできればいいんだろうか……?
ファイルシステム本体に実装する必要はないです。
featureの指定をするのでビルドは下記のようなコマンドで行います。
> cargo build --features abi-7-11,fuse-abi/abi-7-11
ちなみにそもそもどうやってioctlがコールされているかを捕まえたかという話もありますが……printlnでトレースしたわけですね。
その他
workspaceの利用
当初はCargo.toml
に下記を書いてhello_fsをビルドしていましたが、ワーニングが出てました。
[[bin]]
name = "hello_fs"
path = "examples/hello.rs"
なので「Cargoのワークスペース」を少し読んで、rust-fuseプロジェクトの中にhello_fsというサブディレクトリを作り、examples/hello.rs
をhello_fs/src/main.rs
として移動させ、Cargo.tomlにも下記のようにworkspaceを定義しました。
[workspace]
members = [
".",
"fuse-abi",
"fuse-sys",
"hello_fs",
]
workspaceはsrc/main.rsが依存していれば勝手にビルドされるようですが、hello_fsは依存されていないので下記のように-p
でビルドを指定してやります。
> cargo build --features abi-7-11,fuse-abi/abi-7-11 -p hello_fs
まとめ
一通り書いてみると、下記のような知見が得られた気がしました。
- FUSEに関して:
- mknodやwriteなどの実装を追加する方法
- アプリケーションによってioctlなど想定しないシステムコールがやってくる
- Rustに関して:
- メモリのヒープとdropトレイトの実装
- ヒープしたメモリのポインターの扱い
- ビルド時のfeatureの指定
- workspaceの利用
そもそもRustのコーディングにおいては文字列を渡すのですら苦労しました。しかしそれも乗り越え……やはりこういったまとまったコーディングがプログラミング言語全体も含むスキルの向上に役立つなあと感じてます。