ファイルの編集と置き換えの違い または シェルスクリプトの安全な置き換え
この記事の目的
unixでのファイルの編集と置き換えの違いをまとめます。
ファイル名→inode→ファイル実体の対応づけ
UNIX系OSのファイルシステムは、「ファイル名→ファイル実体」という対応関係ではなく、間にinodeを挟んだ「ファイル名 → inode → ファイル実体」という対応づけを行っています。
「ファイル名→inode」の対応づけは、ディレクトリエントリにより行われます。
ディレクトリ内でファイル名とinode番号の対応づけが行われていて、ls -i
などで確認できます。
「inode→ファイル実体」の対応づけは、ファイルシステム内部で行われ、ユーザからは隠されます。
inodeは、ファイル種類の情報、ファイルのアクセス権限やタイムスタンプ、ディスク上のどこにファイルの実データが存在するかというメタデータを保持するデータ構造です。inodeには番号がついていて、(デバイス番号、inode番号) の組み合わせでファイル実体がシステム全体の中で一意に定まります。
inodeを確認するコマンド例
ls に -i オプションをつけると、ファイルの inode番号を見ることができます。以下は / ディレクトリで ls -i をする例です。
$ ls -i / 123242 bin 141 home 33709218 mnt 19489 run 16892486 tmp 128 boot 123245 lib 50497111 opt 144 sbin 33575067 usr 1025 dev 143 lib64 1 proc 142 srv 50331777 var 16777345 etc 16892485 media 33575041 root 1 sys
この例ではinode番号が同じ1のディレクトリが複数存在することがわかります。これはそれぞれ別のファイルシステムのmount pointで、各ファイルシステムのinode番号1番が付与されているためです。
lsofコマンドで、あるプロセスが扱うファイルを一覧することができます。DEVICEという欄がデバイス番号です。"253,0"のようにmajor, minorの2つの数字の組み合わせです。NODEという欄がinode番号です。 50498017 のように1つの数字が表示されていることがわかります。
$ lsof -p $$ COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME bash 2734 kmoriwak cwd DIR 253,0 4096 33575046 /home/kmoriwak bash 2734 kmoriwak rtd DIR 253,0 224 128 / bash 2734 kmoriwak txt REG 253,0 1150584 50498017 /usr/bin/bash bash 2734 kmoriwak mem REG 253,0 9253600 51182031 /var/lib/sss/mc/passwd bash 2734 kmoriwak mem REG 253,0 46280 1156751 /usr/lib64/libnss_sss.so.2 (中略) bash 2734 kmoriwak 0u CHR 136,0 0t0 3 /dev/pts/0 bash 2734 kmoriwak 1u CHR 136,0 0t0 3 /dev/pts/0 bash 2734 kmoriwak 2u CHR 136,0 0t0 3 /dev/pts/0 bash 2734 kmoriwak 3r REG 253,0 9253600 51182031 /var/lib/sss/mc/passwd bash 2734 kmoriwak 255u CHR 136,0 0t0 3 /dev/pts/0
念のため ls -i /usr/bn/bash をして対応していることを確認しておきましょう。
$ ls -i /usr/bn/bash 50498017 /usr/bin/bash
ファイルの編集
当たり前に聞こえますが、通常のファイルは書き換えることができます。 echo でファイルに書き込んで、内容を編集してもinodeは同じままであることを確認します。
$ echo hoge > hoge.txt $ ls -i hoge.txt 9871508 hoge.txt $ echo fuga > hoge.txt $ ls -i hoge.txt 9871508 hoge.txt
他プロセスでファイルが編集された時、既存プロセスからどう見える?
1つのシステムの中で複数のプロセスが複数ファイルを操作でき、編集ができるので、あるファイルが編集された際に他のプロセスからどう見えるのかを注意する必要があります。
少なくともlinux+glibcでは、ファイル編集がされたあとの読み込みでは全てのプロセスから編集後の状態が読みだされます。(厳密な動作はlibcやカーネルの実装により変わります。NFSのような複数ホストを対象とするサービスは一貫性を犠牲にしてパフォーマンスを上げているため動作が違います。)
簡単なテストプログラムで様子をみてみましょう。 以下はhoge.txt を読んで3秒おきに出力するだけのプログラムです。
loop.py
import time f = open("hoge.txt") while 1: f.seek(0) print(f.read()) time.sleep(3)
これを実行しながら、他の端末で書き換えます。
$ echo hoge > hoge.txt $ python3 loop.py (別の端末で) $ echo fuga > hoge.txt
echoでhoge.txtを書き換えたあと、loop.pyの出力がhogeからfugaに変わることがわかります。
ファイルの置き換え
ファイルの置き換えは、新しいファイル実体を作成して同じ名前に置き換える操作です。inodeに注意してmvコマンドによる置き換え操作をみてみましょう。
$ echo hoge > hoge.txt $ ls -i hoge.txt 9871798 hoge.txt $ echo fuga > fuga.txt $ ls -i fuga.txt 9872073 fuga.txt $ mv fuga.txt hoge.txt $ ls -i hoge.txt 9872073 hoge.txt
「hoge.txt というファイル名から内容をみると"fuga"になっている」という点はさきほどの編集と同じですが、inode番号が変わっています。
他プロセスでファイルが置き換えられた時、既存プロセスからどう見える?
置き換えにより既存プロセスからどう見えるかを確認しましょう。 さきほどのloop.pyを実行しながら、他の端末で書き換えます。
$ echo hoge > hoge.txt $ python3 loop.py (別の端末で) $ echo fuga > fuga.txt (別の端末で) $ mv fuga.txt hoge.txt
今回は loop.py の出力が変わらず、hogeのままであることがわかります。 そのままloop.pyを終了せずに、loop.pyから見たファイルの状態を見てみましょう。
$ ps aux|grep loop.py kmoriwak 4450 0.0 0.0 32224 8704 pts/0 S+ 12:30 0:00 python3 loop.py kmoriwak 4535 0.0 0.0 12136 1140 pts/1 S+ 12:30 0:00 grep --color=auto loop.py $ lsof -p 4450 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME python3 4450 kmoriwak cwd DIR 253,0 37 35375171 /tmp/test python3 4450 kmoriwak rtd DIR 253,0 224 128 / (中略) python3 4450 kmoriwak 3r REG 253,0 5 35375173 /tmp/test/hoge.txt (deleted) $ ls -i hoge.txt 35375180 hoge.txt
ファイル名のあとに(deleted)とあります。これは現在同じ名前で確認できる /tmp/test/hoge.txt とはinode番号が違う別のファイルを参照していることを示しています。
シェルスクリプトの編集により事故が起きる仕組み
シェルスクリプトを実行するときには、(例外はありますが)実行と読み込みを交互に行います。簡単に確認してみます。
ゆっくり実行されるシェルスクリプトyukkuri.shを作ります。
yukkuri.sh
sleep 3 echo hoge sleep 3 echo fuga sleep 3 echo piyo
strace経由でyukkuri.sh を実行して、プロセスの動作を見てみましょう。
$ strace -e read,write -o out.log bash yukkuri.sh
このような出力が得られます。
(略) read(255, "\nsleep 3\necho hoge\nsleep 3\necho "..., 55) = 55 --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=30951, si_uid=1000, si_status=0, si_utime=0, si_stime=0} --- read(255, "echo hoge\nsleep 3\necho fuga\nslee"..., 55) = 46 write(1, "hoge\n", 5) = 5 --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=30952, si_uid=1000, si_status=0, si_utime=0, si_stime=0} --- read(255, "echo fuga\nsleep 3\necho piyo\n", 55) = 28 write(1, "fuga\n", 5) = 5 --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=30953, si_uid=1000, si_status=0, si_utime=0, si_stime=0} --- read(255, "echo piyo\n", 55) = 10 write(1, "piyo\n", 5) = 5 read(255, "", 55) = 0 +++ exited with 0 +++
最初にELFなんとかやGNUなんとかが読み込みされているのはライブラリの読み込みや初期化で、 SIGCHLDは、sleepプロセスの終了を意味しています。 スクリプトの続きを読むこととコマンドの実行を繰り返していることがわかります。
このreadを行う前にファイルの該当部分を編集することで、実行中のスクリプトの内容が一部変更されます。実際に試してみてください。
mattnさんの記事を読むともっとよくわかります。 zenn.dev
安全な置き換え方法
mattnさんの記事でもvimで上書き(置き換え)、cpで上書き(編集)、tarで上書き(置き換え)により再現有無が異なっていますが、利用するツールによりファイルが既に存在する場合の動作が異なります。オプションにより動作が変わるものもあります。不安がある場合は素振りをしてinode番号が変更される事を確認しましょう。
編集するもの:
- cp
- dd
- シェルのリダイレクト
- エディタの一部
置き換えするもの: