ファイルの編集と置き換えの違い または シェルスクリプトの安全な置き換え

この記事の目的

unixでのファイルの編集と置き換えの違いをまとめます。

  • unix系OSでのファイルの編集と置き換えの違いについて説明する。
  • シェルスクリプトの編集により事故が起きる仕組みを理解する。
  • 安全な置き換えの手順を理解する。

ファイル名→inode→ファイル実体の対応づけ

UNIX系OSファイルシステムは、「ファイル名→ファイル実体」という対応関係ではなく、間にinodeを挟んだ「ファイル名 → inode → ファイル実体」という対応づけを行っています。

f:id:mrwk:20211230113629p:plain
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番号が違う別のファイルを参照していることを示しています。

f:id:mrwk:20211230125255p:plain
ファイル置き換え時のイメージ

シェルスクリプトの編集により事故が起きる仕組み

シェルスクリプトを実行するときには、(例外はありますが)実行と読み込みを交互に行います。簡単に確認してみます。

ゆっくり実行されるシェルスクリプト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
  • シェルのリダイレクト
  • エディタの一部

置き換えするもの:

  • mv
  • tar
  • rpm, debなどのパッケージインストール
  • エディタの一部