Open8

OS: ファイルシステム

nukopynukopy

ファイルシステムの勉強めも。GPT 5-thinking と対話しながらまとめさせたもの。

ファイルシステムとは

カーネルが提供する VFS(Virtual File System)API の下で、永続媒体や仮想媒体に「名前・権限・データ」を記録・参照する仕組みと実装群
ユーザ空間プロセスの open/read/write/rename/stat などは VFS に入り、個々のファイルシステムドライバ(ext4, xfs, nfs, 9p/virtiofs, fuse.* など)に委譲される。

コア構成(Linuxの代表的モデル)

  • VFS オブジェクト

    • dentry(directory entry):パス名→inode のキャッシュ。名前解決を高速化。
    • inodeファイルの実体メタデータ(型、モード、UID/GID、サイズ、時刻、リンク数、データ位置の情報=ブロック/エクステント参照)。
    • fileオープン中のハンドル(カーソル/フラグ等)。FDはこれを指す。
  • オンディスク構造(例:ext4)

    • superblock:FS 全体情報(サイズ、ブロックサイズ、機能フラグ)。
    • ブロックグループ:inode テーブル、データブロック、ビットマップ等のまとまり。
    • inode:メタデータ+データブロックへの参照(ext4は通常 extents)。
    • データブロック/エクステントファイルのバイト列の実体。スパース(穴)も表現可能。
  • キャッシュ層

    • page cache:ファイルデータのページ(通常 4KiB)をキャッシュ。書き込みは dirty page として遅延フラッシュ。
    • inode/dentry キャッシュ:名前解決・属性参照の高速化。

「ファイルの実体」はどこか

  • 実体=データブロック(エクステント):ストレージ上の連続/非連続領域に ファイルのバイト列が置かれる。
  • inode は“実体の所在”の索引:権限やサイズとともに、どのブロック群にデータがあるかを保持。
  • ディレクトリは名前表name → inode番号 のレコード集合。名前を消す=そのレコードを削除(実体はリンク数が 0 かつ未オープンになったとき解放)。

名前と実体の関係(削除/リンクの要点)

  • 削除は親ディレクトリの操作rm親ディレクトリの w+x が必要。ファイル権限は無関係。
  • ハードリンク:複数の名前が同一 inode を指す。リンク数が 0 でも FD が開きっぱなしなら実体は解放されない。
  • シンボリックリンク:中身がパス文字列の別 inode。実体は別にある。

読み書きの経路(バッファリング)

  1. write():page cache に書込み → dirty → バックグラウンドで FS ドライバが書き戻し(ジャーナリングの順序制御あり)。
  2. read():page cache にヒットすればメモリから。ミスならブロックから読み込み。
  3. mmap:ページをユーザ空間にマップ。書込みは dirty page として扱われる。
  4. O_DIRECT:page cache をバイパスして I/O(アラインメント等の制約あり)。

権限チェックが行われる場所

  • VFS が UID/GID とモードを参照して user/group/others の順に判定(必要なら ACL/ケーパビリティも考慮)。
  • ファイル内容の更新可否ファイル自身の w
  • 作成/削除/改名親ディレクトリの w+x
  • パス探索には各ディレクトリの x が必要。ls の列挙は r(+通常 x)。

ジャーナリング(ext4の概略)

  • 目的:クラッシュ時にメタデータ整合性を保つ。

  • モード

    • ordered(既定)データを書き戻してからメタデータをジャーナル確定。中身が古ゴミで見えるのを防ぐ。
    • writeback:順序保証を弱め、性能重視。
    • journal:データもジャーナルに書く(堅牢だが遅い)。

ネットワーク/仮想FS(実体がローカルに無い場合)

  • NFS:実体はサーバ側。UID/GID は数値で突合
  • 9p/virtiofs(UTM/QEMUの共有):実体はホスト側ファイルホストの UID/GID がそのまま見える。
  • FUSEユーザ空間プロセスが FS を実装。カーネルの fuse モジュールが /dev/fuse 経由で転送。

FUSE とは

