「Text file busy」の対処 - 実行中バイナリの上書き
「Text file busy」とは何のエラーか?
結論: いま実行中のプログラム(または使用中の共有ライブラリ)の本体ファイルを、上書き・切り詰めしようとして弾かれている状態。カーネルが
ETXTBSY(errno 26)を返している。走っているプロセスを止めるか、上書きではなく「置き換え(rename)」に切り替えれば解決する。
典型的にはバイナリのコピー・ビルド・デプロイで現れる。
cp: cannot create regular file '/usr/local/bin/myapp': Text file busy
Text file busy の "text" は、歴史的な Unix 用語でプログラムの コードセグメント(text segment) を指す。ファイルの中身が日本語テキストかどうかとは無関係で、「実行コードのファイルが使用中」という意味。
カーネルがこのエラーを返すのは、おおむね次の操作のとき。
- 実行中のバイナリを書き込みモードで開こうとした(
cpでの上書き、> fileでのリダイレクトなど) - 使用中の共有ライブラリ(
.so)を上書きしようとした(メモリに mmap 済み) - 書き込み用に開いているファイルを
execve()で実行しようとした(ビルド直後の競合など)
Text file busy は「権限がない(Permission denied)」とも「読み取り専用FS(Read-only file system)」とも別物。権限・FS は正しくても、実行中であるという一点で書き込みが拒否される。エラー文を取り違えると対処を誤る。
なぜ実行中のバイナリは上書きできないのか?
結論: Linux はプロセスが実行中の実行ファイル(とロード中の共有ライブラリ)の inode を「書き込み禁止」として保護する。走行中のコードを途中で書き換えるとクラッシュや未定義動作を招くため、カーネルが
ETXTBSYで先回りして防いでいる。
プログラムを起動すると、カーネルはその実行ファイルの内容を mmap でメモリにマップ して実行する。ページは必要に応じて遅延読み込みされるため、ファイルの実体は実行中ずっと参照され続ける。
ここで誰かがファイルを書き換えると、まだ読み込んでいないページが別物に変わり、整合性が崩れる。これを防ぐためカーネルは inode 単位で「実行中フラグ」を立て、書き込みオープン(O_WRONLY / O_RDWR)や切り詰め(truncate)を ETXTBSY で拒否する。共有ライブラリも実行コードを含むため同じ保護対象になる。
逆方向の保護もある。あるファイルを 書き込み用に開いたまま そのファイルを execve() で実行しようとしても ETXTBSY になる。ビルドスクリプトが出力ファイルを閉じ忘れたまま実行に進むと、このパターンに当たる。
なお、シェルスクリプトは通常このエラーにならない。スクリプトは bash などのインタプリタが「データとして読む」だけで、保護対象の実行イメージにはならないため。ETXTBSY は主に ELF バイナリと共有ライブラリで起きる。
原因プロセスを特定するには?
結論:
lsof <ファイル>またはfuser <ファイル>で、そのバイナリを掴んでいるプロセスを一覧する。出てきた PID が「止めるべき相手」。サービス管理下のプロセスならsystemctlで止めるのが安全。
lsof でファイル単位に見る
lsof に上書きしたいファイルパスを渡すと、それを開いている・実行しているプロセスが分かる。
lsof /usr/local/bin/myapp
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME myapp 4821 deploy txt REG 259,1 6291456 131080 /usr/local/bin/myapp
FD 列の txt が「実行中のテキスト(コード)として使用」を示す。この myapp(PID 4821)が原因。共有ライブラリの場合は mem(mmap 済み)として現れる。
fuser で素早く PID を出す
PID だけ手早く知りたいなら fuser。
fuser /usr/local/bin/myapp
/usr/local/bin/myapp: 4821e
末尾の e は 実行中(executable being run) を意味する。fuser -v で USER / COMMAND まで確認できる。
原因が常駐サービスやデーモンの場合、kill で直接落とすと自動再起動や中途半端な状態を招く。サービスは systemctl stop で止めるのが筋。手動起動のプロセスのみ kill を検討する。
上書きせずに安全に置き換えるには?
結論: 走行中プロセスを止められないなら、
cpでの上書きをやめてmv(rename)やinstallで 新しいファイルに差し替える。rename は inode を作り直してディレクトリエントリを張り替えるだけなので、実行中プロセスは古い inode を保持したまま、新バイナリと衝突しない。
なぜ mv は通って cp は弾かれるのか
cp new /usr/local/bin/myapp… 既存の inode をO_TRUNCで開いて中身を書き換える(in-place)→ 実行中なのでETXTBSYmv new /usr/local/bin/myapp… 新しい inode をmyappという名前に rename し、古い名前を置き換える → 実行中 inode には触れないので成功
ポイントは「同じ場所のファイルシステム内」で完結させること。/tmp(別FS)から mv すると内部的にコピー+削除になり、上書きパスを踏む場合がある。同一ディレクトリに新ファイルを置いてから rename するのが定石。
# 同じディレクトリにダウンロード/ビルドしてから rename cp myapp.new /usr/local/bin/myapp.new # 別名でまず配置 mv /usr/local/bin/myapp.new /usr/local/bin/myapp # 原子的に差し替え
走行中のプロセスは古いバイナリ(ディレクトリから消えたが inode は生存)で動き続け、次回起動時から新バイナリになる。
install を使うとより簡潔
install は「一時ファイルに書いて rename」を内部で行い、権限・所有者もまとめて設定できる。デプロイ用途に向く。
sudo install -m 0755 -o root -g root myapp.new /usr/local/bin/myapp
どうしても同じ inode に書きたいなら先に削除
rm でディレクトリエントリを消してから書けば、新規作成扱いになり ETXTBSY を避けられる(unlink 自体は実行中でも可能)。
sudo rm /usr/local/bin/myapp # 実行中でも unlink は通る sudo cp myapp.new /usr/local/bin/myapp
最も確実なのは「プロセスを止めてから上書き」。サービスなら systemctl stop myapp && cp ... && systemctl start myapp。無停止で差し替えたい場合に rename / install 方式を使う、と覚えると判断が速い。
デプロイやビルドで再発させないには?
結論: デプロイは「上書き」ではなく「rename による原子的差し替え」を原則にする。ビルドでは出力ファイルのディスクリプタを確実に閉じ、実行中の同名バイナリへ直接書かない。
再発を防ぐチェックポイント。
- デプロイスクリプトで
cp -f直書きをやめる … 一時名へ配置 →mv/installで差し替え。多くのデプロイツール(go buildの出力差し替え等)がこの方式を採る - 稼働中サービスは停止→更新→再起動を基本に … 無停止更新が必要なら rename 方式、または systemd の
ExecReload/ ソケットアクティベーションを検討 - ビルド直後の実行は出力 fd を閉じてから … スクリプトで
make && ./outのように繋ぐとき、ビルドが書き込みハンドルを残しているとETXTBSY。明示的に閉じる・別ステップに分ける - 共有ライブラリ更新も同様 … 使用中の
.soは上書きせず rename で差し替え。パッケージマネージャ(apt/dnf)はこの手順を内部で行うため、手動cpでの.so上書きは避ける
NFS など一部のネットワークファイルシステムでは、別ホストで実行中のバイナリに対して ETXTBSY の挙動が一貫しないことがある。共有ストレージ上の実行ファイルを更新する際は、各ホストでプロセスを止めてから差し替えるのが安全。