VFS:パス名からオープンされたファイルへ

このページの目次

ファイルを削除しても、既にオープンされているファイル記述子(fd)からは読み取りが可能です。同じパスを2回オープンした場合、読み込み位置が必ずしも共有されるわけではありません。⁠名前、オブジェクト、オープンインスタンスという3つの層をたどることで、VFSのインターフェース間の関係性を結びつけることができます。

VFSは統一されたエントリポイントを提供しますが、具体的な読み書き、待機条件、および機能は、ファイル、パイプ、デバイスなどのオブジェクトによって実装されます。

説明すべき現象該当セクション
2つのfdの読み込み位置が互いに影響し合うのはなぜか?第1節:オープンインスタンス
パスは既にチェック済みなのに、オープンが失敗するのはなぜか?第2~3節:検索と操作ディスパッチ
unlink、close はいつ実際にリソースを解放するのか?第4節:参照実験
同じパスでもコンテナ内で内容が異なるのはなぜか?第5~6節:マウントビューと位置特定

1. まず、名前、オブジェクト、オープンインスタンスを区別する

オブジェクト回答する質問重要な境界
super_blockどのファイルシステムインスタンスか?マウントポイントと1対1で対応するわけではない;複数のマウントで共有可能
inodeどのファイルシステムオブジェクトか?タイプ、権限、サイズなどを含むが、パス文字列を識別子としない
dentry親ディレクトリ内のこの名前は誰に対応するか?メモリ上の名前解決オブジェクトであり、ディスク上のディレクトリエントリをそのままコピーしたものではない
struct file今回のオープンでどのような状態か?open file description に対応し、オフセット、状態フラグ、操作テーブルを保持する
fdプロセスはどのエントリを通じてオープンインスタンスにアクセスするか?小さな整数は再利用可能;FD_CLOEXEC などは記述子層に属する
mount + dentryどのマウントビューでオブジェクトを位置特定するか?両者を組み合わせて struct path を構成する。dentry のみでは完全な位置を記述できない

ハードリンクは、同一のinodeに複数の名前を提供する。シンボリックリンク自体は別のinodeであり、解決時にはその内容に基づいて検索を続ける。通常のローカルファイルにおいて、(st_dev, st_ino) はその時点でのファイルの同一性を比較するために使用できるが、inode番号は削除後に再利用される可能性があるため、恒久的なグローバルIDとして直接使用することはできない。オーバーレイファイルシステムなどの環境では、その識別マッピングルールも考慮する必要がある。

2回のopenと1回のdup:どの層で共有されるか? fd A 最初のopen fd B dup(A) fd C 2回目のopen struct file ① A、Bはオフセットと状態を共有 struct file ② Cは独立したオフセットと状態を持つ 同一のinode 同一のファイル内容とメタデータ 2つのfileはそれぞれ path (mount + dentry) を保持 fd はエントリ、file はオープンインスタンス、inode はファイル名ではない。複数の名前で同一のinodeを指し示すことができる。

図中の参照関係に従ってオフセットの変化を読む:

fd の取得方法オープンインスタンス後続の読み込み位置
独立したopenで同一パス通常、それぞれ独立した struct file各自が進行する
dup、または fork による継承同一の struct file を参照する場合がある片方が読み込むと、共有オフセットが進行する
pread既存のオープンインスタンスを使用するが、明示的にオフセットを指定共有位置は進行しない

図において、AとBはオープンインスタンスを共有しているため、Aが読み込むとBは新しいオフセットから継続する。Cは独立してオープンされているため、このオフセットの変化の影響を受けない。open(2) を参照。

2. なぜパス検索には dentry と mount が必要なのか

/srv/data/a を解析する際には、ディレクトリの権限、ディレクトリエントリ、シンボリックリンク、およびマウントの境界をコンポーネントごとに処理する必要がある。相対パスは作業ディレクトリまたは openat() のディレクトリfdから開始され、絶対パスは呼び出し元のルートディレクトリビューから開始される。最後のコンポーネントは、作成、排他、シンボリックリンクのフォローなどのフラグの影響も受ける。

ネガティブdentry は、「この親ディレクトリ内のこの名前にはinodeが関連付けられていない」という検索結果をキャッシュする。これは重複する失敗検索を減らすことができるが、永久的に存在しないことの証明ではない。作成、名前変更、キャッシュの有効期限切れ、およびリモートファイルシステムの再検証により、結果が変更される可能性がある。

ファストパス:キャッシュ関係が安定している場合、項目ごとの調整を削減

RCU-walk は、RCU保護とシーケンスチェックの下で安定した関係を読み取り、参照カウントとロック操作を削減する。同時変更やブロッキング検証が必要な場合など、対応できない場合は、REF-walk に切り替えるか、再検索を行う。

したがって、dcacheのヒットが、オープン全体でロックなし・待機なしを保証するわけではない。権限チェックと最終的なオープンには、独自の同期要件がある。パス検索の設計 を参照。

検索で存在が確認でき、その後オープンしても、なぜまだ間違っている可能性があるのか?

プログラムが a が存在しないことをチェックした直後に、別のプロセスが a を作成し、その後、最初のプログラムが通常の open(O_CREAT) で相手のファイルを開いたと仮定する。2つのシステムコールの間に事実関係が変化している。新規ファイルの作成では、排他的な要求を O_CREAT | O_EXCL に委ねるべきである。指定されたディレクトリからパスが逸脱することを制限したり、特定種類のシンボリックリンクを禁止したりする必要がある場合は、文字列のプレフィックスチェックのみを行うのではなく、脅威モデルに応じて openat2() の解析制約を使用する。

O_TMPFILE は、指定されたディレクトリが存在するファイルシステム上に無名inodeを作成する独立したオープン方法であり、ファイルシステムのサポートが必要である。これは、適当に名前を付けてすぐに削除する構文糖衣ではない。その後、ディレクトリにリンクできるかどうかは、オープンフラグ、権限、およびリンク方法によって制約される。

3. 操作テーブルは役割分担を担当し、すべての能力が存在することを保証しない

操作テーブル一般的な責任ソースコード読書時のエントリ
super_operationsinode の割り当て/回収、ファイルシステムの同期などファイルシステムインスタンスの管理
inode_operationsディレクトリ検索、作成、リンク、属性操作名前とメタデータ操作
file_operations読み書き、位置指定、マップ、poll、ioctl、fsyncオープン済みオブジェクトの操作
address_space_operationsページキャッシュの充填、プリフェッチ、書き戻しなどキャッシュとバックエンドストレージの連携
dentry_operations再検証、名前比較などファイルシステム固有の名前セマンティクス

カーネルは一般的な補助実装を提供しており、ファイルシステムは再利用を選択できる。コールバックが欠けている場合、自動的に補完されるとは限らない。ある能力が実装されていない場合、呼び出しは失敗するか、インターフェースで規定されたデフォルト動作を持つことがある。例えば、パイプは通常のファイルのようなランダム位置指定をサポートせず、デバイス read() は必ずしもページキャッシュに入らない。操作契約は VFS ドキュメント にあり、具体的なシグネチャは対象となるカーネルのヘッダーファイルに基づくべきである。

通常のファイルの場合、バッファ付き読み取りは通常ページキャッシュを参照し、準備が整わない場合は待機するか、バックエンド読み取りを開始する。バックエンドはブロックデバイス也可以是ネットワークサービスである。バッファ付き書き込みは通常、キャッシュを最初に修正するが、メモリ割り当て、旧データの読み取り、またはダーティページ制限により待機する可能性がある。O_SYNC、明示的な同期、および直接I/Oは完了条件を変更するため、「write はメモリのみを変更し、ここでストレージを待機することはない」という一般化は誤りである。

4. unlink、close、および最終的な解放は別の3つの事象

unlink() はディレクトリ内の名前を削除する。ハードリンクが残っている場合、他の名前を通じて依然としてアクセス可能である。リンク数が0になっても、既存のオープン参照により、コンテンツにアクセスできる場合が一般的である。ディスクスペースがいつ回収されるかは、オープン参照、マップ、ファイルシステムの回収メカニズム、スナップショットなどの要因に依存する。

操作直接変更される関係依然としてコンテンツを保持している可能性のある参照
unlink名前とオブジェクト間のバインディングを削除他のハードリンク、オープン参照、マップなど
close(fd)現在の記述子参照を削除dup、継承、またはマップなどの他の参照
munmapそのマップ関係を取消他のマップ、オープン参照など

ドライバの release は、最後のオープン参照に基づいて解放されるものと理解すべきであり、毎回の close でカウントするものではない。flush などのコールバックは異なるトリガーセマンティクスを持つ。ファイルマップは、元のfdが閉じられた後も有効であり続ける可能性がある。

