ファイル記述子と読み取り位置
記述子の複製、名前の削除、同名ファイルの再作成を通して、開いた参照が何を保持するかを確かめます。
説明
ファイルをopenすると、以後の読書きに使うファイル記述子が得られます。ディレクトリ内の名前と、開いた記述子からの参照は別です。また、同じファイルを別々にopenする場合と、dupで記述子を複製する場合では読取り位置の共有が異なります。
ALPHAというファイルを開き、複製した記述子と別に開いた記述子から読みます。そのまま名前を削除し、同じ名前でBETAを作って、古い参照と新しい名前が別の内容を指すことを確認します。Unix系のファイル操作を例にします。
1開いた参照の属性
一時領域へALPHAを保存し、読書き可能で開きます。fstatは記述子が参照するファイルの属性を返します。
import os,tempfile; from pathlib import PathファイルAPI; temp = tempfile.TemporaryDirectory(); path = Path(temp.name)/'note'作業領域と名前; path.write_bytes(b'ALPHA')5バイト保存; fd = os.open(path,os.O_RDWR)ファイルを開く; info = os.fstat(fd)記述子から属性取得; print(info.st_size,info.st_nlink)サイズと名前の数保存したバイト数5の後に、サイズ5とリンク数1が表示されます。パスから取得するstatと違い、fstatは開いた参照を対象にするため、後で名前を消しても属性を取得できます。
2複製した記述子は読取り位置を共有
dupで記述子を複製します。一方から2バイト、もう一方から2バイト読みます。
copy_fd = os.dup(fd)記述子の複製; print(os.read(fd,2),os.read(copy_fd,2))二つの記述子で読む; print(os.lseek(fd,0,os.SEEK_CUR))共有された現在位置AL、PHと続けて読まれ、現在位置は4です。二つの記述子が同じopen file descriptionを参照するため、ファイル位置が共有されます。記述子の番号が違うことだけで読取り位置が独立しているとは言えません。
3別にopenした場合の位置
同じパスをもう一度openし、先頭から読みます。先のfdの位置は変わるかも調べます。
separate = os.open(path,os.O_RDONLY)別に開く; print(os.read(separate,2))新しい位置から読む; print(os.lseek(fd,0,os.SEEK_CUR))元の位置新しく開いた方はALを読み、元のfdは位置4のままです。同じinodeでも、別のopenで作ったopen file descriptionは位置を独立して持ちます。
4名前を消してから開いた参照を読む
noteという名前を削除し、fdを先頭へ戻して内容を読みます。
path.unlink()名前を削除; print(path.exists(),os.fstat(fd).st_nlink)パスとリンク数; os.lseek(fd,0,os.SEEK_SET)開いた参照を先頭へ; print(os.read(fd,5))名前なしの実体を読む名前はなくリンク数は0ですが、ALPHAが読めます。途中の0はlseekが返した移動後の位置です。最後の名前が消えても、開いた参照が残る間は実体を使えるため、ログを削除しても容量が直ちに戻らない原因になる場合があります。
5同じ名前に新しいファイルを作る
同じnoteというパスへBETAを書きます。新しいパスの属性と、まだ開いている古いファイルの属性を比べます。
path.write_bytes(b'BETA')同名の新規ファイル; old,new = os.fstat(fd),path.stat()古い参照と新しい名前の属性; print((old.st_dev,old.st_ino)==(new.st_dev,new.st_ino))実体を比較; print(path.read_bytes())新しい内容書込み4バイト、実体の一致はFalse、パスから読む内容はBETAです。同じ名前を再利用しても、既に開いているfdが新しい実体へ切り替わるわけではありません。
6古いファイルへ追記する
古いfdを末尾へ移動し、!を追加します。名前から見えるBETAと古い実体のサイズを比べます。
os.lseek(fd,0,os.SEEK_END)古い実体の末尾; before = os.fstat(fd)変更前の属性; os.write(fd,b'!')1バイト追記; after = os.fstat(fd)変更後の属性; print(after.st_size,path.stat().st_size)二つの実体のサイズ; print(after.st_mtime_ns>=before.st_mtime_ns)変更時刻の順序末尾位置5、書込み1バイト、サイズは古い実体6と新しいファイル4です。mtimeは内容変更時刻で、表示したTrueは前の値以降であることを示します。同じ時刻値になるかは時刻精度にも依存します。ctimeは属性状態の変更時刻であり、Unixで一律に作成時刻を表す項目ではありません。
7一つ閉じても複製した参照は残る
fdと別openのseparateを閉じ、dupで作ったcopy_fdだけを残します。その参照を先頭へ戻して読みます。
os.close(fd); os.close(separate)二つの記述子を閉じる; os.lseek(copy_fd,0,os.SEEK_SET)残った参照を先頭へ; print(os.read(copy_fd,6))古い実体の全内容位置0の後にALPHA!が読めます。名前がなくても残った参照を通して利用でき、別の記述子を閉じただけでは最後の参照になりません。
8最後の参照を閉じる
古い実体への最後の記述子を閉じ、新しいnoteがそのまま読めることを確かめます。
os.close(copy_fd)最後の古い参照を閉じる; print(path.read_bytes())新しい名前の内容; temp.cleanup()一時領域を片付けるBETAが残ります。古い実体は名前も開いた参照もなくなり、資源回収の対象になります。名前の寿命、開いた参照の寿命、同名で新しく作ったファイルの寿命を別々に追うと、置換やログの切替を理解できます。
まとめ
dupは開いた参照のファイル位置を共有し、別のopenは位置を独立して持ちます。
最後の名前を削除しても、開いた参照が残る間はその実体を利用できます。
同じパスを作り直しても、既に開いている記述子は古い実体を参照し続けます。
操作履歴と解説を振り返れます。
Python 3
import os,tempfile; from pathlib import Path; temp = tempfile.TemporaryDirectory(); path = Path(temp.name)/'note'; path.write_bytes(b'ALPHA'); fd = os.open(path,os.O_RDWR); info = os.fstat(fd); print(info.st_size,info.st_nlink)5 5 1
copy_fd = os.dup(fd); print(os.read(fd,2),os.read(copy_fd,2)); print(os.lseek(fd,0,os.SEEK_CUR))b'AL' b'PH' 4
separate = os.open(path,os.O_RDONLY); print(os.read(separate,2)); print(os.lseek(fd,0,os.SEEK_CUR))b'AL' 4
path.unlink(); print(path.exists(),os.fstat(fd).st_nlink); os.lseek(fd,0,os.SEEK_SET); print(os.read(fd,5))False 0 0 b'ALPHA'
path.write_bytes(b'BETA'); old,new = os.fstat(fd),path.stat(); print((old.st_dev,old.st_ino)==(new.st_dev,new.st_ino)); print(path.read_bytes())4 False b'BETA'
os.lseek(fd,0,os.SEEK_END); before = os.fstat(fd); os.write(fd,b'!'); after = os.fstat(fd); print(after.st_size,path.stat().st_size); print(after.st_mtime_ns>=before.st_mtime_ns)5 1 6 4 True
os.close(fd); os.close(separate); os.lseek(copy_fd,0,os.SEEK_SET); print(os.read(copy_fd,6))0 b'ALPHA!'
os.close(copy_fd); print(path.read_bytes()); temp.cleanup()b'BETA'
演習完了