Filesystem in Userspaceカーネルモジュール fuse が VFS 要求をユーザ空間へ転送し、ユーザ空間デーモン(sshfs, bindfs, encfs…)が実装・応答する。
利点:カーネル改修不要で FS を作れる。コスト:ユーザ空間との境界越えでオーバーヘッドがある。

bindfs とは(位置づけと原理)

  • 位置づけ:既存ディレクトリをFUSEで再マウントする変換レイヤ

  • 目的所有者/グループ/モードの“見え方”を写像(例:ホスト 501:20 → ゲスト 1000:1000)。実体(ホスト上の inode/データブロック)は変更しない。

  • 代表オプション

    • map=HOST_UID/GUEST_UID:@HOST_GID/@GUEST_GID
    • perms=...(見かけの r/w/x を調整)
    • create-as-user(新規作成をゲストユーザ所有で露出)

パス解決の正確な流れ(重要)

  • 各段階で x 権限が必要(ディレクトリの探索権)。
  • 最終ファイルは 自身の r/w/x で内容アクセス可否を決定。
  • rm cbw+x が必要。

あなたのケースに適用(UTMの共有)

  • 実体:ホスト側のファイル(macOS)。
  • ゲスト:virtiofs/9p 経由で参照 → UID=501,GID=20そのまま見える
  • 既存ファイルが 0644 なら、vagrant(1000:1000)書けない
  • bindfs を挟み UID/GID を写像すれば、VFS の権限判定で 自分の所有物として w が通る
  • これは 名前や見かけを変えるだけで、ホスト上の実体(inode/データブロック)は不変

検証コマンド(短いセット)

# FS種別とマウント点
stat -fc '%T %m' /home/vagrant/toy-tcpip-rs

# 数値の所有者・inode と権限
ls -lni /home/vagrant/toy-tcpip-rs | head
# → 501:20 が見えればホスト由来

# 名前解決/キャッシュの観点(dentry/inode は内部構造だが)
getconf PAGE_SIZE

まとめ(誤解しがちな点を正確に)

  • ファイルの実体はデータブロック(エクステント)。inode がその場所と属性を持つ。
  • ディレクトリは名前表。削除は親の w+x。内容の更新はファイル自身の w
  • VFS は UID/GID の数値で判定。ホストの UID が見える共有では、そのままだと他人扱い。
  • bindfs は“見え方の変換”実体をいじらず、ゲストから編集可能にする。

他の章(ext4 の on-disk をもう少し掘る/NFS の UID マッピング/virtiofs の security model など)も要るなら続けて出す。

nukopynukopy

まず位置づけ(何者か・どこにいるか)

名前 種別 何をする 実装される場所
ext4 オンディスクのローカルFS ブロックデバイス上にファイル/ディレクトリを保存 Linuxカーネル内のext4ドライバ
VirtFS (9p/virtio-9p) “共有”用のネットワークFS ホストのディレクトリをゲストに見せる(QEMUの9p) ゲストLinuxの9pクライアント(カーネル)+QEMU(ホスト側)
virtio-fs “共有”用の近代FS ホストのディレクトリをゲストに高速共有(DAX等) ゲストLinuxのvirtiofsドライバ(カーネル+FUSE)+ホストのvirtiofsd

つまり:ext4 = 実体を置くFS9p/VirtFS・virtio-fs = 実体を“見せる”ための搬送手段。レイヤが違う。


ext4(ローカルFS:実体はここにある)

  • 対象:/dev/sda1 みたいなブロックデバイス
  • オンディスク構造:superblock、inodeテーブル、ディレクトリエントリ(name→inode)、データブロック(実体)
  • 実装:Linuxカーネルの ext4 ドライバ。ユーザ空間は VFS 経由で使う。

コマンド例

mkfs.ext4 /dev/vdb1
mount -t ext4 /dev/vdb1 /mnt

他OS

  • Windows/macOS標準ではext4を読めない。サードパーティドライバが必要。

