ファイルを削除しても容量が減らない理由(unlink とリンクカウント)

ファイルを削除したはずなのにディスク容量が減らない。この現象は Unix のファイル削除の仕組みを理解していないと謎に見える。

削除と unlink は違う

Unix でファイルを「削除」するとき、実際に呼ばれるのは unlink() システムコールだ。

一般的な「削除」のイメージ

ファイルのデータを消去し、ディスク領域を解放する

unlink() の実際の動作

ディレクトリエントリ(ファイル名と inode の紐付け)を削除し、inode のリンクカウントを 1 減らす

import os

# Python の os.remove() や os.unlink() は
# 内部で unlink() システムコールを呼ぶ
os.remove("data.txt")
os.unlink("data.txt")  # 同じ動作

「unlink」という名前が示すとおり、ファイル名と inode の「リンクを解除」するだけで、inode 自体を削除するわけではない。

リンクカウントとファイルの実体

inode はリンクカウント(参照数)を持っている。このカウントがゼロになったとき、初めてファイルの実体が削除される。

# リンクカウントを確認
stat data.txt | grep Links
# Links: 1

# ハードリンクを作るとカウントが増える
ln data.txt data_link.txt
stat data.txt | grep Links
# Links: 2

# 片方を削除してもカウントが 1 なのでデータは残る
rm data.txt
cat data_link.txt  # まだ読める

開いているファイルを削除した場合

ここが重要なポイントだ。ファイルを開いているプロセスがある場合、リンクカウントがゼロになっても inode は削除されない。

import os
import time

# ファイルを開く
f = open("large_file.txt", "r")

# 別のターミナルで削除しても...
# rm large_file.txt

# ファイルは読み続けられる
data = f.read()

# close() して初めてディスク容量が解放される
f.close()

カーネルは「リンクカウント = 0」かつ「オープンしているプロセスがない」の両方を満たしたときに、初めて inode とデータブロックを解放する。

ディスク容量が減らない典型例

ログファイルを削除したのに容量が減らない

ログを書き込んでいるプロセスがファイルを開いたまま。プロセスを再起動するか、ログローテートで truncate する必要がある。

大きなファイルを削除したのに減らない

バックグラウンドプロセスがそのファイルを開いている。lsof でどのプロセスが開いているか確認できる。

# 削除されたが開かれているファイルを探す
lsof | grep deleted
# python  12345 user  3r   REG  8,1  1073741824  0 /tmp/large_file.txt (deleted)

# プロセス 12345 がファイルを開いたまま
# このプロセスを終了すると容量が解放される

/proc からファイルを復元する

Linux では、削除されたがまだ開かれているファイルを /proc 経由で救出できる。

# プロセス 12345 のファイルディスクリプタ 3 が削除されたファイル
ls -l /proc/12345/fd/3
# lrwx------ 1 user user 64 Jan 15 10:00 /proc/12345/fd/3 -> /tmp/large_file.txt (deleted)

# 中身をコピーして復元
cp /proc/12345/fd/3 /tmp/recovered_file.txt

これは「inode がまだ存在している」から可能な技だ。

truncate と unlink の違い

ファイルを空にしたいだけなら、unlink ではなく truncate を使うべきケースがある。

unlink (rm)

ディレクトリエントリを削除。開いているプロセスは古い inode を参照し続ける

truncate

ファイルサイズをゼロにする。inode は同じまま、開いているプロセスにも即座に反映される

# ログファイルを空にする正しい方法
# 悪い例: rm して touch
rm app.log
touch app.log
# → プロセスは古い inode に書き込み続ける

# 良い例: truncate
truncate -s 0 app.log
# または
> app.log
# → 同じ inode なので即座に容量が減る
# Python でファイルを truncate
with open("app.log", "w") as f:
    pass  # 開いて閉じるだけで空になる

# または明示的に
import os
os.truncate("app.log", 0)

まとめ

ファイル「削除」は unlink(リンク解除)

リンクカウントがゼロになっても開いているプロセスがあれば残る

全プロセスが閉じて初めてディスク領域が解放される

ログなどは truncate で空にするのが正解

「削除」という言葉に惑わされず、「リンクを切る」という動作を理解すれば、容量が減らない謎が解ける。