ファイルシステムのためのRust
(lwn.net)- 2024年の LSFMM+BPF Summit で、LinuxファイルシステムにRustを適用する方法が議論され、2023年12月のRFC以降に出た2本目のRFCパッチが議論の中心となった
- Rust-for-Linux側は、ファイルシステムAPIの要件を Rustの型システム に組み込み、コンパイル時のエラー検出、リソース解放の自動化、メモリ関連脆弱性の削減を狙う
iget_locked()の事例は、Cの呼び出し側が直接処理していたnullチェック、inode状態の区別、失敗処理などを、Rustのget_or_create_inode()が 型と自動クリーンアップ で強制しようとする方向性を示している- Dave Chinner、Christian Brauner、James Bottomley、Ted Ts'o らは、C APIとRust APIの名前の不一致、API同期、オブジェクトのライフサイクル差異、50以上のファイルシステム を考慮したメンテナンス負担を懸念した
- 対立の核心は、Rust抽象化の利点そのものよりも、Cコードが変わり続ける中でRustバインディングと抽象化を追随させる 痛みを誰が負担するのか にある
LSFMM+BPFで開かれたファイルシステムRustセッション
- 2024年の Linux Storage, Filesystem, Memory Management, and BPF Summit で、Wedson Almeida FilhoとKent OverstreetがLinuxファイルシステムにRustを使う方法を取り上げた
- Almeidaは2023年12月にファイルシステム向けRust抽象化のRFCパッチセットを投稿しており、このアプローチをめぐって意見の相違があった
- セッションが開かれた5月中旬の同じ日に、Almeidaは2本目のRFCパッチ版を投稿し、ほかのRust関連トピックと合わせて議論しようとした
Rust-for-Linuxのファイルシステム抽象化の目標
- 提案されたファイルシステム抽象化は、Rust-for-Linux project が目指す方向性を反映している
- 核心は、ファイルシステムAPIの要件を Rustの型システム でより多く表現し、コンパイル時にミスを捕捉することにある
- Cコードでは簡単に提供しにくい作業も自動化しようとしている
- 例: リソースの解放
- 目標は、ファイルシステム開発体験をより生産的にし、コンパイラが見つけられる問題のデバッグ時間を減らし、メモリ関連脆弱性 を低減することにある
- Overstreetはbcachefsで2週間がかりのバグ追跡をあまりにも多く経験しており、RustはCより多くのものを提供すると見ている
- Rustは未定義動作を排除する
- コード内部で何が起きているかを見られる機能を提供する
- Rustコードの正しさを証明できるようになれば、機能開発を妨げるバグははるかに少なくなると見ている
iget_locked() をめぐる型システムの事例
- Almeidaはスライドで、現在のカーネルの
iget_locked()には複雑な要件がある例として挙げた - Cの呼び出し側は複数の条件を自分で処理しなければならない
- 戻り値がnullかどうかを確認しなければならない
- 返された
struct inodeが新しいinodeなのか既存のinodeなのかを確認しなければならない - 新しいinodeであれば、使用前に初期化しなければならない
- 初期化に失敗した場合は
iget_failed()を呼び出さなければならない
- Al ViroはAlmeidaのスライドにある
iget_locked()呼び出し側の要件の一部に同意せず、詳細な動作をめぐって議論が続いた - Overstreetは、こうした規則を Rustの型と抽象化 にカプセル化すれば、コンパイラが正しい処理を強制できると見ている
- Almeidaが提示したRust側の対応関数は
get_or_create_inode()だった- Cと同じく、失敗の有無は確認しなければならない
- 成功時、呼び出し側は通常の参照カウント付きinodeを受け取るか、新しいinodeを受け取る
- 通常のinodeは、オブジェクトがそれ以上参照されなくなると参照カウントが自動的に減少する
- 新しいinodeは、初期化されなければ
iget_failed()に相当する処理を自動的に呼び出す - 新しいinodeが一度初期化されると通常のinodeになり、その後は参照カウントの自動減少が適用される
- これらの動作は 型システム によって強制される
- Viroは、これらの制約が実際のソースコードのどこに定義されるのか疑問を呈した
- Almeidaは、Viroやほかのファイルシステム開発者から制約を把握したうえで、それを強制する型と抽象化を作ろうとしているのだと答えた
C APIとRust APIの間の断絶
- Dave Chinnerは、C APIとRust APIの名前が異なると、既存の開発者がCコードを見てRustで対応する呼び出しを知るのが難しいと見ている
- 同じ名前を使わなければ、既存の開発コミュニティにとって完全に見慣れないAPIになり得るという懸念も出た
- Cコードが変わるとRustコードも追随しなければならないため、その作業を誰が担うのかが問題として残る
- Almeidaは議論が必要な事項だと認めた
- 名前変更に反対しているわけではない
- ただし
iget_locked()が良い名前だとは見ておらず、より良い名前を作る機会とも捉えられるとした
- Viroは、
iget_locked()はsuperblockオブジェクトのメンバー関数ではなくライブラリ関数なので、例としての選択は良くないと見ている - Almeidaは、
get_or_create_inode()もライブラリ関数に変えられ、例は制約を型にエンコードする方法を示すためのものだったと答えた
汎用抽象化と単純なファイルシステム中心アプローチの選択
- Christian Braunerは、Rust抽象化がすべてのカーネルファイルシステムのための汎用抽象化なのか、Rustで書かれたより単純なファイルシステムに必要な機能中心なのかを、まず決めるべきだと見ている
- 長期的に
get_or_create_inode()のような関数がiget_locked()よりはるかに多くの制約を含むようになると、問題が生じ得る - Cコードは特に初期にはRustコードより速く進化し得るため、2つのAPIを継続的に 同期 しなければならない
- Overstreetは、Rust抽象化を追加しながらリファクタリングと整理を同時に行うかどうかが核心であり、それが必要だと強く見ている
- James Bottomleyは、オブジェクトのライフサイクルはRust APIにはエンコードされるが、Cにはそれに対応する表現がないと見ている
- 片方でオブジェクトのライフサイクルが変わると、もう片方にバグが生じ得る
- Chinnerは、inodeオブジェクトのライフサイクルは時にファイルシステムごとに異なると述べた
- 単一のライフサイクル理解をAPIに入れると、一部のファイルシステムではその関数群が動作しない可能性がある
- Almeidaは、この例は現在
iget_locked()を呼び出しており恩恵を受けられるファイルシステムにだけ使われるものだと答えた- Rust開発者たちは、ファイルシステムが現在行っている方法を変えるよう強制しようとしているわけではない
「痛みを誰が負担するのか」
- Ted Ts'oは、Rustという「宗教」へ全員を転向させようとする試みがあるように見えるが、Linuxには 50以上のファイルシステム があり、すぐに移行されるわけではないと述べた
- Cコードは改善され続け、その変更がRustバインディングを壊すと、そのバインディングに依存するファイルシステムも壊れ得る
- Ts'oは当面Rustバインディングは二級市民であり、壊れたRustバインディングはファイルシステムコミュニティ全体ではなくRust-for-Linux開発者の問題だと見ている
- 彼は、Rustバインディング開発とCコードの進化を並行しながら、大量の意味を型システムに組み込むアプローチが良いのか悪いのかは1〜2年以内に明らかになるだろうと見ている
- Ts'oにとって大きな変化は、結局 痛みの配分 の問題である
- C APIを変更する開発者は、影響を受けたCコードは直しても、Rustを知らないためRustバインディングは直さないと言える
- Almeidaは、C APIを固定しようとしているのではなく、ファイルシステム開発者がAPIの意味を説明してくれれば、それをRustにエンコードしようとしているのだと答えた
- Bottomleyは、意味がバインディングにより多くエンコードされるほど、同期の観点ではより脆くなり得ると見ている
- Almeidaは、APIが変わればAPI利用者を更新しなければならないのは、ほかの利用者と同じだと答えた
メソッド、関数、型に何を入れるか
- Viroは、
iget_locked()の代替案がメソッドに依存している点をあらためて問題視した- メソッドを使うと引数が明示的に指定されないと見ている
- Overstreetは、メソッドへの不満は継承に過度に依存するC++のような言語に由来すると見ている
- Rustはそうではなく、Rustのメソッドはおおむね構文上の要素だと述べた
- Jan Karaは、inode自体に伴う動作と
iget_locked()関数に内在する動作を区別した- inodeには参照カウントとその処理のような動作が伴う
iget_locked()関数には別の動作が内在する
- OverstreetとAlmeidaは、両方の部分が型にエンコードされるが分離されており、inode型を使うほかの関数は互いに異なる性質の戻り値を持ち得ると答えた
- Viroは、VFSでinodeが現在の方式で動作する理由を説明し、小さく始めて進む方向を見ようという点には同意した
- Overstreetは、今回の例は複雑で出発点として良くなかったかもしれないと述べ、Viroは「いや、そうではない」と答えてセッションは締めくくられた
1件のコメント
Hacker News のコメント
各ファイルシステムが inode のライフサイクルをカスタムで持つと言いながら、同じライフサイクル管理関数を使い、セマンティクスだけが異なるという点が理解できない
同じ関数が実装の詳細によって異なる使われ方をしなければならないなら、抽象化レイヤーの逆のように聞こえる
inode のライフサイクルがファイルシステムごとに異なるなら、ファイルシステムごとの関数で管理すべき
そうした情報を集めていくと、このような疑問がそもそも出ないようにリファクタリングすべき箇所が見えてくるかもしれず、それは良いことだ
ファイルシステムごとの動作を扱いつつ、カーネル側には一貫したインターフェースを維持する VFS レイヤーの概要だ
inode のライフサイクルは、議論を始めるための初期事例だったのかもしれない
コンパイラが一時参照を手助けする一方で、ファイルシステムは依然としてリンク数をディスクに保存する必要がある
iget_locked()はそのうちの特定のパターンだすべてのファイルシステムがその方式を使うわけではなく、状況によっては使わないこともある
たとえば FAT は inode 番号を生成し、FAT 上の位置から inode への独自のマッピングを維持するため使わない
procのように inode オブジェクトをキャッシュしないファイルシステムもあるinode オブジェクト自体はどこから来たにせよ状態の流れは同じに見えるため、利用者側から見た
inodeの使い方は変わらない変わるのは、ファイルシステムレイヤーが inode オブジェクトを作成し、内部で扱う方法だ
質問の仕方を間違えているのではないかと思う
RustがCをもっと簡単に呼び出せるように変わるべきなのだろうか?
Rustを少し触ってみたが、趣味の開発者の立場ではCとどう相互運用すればよいのか、まだ明確ではない
一方、C++やObjective Cでは正しいヘッダーをインクルードして関数を呼び出せばよい
SwiftはObjective Cファイルを取り込めるし、そこからCを呼び出せる
この場合は、カーネル開発者に言語へ合わせることを期待するより、Rust言語のほうが少し柔軟になるべきではないかと思う
外部関数を宣言して呼び出せばよい
たとえばRustの本の https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html#usin... にあるように、
extern "C"で宣言できる複雑なライブラリで宣言をすべて手で書きたくないなら、Cヘッダーファイルから
extern宣言を自動生成するbindgenのようなツールを使えばよい: https://github.com/rust-lang/rust-bindgenbindgenのようなものがRustに含まれていて、サードパーティ依存や
build.rsの設定なしで使えるとよい、という主張はできるが、この記事の核心はそこではない問題は低レベルのバインディングではなく、Rustらしい高レベルラッパーであり、任意のCコードからそのようなラッパーを自動で作り出す汎用ツールはあり得ない
記事はRustでカーネルのファイルシステムドライバーなどを実際に実装する方法を探る内容
カーネル内のRustコードは必然的にCインターフェースを利用するという点も重要
想定している用途にはbindgenがかなりよく合う: https://github.com/rust-lang/rust-bindgen
Rustから呼び出すには
extern "C" fn foo() -> Tを宣言し、#[link]属性やbuild.rsでリンクフラグを渡せばよいbindgenクレートでバインディングを事前生成するか、build.rsで作ってinclude!()で取り込める通常は生成されたバインディングだけを含む**
-sysクレート**を作り、実際のコードではそのsysクレートのバインディングを普段どおりuseするC++やObjective Cでも、正しいヘッダーをインクルードするだけでなく、ライブラリにリンクする必要がある
双方向の呼び出しは可能だが、CがRustで表現できるものを表現できないなら、双方が共通で使うべきAPI設計に重要な影響が生じる
C関数を
extern "C"で宣言して呼び出せばよい通常は
unsafeが必要で、参照を生ポインタに変換したりキャストしたりする必要があるが、構文自体は簡単Cヘッダーファイルをスキャンして宣言を作るツールもあり、bindgenが最もよく使われている
この記事の論点は言語自体というより、Rustをどう使うかに近い
Rust-for-Linuxの開発者たちは、Rustの機能と型システムでAPI呼び出しのセマンティクスをエンコードし、より安全でミスの少ないものにしたいと考えている
C側の人たちは、そうするとC APIの動作やセマンティクスを発展させにくくなるのではないかと懸念している
C APIが変わるとRust APIも直す必要があり、その作業を引き受けたくないから
より受け入れやすい代替案は、Rust APIにセマンティクスをエンコードする際にRustの機能と型システムをあまり使わないこと
そうすればC API変更時のRust API更新は機械的で単純になるが、Rust-for-LinuxがRustの機能でより良く安全なAPIを作れないなら、この作業にどんな意味があるのか疑問が生じる
ただ、言語を十分に理解していないと認めながら、この話題について断定的に語るのは少し変だ
このRust APIがC APIを包むものなのか、それとも再実装なのか、Linuxファイルシステムに詳しくないので明確ではなかった
再実装または別APIなら、C APIと名前をそのまま維持するのは時間がたつほど混乱を生むと思う
最初は慣れた開発者がより早く理解する助けになるとしても、そうだと思う
iget_locked()相当のものを示しており、名前はget_or_create_inode()だった答えは再実装であり、同じ名前は使わない方向に見える
こうした議論が普段たどる流れと変更の規模を考えると、今回の議論は驚くほど礼儀正しいほうだ
このスレッドの否定的な雰囲気には同意しない
関係者たちが核心的な痛点を無駄話なしに明確に伝えたという点で、かなり楽観的に見ている
実際の議論は、強い意見を持つ変わり者たち同士のプログラミング言語論争らしく、激しく、散漫で、揚げ足取りも多かったのだろうと思う
この要約を書いたJake Edgeは、そうした部分を取り除いて核心だけを書くのが非常にうまいようだ
lwn.netページ下部のコメントの一部はかなり無礼
自分が貢献しているオープンソースプロジェクトにScience advances one funeral at a timeのようなコメントを受けるところを想像してみればよい
Linux カーネルに選択肢が増えるのは常に有益
ただし Rust がすべての答えとは限らない
Rust は安全なプログラミングモデルを保証しようと最善を尽くすが、そのモデルにも限界はある
メモリの問題なら Rust を使い、並行性の問題なら Rust に置き換えよう、というふうに見えるかもしれないが、
unsafeブロックなしに C がしているすべてのことをできるわけではないRust はこうした問題に新しい視点を与えられるが、完全な解決策ではない
C、とくにカーネルで使われる C は、暗黙のルールを全員が完全に把握しているべきだという責任を各人に押しつける
それはスケールしない
同じデータ構造を使うカーネル開発者たちが一部屋に集まっても、そのルールについて完全には合意できなかった
Rust は、知っておくべきルールを表に出し、誰か別の人がルール遵守を保証できるなら、それを自分の問題ではなくすることに強い
ときには結果が最適でないこともあるが、Linux カーネルでも最適でないデフォルトが正しい場合は多く、より高い性能のために奇妙なルールをさらに6つ学ぶ余力のある人には
unsafeという抜け道を用意すればよいunsafeブロックは使える必要なときだけ使うべきというだけ
影響範囲が非常に限定された
unsafeブロックを使ったからといって、残りのコードで得られる保証がすべて消えるわけではないunsafeコードが必要だというのは正しいしかし
unsafeを使わなければならないから Rust は適していない、というのは誤解Rust の安全/非安全の区分は、コードのどの部分が非安全なのかを明確に示し、小さな部分に監査を集中させ、その部分さえ正しければ残りは動くと信頼できるようにすることが目的
ファイルシステムコードで必ず
unsafeでなければならない十分な理由はあるのだろうかおそらく数か所に必要な、ごく小さなサブセットだと思う
内部で何が起きているのか直感的ではない
議事録を見ると、カーネル内の Rust は追加の複雑性コストのように見える
オペレーティングシステムを最初から書き直すなら、言語の力を十分に使える
しかし既に巨大なコードベースの横に付け足すと、ここで見られるような追加の問題が生じる
Asahi Linux の Rust GPU ドライバが1か月で作られたというブログを見ればよい
Google で
tales of the m1 gpuを検索すればよく、筆者は Hacker News に非常に否定的な考えを持っている望むならリンクで読める: https://asahilinux.org/2022/11/tales-of-the-m1-gpu/
普遍的に当てはまるかは、今後数年を見なければならない
そのコストは、未来と進歩を受け入れるために必要なものと見なされるだろう
なぜ未知のバグや妥協の多い巨大な流れに飛び込むのではなく、安全なサブセットに制限しないのか疑問
既に巨大すぎるからといって、革新を止めて無期限の保守状態に入るべきだという意味ではない
現実の税金のように、一方のコストが別の問題を相殺するために使われるなら、純損失ではないかもしれない
結論も出ていない単一の議論だけを見て、何の問題も相殺できないだろうと言うのは、論旨が浅く見える
C API と Rust API の名前がずれていて、C コードを見ても対応する Rust の呼び出しが分からないという部分は、古い命名規則との戦いのように見える
同じ名前を維持しつつ、代替名が欲しいときは新しい名前が古い名前をラップするようにして、うまく解決したことがある
それでも命名は難しい
残りの2つは並行性とオフバイワンエラー