VirtFS (9p / virtio-9p)(QEMUの共有:実体は“ホスト”)

  • 目的ホストのディレクトリをゲストに見せる。UTM/QEMUで “VirtFS/9P” と呼ばれるやつ。
  • 仕組み:ゲストは 9p(Plan9) クライアントをカーネルに持ち、virtio 経由で QEMU にRPCを投げ、QEMUがホストの実FSを触る。
  • 特性UID/GIDはホストの数値がそのまま見えやすい → あなたが悩んだ「501:20 で書けない」の正体。

コマンド例(ゲストLinux)

mount -t 9p -o trans=virtio,msize=262144,version=9p2000.L SHARETAG /mnt

他OS

  • ゲスト側に9pクライアントが要るので、Linuxゲストが前提
  • ホストはQEMUが頑張るのでmacOS/Windowsでも“共有自体”は可能(UTMがこれ)。

virtio-fs(より新しい共有:実体は“ホスト”)

  • 目的:ホスト→ゲスト共有を9pより速く・一貫性よく。DAXでページキャッシュを共有できる。
  • 構成:ゲストはvirtiofsカーネルドライバ(FUSE連携)。ホストはvirtiofsd(ユーザ空間デーモン)。両者がvirtioキューでやり取り。

コマンド例(ゲストLinux)

mount -t virtiofs SHARETAG /mnt

他OS

  • ゲスト側:virtio-fsドライバはLinuxで成熟。
  • ホスト側virtiofsdは主にLinuxで実用。macOS/Windowsホストはサポートが弱い/限定的。
    → UTMがmacで採るのは実績のある9p(VirtFS) の方。

それ、カーネルに含まれる?

  • ext4Linuxカーネル内
  • 9p(VirtFS)ゲストLinuxカーネル内にクライアント実装(9p, 9pnet_virtio)。ホスト側はQEMU。
  • virtio-fsゲストLinuxカーネルにドライバ/ホスト側はユーザ空間の virtiofsd
  • Windows/macOS:カーネルに ext4/9p/virtio-fs は標準では入ってない(一部は試験的/外部ドライバ)。

なぜ共通で使えないの?

  1. ドライバ不在:OSごとにカーネルドライバが必要。ext4/9p/virtio-fsはLinux優先で実装が進んだ。
  2. 設計がLinux寄り:virtio(仮想I/O)やFUSEの流儀はLinuxエコシステムが前提。
  3. ホスト依存のデーモン:virtio-fs は virtiofsd(ホスト) が肝。Linux以外の安定実装が薄い。
  4. 保守コスト:Windows/macOSに公式サポートを入れるのはコストが高い&ニーズが限定的。

あなたの環境に当てはめると(UTM on macOS → Linuxゲスト)

  • 実体macOS側のAPFS/HFS+上のファイル。
  • 共有は VirtFS(9p)
  • ゲストはLinuxなので9pクライアントOK。だが**所有者はホストのUID/GIDが“そのまま”**見える → 501:20 問題発生。
  • 解決はbindfsでUID/GIDを書き換えて見せる(またはNFSv4でmap_uid/map_gid)。

実務の選び方(開発用途)

  • 1台のLinux VMでサクッと共有9p(VirtFS)(UTMの既定)。必要なら bindfs で権限マップ。
  • さらに速く & 一貫性重視(Linuxホスト↔Linuxゲスト)virtio-fs
  • 物理ディスク/ボリュームのデータ置き場ext4(Linuxならこれが王道)。
nukopynukopy

前提

macOS ホスト上の UTM/QEMULinux VM を動かし、ホストのディレクトリをVM内にマウントして使うケース。共有方式は主に 9p(VirtFS)。virtio-fs は参考として並記。


関係図(構成要素とレイヤの対応)

  • 太線は共有ディレクトリ経路。あなたのケースは 9p(VirtFS) を使用(virtio-fs は macOS では実運用が限られるため薄色表示)。

シーケンス図:VM 内にてホストからマウントされているディレクトリ内のファイルに書き込む流れ

読みread(2))は概ね逆方向。ヒットすれば ページキャッシュ から返る。
virtio-fs の場合は C9/QSvirtio-fs クライアント / virtiofsd に置き換わり、場面により DAX でページを直接マッピングする。


