ファイルを削除しても、既にオープンされているファイル記述子(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 は独立したオフセットを使用する。
{"id":"vfs-name-and-open-lifetime","title":"名前が削除されても、参照は誰を指しているのか?","summary":"unlink は名前を無効にし、close は記述子を無効にする。dup はオープンインスタンスとオフセットを共有する。独立した open は独立したオフセットを使用する。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"track-row\"><span data-i18n=\"names\"></span><div id=\"names\" class=\"track\"></div></div><div class=\"actions\"><button id=\"link\" data-i18n=\"link\"></button><button id=\"unlinkA\" data-i18n=\"unlinkA\"></button><button id=\"unlinkB\" data-i18n=\"unlinkB\"></button></div><label for=\"fd\" data-i18n=\"choose\"></label><select id=\"fd\"></select><div class=\"track-row\"><span data-i18n=\"fds\"></span><div id=\"fds\" class=\"track\"></div></div><div class=\"actions\"><button id=\"open\" data-i18n=\"open\"></button><button id=\"dup\" data-i18n=\"dup\"></button></div><div class=\"actions\"><button id=\"read\" data-i18n=\"read\"></button><button id=\"close\" data-i18n=\"close\"></button></div><div class=\"track-row\"><span data-i18n=\"content\"></span><div id=\"content\" class=\"track\"></div></div><div class=\"metrics\"><div><span data-i18n=\"links\"></span><strong id=\"links\"></strong></div><div><span data-i18n=\"refs\"></span><strong id=\"refs\"></strong></div></div><div class=\"readout\"><span data-i18n=\"last\"></span><strong id=\"last\"></strong></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"note\"></p></div><div class=\"actions\"><button id=\"reset\" data-i18n=\"reset\"></button></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n.track#fds{flex-direction:column}.readout strong{display:block;font-size:24px}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}let names=[true,false],fds=[0,null,null],files=[{offset:0}],chosen=0,last='—';function draw(){cells('names',names.map((n,i)=>({text:(i?'B':'A')+(n?' → inode 7':' ×'),cls:n?'on':'empty'})));cells('fds',fds.map((f,i)=>({text:'fd '+i+' → '+(f===null?'—':'F'+f+' [offset '+files[f].offset+'] → inode 7'),cls:f===null?'empty':i===chosen?'on':''})));const refs=new Set(fds.filter(x=>x!==null)).size,alive=names.some(Boolean)||refs;cells('content',[{text:alive?'A B C D':t('gone'),cls:alive?'gold':'empty'}]);set('links',names.filter(Boolean).length);set('refs',refs);set('last',last);$('#link').disabled=names[1]||!names[0];$('#unlinkA').disabled=!names[0];$('#unlinkB').disabled=!names[1];$('#open').disabled=!names.some(Boolean)||!fds.includes(null);$('#dup').disabled=fds[chosen]===null||!fds.includes(null);$('#read').disabled=$('#close').disabled=fds[chosen]===null;set('explanation',t('note'));}for(let i=0;i<3;i++){const e=document.createElement('option');e.value=i;e.textContent='fd '+i;$('#fd').append(e);}$('#fd').onchange=()=>{chosen=+$('#fd').value;draw();};$('#link').onclick=()=>{names[1]=true;draw();};$('#unlinkA').onclick=()=>{names[0]=false;draw();};$('#unlinkB').onclick=()=>{names[1]=false;draw();};$('#open').onclick=()=>{const slot=fds.indexOf(null);if(slot<0||!names.some(Boolean))return;fds[slot]=files.length;files.push({offset:0});draw();};$('#dup').onclick=()=>{const slot=fds.indexOf(null);if(slot>=0&&fds[chosen]!==null)fds[slot]=fds[chosen];draw();};$('#read').onclick=()=>{const f=files[fds[chosen]];if(!f)return;last='fd '+chosen+' → '+('ABCD'[f.offset]??'EOF');if(f.offset<4)f.offset++;draw();};$('#close').onclick=()=>{fds[chosen]=null;draw();};$('#reset').onclick=()=>{names=[true,false];fds=[0,null,null];files=[{offset:0}];last='—';draw();};draw();","audio":false,"strings":{"choose":"どのfdを操作するか","close":"選択したfdを閉じる","content":"依然として参照されているコンテンツ","dup":"選択したfdをコピー","fds":"fd → オープンインスタンス → inode 7","gone":"参照なし;コンテンツは回収可能","heading":"名前が削除されても、参照は誰を指しているのか?","intro":"名前とfdを別々に操作する。fdをコピーして交互に読み取り、すべての名前を削除した後、オープンインスタンスとコンテンツがまだ存在するかどうかを観察する。","last":"直近の読み取り","link":"ハードリンク B を追加","links":"名前の数","names":"名前 → inode 7","note":"まず矢印が残っているかどうかを確認し、次に読み込み位置を確認する。fdの番号は再利用されるため、ファイルの同一性を表すために使用してはならない。","open":"既存の名前から open","read":"1バイト読み取る","refs":"オープンインスタンスの数","reset":"リセット","scope":"通常のファイル1つ、名前2つ、fd3つ、コンテンツはABCD。マップ、スナップショット、並行処理なし。最後の参照が消えることは、モデルがコンテンツを必要としなくなることを意味するだけで、ファイルシステムの実際の回収タイミングをシミュレートするものではない。","scopeLabel":"モデルの範囲と規約","unlinkA":"名前 A を削除","unlinkB":"名前 B を削除"}}
以下では、一時ディレクトリ内で共有オフセットと「名前が削除されても、オープン参照からは読み取り可能」を検証する:
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" assert os . read ( c , 2 ) == b " ab" 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 に接続する。