まず図の中で、名前とfdを別々に操作する。独立したオープン、dup、読み取り、unlink、および close は、異なる参照を変更する。最後の参照が消えるまで、コンテンツとオフセットには明確な所有者が存在する。

図を準備しています
名前が削除されても、参照は誰を指しているのか?

unlink は名前を無効にし、close は記述子を無効にする。dup はオープンインスタンスとオフセットを共有する。独立した open は独立したオフセットを使用する。

以下では、一時ディレクトリ内で共有オフセットと「名前が削除されても、オープン参照からは読み取り可能」を検証する:

import os
import tempfile
from pathlib import Path

with tempfile.TemporaryDirectory(prefix="vfs-demo-") as directory:
    path = Path(directory) / "a"
    path.write_bytes(b"abcdef")
    fds = []
    try:
        a = os.open(path, os.O_RDONLY)
        fds.append(a)
        b = os.dup(a)
        fds.append(b)
        c = os.open(path, os.O_RDONLY)
        fds.append(c)
        assert os.read(a, 2) == b"ab"
        assert os.read(b, 2) == b"cd"   # a とオフセットを共有
        assert os.read(c, 2) == b"ab"   # 2回目の open のオフセットは独立
        assert os.pread(a, 2, 0) == b"ab"
        assert os.lseek(b, 0, os.SEEK_CUR) == 4
        renamed = path.with_name("renamed")
        path.rename(renamed)
        renamed.unlink()
        assert not renamed.exists()
        assert os.fstat(a).st_nlink == 0
        assert os.pread(c, 6, 0) == b"abcdef"
        print("共有オフセット、独立したオープン、削除後の読み取り:通過")
    finally:
        for fd in reversed(fds):
            os.close(fd)

これはローカルの通常のファイルセマンティクスを検証するものであり、停電時の永続性の実験ではなく、すべてのネットワークファイルシステムの削除後の挙動を直接導き出すものでもない。

5. マウントは名前空間に木構造を接続する

マウントにより、パスのトラバーサルが特定の位置で別のディレクトリ木に入る。元の位置の内容は通常、隠蔽され、削除されていない。bind mount は、既存のサブツリーを別の場所に接続し、基盤となるオブジェクトの共有を継続できるため、「すべてのマウントがディスクを読み取り、独立した superblock を作成する」という主張は成立しない。

mount namespace の分離は、マウントビューを分離するものであり、ファイルデータの複製を分離するものではない。2つのnamespace間で共有ファイルシステムを通じて同じファイルにアクセスする場合、相互の書き込みを依然として確認できる。マウントイベントの伝播は、shared/private/slave などの伝播関係に依存する。namespace の作成だけで、すべてのマウント変更が完全に分離されると断言することはできない。

ソースコードのエントリ:まずファイルシステムを設定し、次にビューをマウントする

新しいマウントインターフェースは、ファイルシステムコンテキストの設定、ファイルシステムツリーの取得、マウントの接続位置へのマウントなど、手順を分離し、fd を使用してオブジェクトを管理し、パラメータエラーを報告しやすくしている。カーネル側の fs_context とユーザー側の fsopen/fsconfig/fsmount/move_mount は、関連するが異なるレベルのインターフェースである。ソースコードを読む際は、設定フェーズとマウントフェーズをそれぞれ追跡するべきである。マウント API を参照。

6. 現象から階層へ位置特定する

現象優先して区別すべき点
2つのfdの読み込み位置が互いに影響し合う同一のオープンインスタンスを参照しているか、dup/fork によって取得されたか
大容量ファイルの削除後、スペースが明らかに減少しない依然としてオープン、マップ、またはスナップショットによって保持されていないか。ファイル名のみを確認しないこと
パスは存在するがオープンに失敗する権限、マウントビュー、シンボリックリンク、最終的なオープン権限、および同時変更を検索
コンテナ内で同じパスの内容が異なるルート、mount namespace、およびマウントのオーバーライド関係
open は遅いが、その後の read は速い名前/権限の再検証とデータキャッシュは異なるパスである

読み取り専用の観測からは、findmnt、/proc/self/mountinfo、/proc/self/fd から始めることができる。/proc/sys/fs/dentry-state はシステム全体のキャッシュ統計であり、フィールドは現在のカーネルドキュメントと照合するべきであり、特定のディレクトリのヒット率を表すものではない。さらに fs/namei.c、``fs/dcache.c、fs/namespace.c` を読み、データパスは Page Cache と I/O に接続する。