構成要素(列挙と説明)

ホスト側(macOS)

  • <ファイルシステム (APFS)>
    ホスト上の実体(inode/データブロック)を保持するオンディスクFS。共有ディレクトリの本当の保管場所
  • <ハイパーバイザ (QEMU/UTM)>
    ゲストに virtio デバイスを提示し、9pサーバ(VirtFS)や virtiofsd 相当と連携して、ホストFSへの操作を代理実行する。
  • <9p サーバ (QEMU内)>
    Plan9 の 9p RPC を処理し、APFS に対する open/read/write 等を実行して結果を返す。
  • <virtiofsd (ユーザ空間デーモン)>
    virtio-fs 用のサーバ。FUSE プロトコル相当でゲストとやり取りし、APFS を操作する(macOS では実用が限定的)。

仮想I/O

  • <仮想デバイス層 (virtio)> / <virtqueue>
    ゲスト↔ホスト間の高速キュー。9p/virtio-fs の要求・応答はここを通る。

ゲスト側(Linux)

  • <OS カーネル (linux)>
    VFS/ページキャッシュ/各FSドライバ/ブロック層などのサブシステムを含む。
  • <VFS サブシステム (VFS)>
    open/read/write/rename/stat の共通インタフェース。dentry/inode/page cache を用いて FS 非依存に扱う。
  • <dentry キャッシュ> / <inode キャッシュ>
    パス名→inode、inode属性のキャッシュ。名前解決と属性参照を高速化。
  • <ページキャッシュ>
    ファイルデータのメモリキャッシュ。write は dirty page として遅延書き戻しされ得る。
  • <ファイルシステム (9p クライアント)>
    共有ディレクトリが 9p でマウントされている場合のクライアントFS。VFS からの操作を 9p RPC に変換する。
  • <ファイルシステム (virtio-fs クライアント)>
    共有が virtio-fs の場合のクライアントFS。FUSE 相当の要求で virtiofsd とやり取り。DAX によりページ共有も可能。
  • <FUSE カーネルモジュール>
    ユーザ空間 FS と連携する枠組み。virtio-fs は FUSE プロトコル相当を用いる。
  • <ファイルシステム (ext4 等; VM内のローカルFS)>
    VMの仮想ディスク上のローカルFS。共有ディレクトリとは別経路。
  • <ブロック層>
    ext4 等が使うブロックデバイスI/Oを扱う層。
  • <ユーザプロセス (エディタ/ビルド/シェル)>
    実際に open/write/read を発行するプロセス。

概念・用語(正確な定義)

  • ファイルの実体:オンディスクFS(ここではAPFS/ext4等)の**データブロック(エクステント)**にあるバイト列。
  • inode:ファイルのメタデータ(型/モード/UID/GID/サイズ/時刻)とデータブロック参照を保持する構造。
  • dentry名前→inode の参照(キャッシュ)。
  • VFS:ファイル操作を抽象化するカーネル層。各FSドライバを統一APIで包む。
  • 9p(VirtFS):Plan9 のネットワークFSプロトコル。QEMU は virtio で搬送し、ホストのファイルをゲストに共有。
  • virtio-fs:ゲストとホストのファイル共有専用FS。virtio + FUSE プロトコルで、DAXによりページ共有を最適化。
  • virtio:仮想デバイス用の汎用I/Oフレームワーク。virtqueue を介して要求・応答をやり取り。
  • FUSEユーザ空間でFSを実装するための仕組み。カーネルの fuse モジュールがリクエストを /dev/fuse 経由で転送。
  • ページキャッシュ:ファイルページのRAMキャッシュ。read はヒットで高速、write は dirty → 書き戻し。
  • マウント:あるFSをあるディレクトリ(マウントポイント)に取り付け、その配下を当該FSの中身として見せる操作。
  • UID/GID:ユーザ/グループの数値ID。権限判定は数値で行われる。共有FSではホストの数値が見えることが多い。
  • セキュリティモデル(9p/virtio-fs):ホスト側での UID/GID の扱い方(passthrough / mapped-xattr など)。最終権限はホストFSでも検査される。
  • DAX(virtio-fs):Direct Access。ページキャッシュをバイパス/共有してコピーを減らす最適化。

重要な実務ポイント(このケースの要約)

  • 実体は常にホスト(APFS) にある。VM は 9p/virtio-fs を通じて代理操作しているに過ぎない。

  • 権限は数値UID/GIDで突き合わされ、ホストの数値がゲストに見える構成では vagrant(1000:1000) は“他人”扱いになりやすい。

  • 書けない既存ファイル問題は、

    • 9p 共有の上に bindfs を挟んで UID/GIDを 501:20→1000:1000 に写像 する、
    • あるいは NFSv4 + map_uid/map_gid に切り替えて解消する。
  • 読み書きの性能/一貫性/キャッシュ挙動は 9p と virtio-fs で異なる(macOSホストでは 9p が現実解)。

nukopynukopy

一言で言うと:

Plan 9 は「Unixを作ったベル研チームが、“次のUnix”として作った分散OS」。そのキモが 9P(ナインピー) という“全部をファイルとして遠隔操作する”超ミニマルなプロトコル。今日あなたが見る 9p(VirtFS) は、この9PをVMのホスト↔ゲスト共有に流用してるやつ。


要点だけ固める

Plan 9 from Bell Labs とは

  • 研究OS(1980s–1990s)。Rob Pike / Ken Thompson / Dennis Ritchie らが開発。

  • 目的:分散環境で一貫した名前空間を提供。「あらゆる資源(ファイル、プロセス、ネット、GUI…)を“ファイル”として扱う」を徹底。

  • 仕掛け:

    • per-process namespace(プロセスごとに見えるツリーを合成/差し替えできる)
    • union mountユーザ空間ファイルサーバ(なんでも“fs”化)
    • ウィンドウ系(rio)、編集器(acme/sam)、イベント配線(plumber)など

9P(Plan 9 Filesystem Protocol)

  • Plan 9 の「資源=ファイル」をメッセージ(RPC)で操作するプロトコル。
  • 代表メッセージ:Twalk(名前解決), Topen, Tread, Twrite, Tclunk(close 的), Tstat/Twstat, Tcreate
    fid”(ファイルハンドルID)や qid(一意ID)を使うシンプル設計。
  • 9Pの拡張版に 9p2000.u / 9p2000.L(Linux向け拡張)などがある。

いま目の前の 9p(VirtFS) は何?

  • QEMU/UTM の“ディレクトリ共有”機能の実装。
    ゲストLinux側:**v9fs(9pクライアント)**がカーネルに常駐。
    ホスト側:QEMU内の9pサーバがホストFSを操作。virtio で高速搬送。

  • マウント例(ゲストLinux):

    sudo mount -t 9p -o trans=virtio,version=9p2000.L,msize=262144 SHARETAG /mnt
    
  • 特徴:単純で実装が軽い。だが UID/GIDは“数値で”ホストのまま見えがち(まさにあなたの 501:20 問題)。

virtio-fs とどう違う?

  • virtio-fs は後発の“共有専用FS”。ホストの virtiofsd(ユーザ空間サーバ)と、ゲストの virtio-fsドライバ(FUSE相当)でやり取り。
    DAX でページ共有し、9pより性能・POSIX整合性が良いことが多い。
  • ただし macOSホストでは virtio-fs が弱い/限定的。UTM では実績のある 9p を使うのが現実路線。

ext4 / 9p / virtio-fs の立ち位置(混同しない)

  • ext4:ローカルディスクに“実体”を置くオンディスクFS(Linuxカーネルの ext4 ドライバ)。
  • 9p(VirtFS):ホストのファイルをRPCで見せる“プロトコル/共有FS”(ゲストは v9fs クライアント)。
  • virtio-fs:ホスト↔ゲスト共有専用のファイルシステム(virtio + FUSE相当)。

超簡図:なぜVMで 9p を見るのか

<ユーザプロセス(ゲスト)> → <VFS(ゲストカーネル)>
  → <9pクライアント(v9fs)> → <virtioキュー>
    → <9pサーバ(QEMU/UTM, ホスト側)> → <ホストFS(APFS)>
  • だから“権限判定の材料(UID/GID/perm)”はホスト由来になりやすい。
  • 対処:bindfs で UID/GID を 501→1000 に写像(見え方だけ変える)、または NFSv4 + map_uid/gid へ移行。

よくある疑問に即答

  • カーネルに含まれる?

    • Plan 9 は別OS。
    • Linux には 9pクライアント(v9fs)virtioカーネル内にある。
  • Windows/macOSでも標準で9pクライアント使える?

    • いいえ。標準では薄い/無い。だから**“Linuxゲストで使う共有”**として見ることが多い。
  • 9pは古い?

    • “古いけど軽い”。VM共有では実用本位で現役。virtio-fsが使えるならそっちが良い場面も多い。

いまのあなた向け一行まとめ

Plan 9=分散OSの研究母艦、9P=その“全部ファイル”を遠隔操作するプロトコル、
**9p(VirtFS)**=QEMU/UTMがVM共有に使ってる実装。書けないのは UID/GID がホストの数値のままだから
bindfs で“見え方をマップ”すればサクッと解決、って話。

nukopynukopy
nukopynukopy

vagrant up のログがこんな感じになる。

まず、ホストのディレクトリを /home/vagrant/_mnt_xxx にマウントして、そのあとに /home/vagrant/xxx/home/vagrant/_mnt_xxx をバインドマウントしてる。

/home/vagrant/_mnt_xxx は、9p/virtFS の仕様上このディレクトリ配下のファイルはホストの UID / GID を引き継いでしまうため、VM のログインユーザ(vagrant ssh 時のログインユーザ)ではマウントしたディレクトリ内のファイルを編集できない。

それを回避するために VM のログインユーザの権限を持つディレクトリを作成し、それをマウントしたディレクトリとバインドマウントする。これで権限エラーを回避し、VM のログインユーザとして VM 内で開発ができるようになる。

vagrant up
# --- log ---
Bringing machine 'default' up with 'utm' provider...
==> default: Importing base box 'utm/ubuntu-24.04'...
==> default: Generating MAC address for NAT networking...
==> default: Checking if box 'utm/ubuntu-24.04' version '0.0.1' is up to date...
==> default: Setting the name of the VM: toy-tcpip
==> default: Forwarding ports...
    default: 80 (guest) => 8080 (host) (adapter 1)
    default: 22 (guest) => 2222 (host) (adapter 1)
==> default: Running 'pre-boot' VM customizations...
==> default: Booting VM...
==> default: Waiting for machine to boot. This may take a few minutes...
    default: SSH address: 127.0.0.1:2222
    default: SSH username: vagrant
    default: SSH auth method: private key
    default: 
    default: Vagrant insecure key detected. Vagrant will automatically replace
    default: this with a newly generated keypair for better security.
    default: 
    default: Inserting generated public key within guest...
    default: Removing insecure key from the guest if it's present...
    default: Key inserted! Disconnecting and reconnecting using new SSH key...
==> default: Machine booted and ready!
==> default: Checking for guest additions in VM...
    default: Guest additions detected
==> default: Setting hostname...
==> default: Mounting shared folders...
    default: /Users/nukopy/Projects/LowLayer/toy-tcpip-rs => /home/vagrant/_mnt_toy-tcpip-rs
==> default: Checking for bindfs in VM...
    default: Package manager detected: apt-get
    default: Fuse kernel module is installed
    default: Fuse kernel module is loaded
==> default: Bindfs seems to not be installed on the virtual machine, installing now
    default: Bindfs 1.14.7 is installed
==> default: Machine is ready to use bindfs!
==> default: Creating bind mounts after synced_folders...
    default: /home/vagrant/_mnt_toy-tcpip-rs => /home/vagrant/toy-tcpip-rs