Split View: 파일 디스크립터와 inode 완전 가이드: df와 du가 다를 때 무슨 일이 벌어지는가
파일 디스크립터와 inode 완전 가이드: df와 du가 다를 때 무슨 일이 벌어지는가
- 들어가며
- 1. 이름과 실체 — inode가 무엇인가
- 2. df와 du가 다른 이유
- 3. 삭제했는데 공간이 안 돌아올 때
- 4. inode 고갈 — 용량이 남아도 파일을 못 만들 때
- 5. 파일 디스크립터 — 프로세스가 무엇을 열고 있는가
- 6. 한도 — Too many open files 해결하기
- 7. 진단 순서 요약
- 퀴즈: 실력을 확인해 보세요
- 마치며
- 참고 자료
- 이어서 읽기
들어가며
운영을 하다 보면 상식에 어긋나 보이는 상황을 만납니다. 로그를 지웠는데 df의 사용량이 그대로입니다. 용량이 30퍼센트 남았는데 파일을 만들 수 없다고 합니다. 파일 이름을 바꿨을 뿐인데 애플리케이션이 계속 옛 파일에 씁니다. ulimit -n을 올렸는데 여전히 "Too many open files"가 납니다.
네 가지 모두 같은 두 개념을 모르면 설명되지 않습니다. inode와 파일 디스크립터입니다. 그리고 이 둘을 알면 네 가지가 전부 당연한 결과로 보입니다.
이 글은 파일시스템 이론서가 아니라 운영자를 위한 레퍼런스입니다. 개념은 필요한 만큼만 설명하고, 나머지는 전부 "이 상황에서 무엇을 확인하고 무엇을 실행하는가"에 씁니다.
기준은 리눅스 커널 5.x 이상, ext4와 XFS입니다. 파일시스템 종류에 따라 동작이 다른 부분은 그때그때 표기합니다. 특히 inode 관련 특성은 ext4와 XFS가 크게 다르며, 이 차이가 운영 중에 실제로 문제가 되는 지점이기도 합니다.
읽는 순서는 개념에서 증상으로 갑니다. 앞의 두 절에서 inode와 디렉터리 엔트리의 관계를 잡고, 그다음부터는 현장에서 만나는 상황을 하나씩 다룹니다. 급한 상황이라면 마지막의 진단 순서 요약표부터 보고 필요한 절로 올라오는 방식도 괜찮습니다.
1. 이름과 실체 — inode가 무엇인가
리눅스에서 파일은 두 부분으로 나뉩니다.
- inode: 파일의 실체입니다. 크기, 권한, 소유자, 시각, 링크 수, 그리고 데이터 블록의 위치를 담습니다. 파일 이름은 여기 들어 있지 않습니다.
- 디렉터리 엔트리: 이름과 inode 번호를 짝지은 항목입니다. 디렉터리는 결국 이런 짝의 목록입니다.
이 분리가 리눅스 파일 동작의 거의 모든 특이점을 설명합니다.
ls -li /var/log/messages
stat /var/log/messages
ls -i와 stat은 inode 번호와 링크 수를 보여 줍니다. stat 출력에서 Links가 링크 수입니다.
파일을 지운다는 것은 정확히는 디렉터리 엔트리를 지우는 것입니다. 그 결과 링크 수가 1 줄어듭니다. 링크 수가 0이 되고 그 파일을 열어 둔 프로세스도 없을 때 비로소 커널이 데이터 블록을 반환합니다. 이 두 조건이 핵심입니다.
하드 링크와 심볼릭 링크의 차이도 여기서 나옵니다.
echo hello > original.txt
ln original.txt hardlink.txt
ln -s original.txt symlink.txt
ls -li original.txt hardlink.txt symlink.txt
- 하드 링크는 같은 inode를 가리키는 또 하나의 이름입니다. inode 번호가 같고 링크 수가 2가 됩니다. 원본을 지워도 하드 링크로 접근할 수 있습니다.
- 심볼릭 링크는 경로 문자열을 담은 별개의 파일입니다. inode 번호가 다르고, 원본을 지우면 깨집니다.
하드 링크에는 제약이 있습니다. 같은 파일시스템 안에서만 만들 수 있고, 디렉터리에는 걸 수 없습니다. inode 번호가 파일시스템 안에서만 유일하기 때문입니다. 이것이 rsync의 --link-dest 기반 세대 백업이 같은 파일시스템에서만 효율적인 이유이기도 합니다.
여기서 파생되는 결과가 하나 더 있습니다. 파일에 대한 쓰기 권한과 삭제 권한은 별개라는 것입니다. 파일을 지우는 행위는 디렉터리 엔트리를 지우는 것이므로 필요한 것은 그 파일이 아니라 그 디렉터리에 대한 쓰기 권한입니다. 읽기 전용 파일이라도 디렉터리에 쓸 수 있으면 지울 수 있습니다. 공유 디렉터리에서 남의 파일이 지워지는 사고가 여기서 나옵니다.
이 문제의 표준 해법이 sticky 비트입니다.
ls -ld /tmp
sudo chmod 1777 /srv/shared
sticky 비트가 걸린 디렉터리에서는 파일 소유자와 디렉터리 소유자, 그리고 root만 파일을 지울 수 있습니다. /tmp가 이 방식으로 보호됩니다. 여러 사용자가 함께 쓰는 디렉터리를 만들 때는 이 비트를 함께 고려하세요.
2. df와 du가 다른 이유
df는 파일시스템의 슈퍼블록이 관리하는 사용량을, du는 경로를 순회하며 만난 파일의 크기를 더합니다. 둘이 다르면 이유는 대개 셋 중 하나입니다.
이유 1 — 삭제되었지만 열려 있는 파일. 가장 흔합니다. 디렉터리 엔트리가 없으니 du는 못 찾지만, 프로세스가 열고 있으니 블록은 반환되지 않습니다.
sudo lsof -nP +L1
sudo lsof -nP +L1 | awk 'NR>1 {print $1, $2, $7, $9}' | sort -k3 -nr | head
+L1은 문서에 따르면 링크 수가 1보다 작은 열린 파일, 즉 이미 unlink된 파일을 나열합니다.
이유 2 — 마운트에 가려진 파일. 어떤 디렉터리에 파일을 넣은 뒤 그 위에 다른 파일시스템을 마운트하면, 아래 파일은 보이지 않지만 공간은 차지합니다.
sudo mkdir -p /mnt/check
sudo mount --bind / /mnt/check
sudo du -x -sh /mnt/check/var/log
sudo umount /mnt/check
바인드 마운트로 루트를 다시 마운트하면 가려진 파일이 보입니다. 확인 후 반드시 언마운트하세요.
이유 3 — 예약 블록. ext4는 기본적으로 일정 비율을 root 전용으로 예약합니다. df의 사용 가능 용량이 총량에서 사용량을 뺀 값보다 작은 이유입니다.
sudo tune2fs -l /dev/sda1 | grep -i 'reserved'
주의: 예약 비율을 줄이는 tune2fs -m 명령이 인터넷에 흔히 소개되지만, 예약 블록은 단편화 방지와 root의 긴급 작업 공간 확보라는 목적이 있습니다. 데이터 전용 볼륨이 아니라면 함부로 0으로 만들지 마세요.
3. 삭제했는데 공간이 안 돌아올 때
가장 자주 겪는 상황이므로 절차를 정리합니다.
1단계 — 확인.
df -h /var
sudo du -x -sh /var
sudo lsof -nP +L1 | head -20
2단계 — 어느 프로세스인지 특정.
sudo lsof -nP +L1 | awk 'NR>1 {print $2}' | sort -u | while read -r P; do
printf '%s\t%s\n' "$P" "$(ps -o comm= -p "$P")"
done
3단계 — 해제. 순서대로 시도합니다.
# 3-1. 재오픈 신호 (가장 안전)
sudo systemctl reload rsyslog
sudo kill -USR1 "$(cat /run/nginx.pid)"
# 3-2. 파일 디스크립터를 직접 비우기 (프로세스는 유지)
sudo truncate -s 0 /proc/1234/fd/7
# 3-3. 서비스 재기동 (마지막 수단)
sudo systemctl restart myapp
파괴적 명령 경고: truncate -s 0 /proc/PID/fd/N은 그 디스크립터가 가리키는 파일의 내용을 즉시 비웁니다. 잘못된 디스크립터 번호를 지정하면 살아 있는 다른 파일을 날립니다. 실행 전에 반드시 대상 링크를 확인하세요.
sudo ls -l /proc/1234/fd/7
반대로 이 성질을 이용해 삭제된 파일을 복구할 수도 있습니다. 프로세스가 아직 열고 있다면 내용이 살아 있습니다.
sudo ls -l /proc/1234/fd | grep deleted
sudo cp /proc/1234/fd/7 /backup/recovered.log
삭제된 파일의 심볼릭 링크에는 경로 뒤에 삭제되었음을 나타내는 표시가 붙습니다. 이 상태에서 cp로 복사하면 내용을 되살릴 수 있습니다. 로그 파일을 실수로 지웠을 때 가장 먼저 시도할 방법입니다.
4. inode 고갈 — 용량이 남아도 파일을 못 만들 때
df -i
df -ih
IUse%가 100퍼센트면 블록이 남아 있어도 새 파일을 만들 수 없습니다. No space left on device 오류가 나는데 df -h는 여유롭다면 이것입니다.
범인을 찾습니다. 파일 개수가 많은 디렉터리를 찾는 문제입니다.
sudo find /var -xdev -type f 2>/dev/null | awk -F/ '{print "/"$2"/"$3}' | sort | uniq -c | sort -nr | head -20
-xdev는 다른 파일시스템으로 넘어가지 않게 합니다. 이미 부하가 높은 상태라면 이 스캔 자체가 부담이 되므로 범위를 좁혀 실행하세요.
전형적인 원인은 다음과 같습니다.
- PHP 세션 파일이나 애플리케이션 캐시가 정리되지 않고 쌓임
- 메일 큐가 밀림
- 임시 파일을 만들고 지우지 않는 배치
- 컨테이너 이미지 레이어의 다수 소파일
ext4에서는 inode 개수가 파일시스템 생성 시점에 고정됩니다. 나중에 늘릴 수 없으므로, 고갈되면 파일을 지우거나 파일시스템을 다시 만드는 수밖에 없습니다. 생성 시 밀도를 조정할 수 있습니다.
sudo mkfs.ext4 -i 8192 /dev/sdb1
sudo mkfs.ext4 -N 10000000 /dev/sdb1
-i는 inode 하나당 바이트 수(작을수록 inode가 많아짐), -N은 inode 개수 직접 지정입니다.
XFS는 다릅니다. inode를 동적으로 할당하므로 사실상 고갈 문제가 거의 없습니다. 다만 옛 커널이나 특정 설정에서 inode가 특정 영역에만 할당되는 제한이 있을 수 있으므로, 대량의 소파일을 다루는 볼륨이라면 XFS를 고려하는 편이 안전합니다. 정확한 옵션과 제약은 사용 중인 배포판의 mkfs 관련 man 페이지에서 확인하세요.
파괴적 명령 경고: mkfs는 대상 장치의 모든 데이터를 지웁니다. 장치 이름을 한 글자 잘못 쓰면 운영 볼륨이 사라집니다. 실행 전에 반드시 확인하세요.
lsblk -f
sudo blkid /dev/sdb1
findmnt /dev/sdb1
5. 파일 디스크립터 — 프로세스가 무엇을 열고 있는가
파일 디스크립터는 프로세스가 연 파일을 가리키는 정수입니다. 0, 1, 2는 각각 표준 입력, 표준 출력, 표준 오류로 관례가 잡혀 있습니다.
ls -l /proc/1234/fd
ls -l /proc/1234/fd | wc -l
sudo lsof -p 1234
sudo lsof -p 1234 | wc -l
/proc/PID/fd의 각 항목은 문서에 따르면 실제 파일을 가리키는 심볼릭 링크입니다. 소켓이나 파이프는 파일 경로 대신 종류와 inode 번호 형태로 표시됩니다.
lsof의 FD 열에는 숫자 외에 다음 값이 나옵니다.
cwd: 현재 작업 디렉터리rtd: 루트 디렉터리txt: 실행 파일 텍스트mem: 메모리 매핑된 파일
숫자 뒤의 문자는 접근 모드입니다. r은 읽기, w는 쓰기, u는 읽기·쓰기입니다.
유용한 조회 조합입니다.
sudo lsof -nP -iTCP -sTCP:LISTEN
sudo lsof -nP -i :5432
sudo lsof -u appuser
sudo lsof +D /var/lib/myapp
sudo lsof -c nginx -a -u www-data
sudo lsof -t -c nginx
-a는 조건들을 AND로 결합합니다. 이것이 없으면 조건들이 OR로 처리되어 예상보다 훨씬 많은 결과가 나옵니다.-t는 PID만 출력하므로 다른 명령에 넘기기 좋습니다.+D는 지정한 디렉터리 아래 전체를 재귀 탐색합니다. 느리므로 범위를 좁혀 쓰세요.
6. 한도 — Too many open files 해결하기
이 오류의 원인은 네 층에 걸쳐 있습니다. 한 층만 고치면 해결되지 않는 경우가 많습니다.
층 1 — 프로세스별 소프트/하드 한도.
ulimit -Sn
ulimit -Hn
cat /proc/1234/limits | grep 'open files'
/proc/PID/limits를 보는 것이 확실합니다. 셸에서 ulimit을 바꿔도 이미 떠 있는 프로세스에는 반영되지 않기 때문입니다.
층 2 — 로그인 세션 한도. /etc/security/limits.conf 또는 /etc/security/limits.d/ 아래 파일입니다.
appuser soft nofile 65535
appuser hard nofile 65535
이 설정은 PAM을 거치는 로그인 세션에만 적용됩니다. systemd가 띄우는 서비스에는 적용되지 않습니다. 이 사실을 모르면 "설정했는데 왜 안 되지"에서 오래 헤맵니다.
층 3 — systemd 유닛 설정.
[Service]
LimitNOFILE=65535
서비스로 실행되는 프로세스의 한도는 여기서 정해집니다. 적용은 데몬 재적재와 서비스 재기동이 모두 필요합니다.
sudo systemctl daemon-reload
sudo systemctl restart myapp
cat /proc/"$(systemctl show -p MainPID --value myapp)"/limits | grep 'open files'
층 4 — 시스템 전체 한도.
cat /proc/sys/fs/file-nr
cat /proc/sys/fs/file-max
sysctl fs.file-max
file-nr은 세 값을 보여 줍니다. 할당된 디스크립터 수, 사용되지 않는 할당분, 최대치입니다. 첫 값이 세 번째에 근접하면 시스템 전체 한도에 도달한 것입니다. 현대 시스템에서 file-max 기본값은 충분히 크므로 여기서 걸리는 일은 드뭅니다.
inotify 감시 한도도 같은 계열의 문제를 만듭니다. 파일 감시를 많이 쓰는 개발 도구나 로그 수집기에서 자주 걸립니다.
cat /proc/sys/fs/inotify/max_user_watches
cat /proc/sys/fs/inotify/max_user_instances
값을 영구적으로 바꾸려면 sysctl 설정 파일에 넣습니다. 커널 파라미터 조정 전반은 리눅스 커널 파라미터 튜닝 가이드를 참고하세요.
7. 진단 순서 요약
증상별로 무엇부터 확인할지 정리합니다.
| 증상 | 첫 명령 | 다음 확인 |
|---|---|---|
| 지웠는데 용량이 안 줄어듦 | lsof +L1 | 해당 프로세스 재오픈 신호 |
| 용량 남는데 파일 생성 실패 | df -i | 소파일 다수 디렉터리 탐색 |
df와 du가 크게 다름 | lsof +L1 | 바인드 마운트로 가려진 파일 확인 |
| Too many open files | /proc/PID/limits | systemd LimitNOFILE 확인 |
| 이름을 바꿨는데 옛 파일에 계속 씀 | ls -l /proc/PID/fd | 재오픈 신호 또는 재기동 |
| 로그 파일을 실수로 삭제 | ls -l /proc/PID/fd | cp /proc/PID/fd/N으로 복구 |
| 백업 용량이 예상보다 큼 | find -links +1 | 하드 링크 보존 옵션 확인 |
마지막 줄을 조금 더 설명하면, 하드 링크가 많은 디렉터리를 하드 링크를 모르는 도구로 복사하면 각 링크가 별개 파일로 복사되어 용량이 몇 배가 됩니다.
find /backup -type f -links +1 | head
du -sh --count-links /backup/daily.0
rsync -aH /backup/ /backup2/
rsync의 -H 옵션이 하드 링크를 보존합니다. tar도 기본적으로 하드 링크를 인식하지만, 아카이브에 두 파일이 모두 포함되어야 관계가 유지된다는 점을 기억하세요.
파일시스템 자체의 손상이 의심되면 검사를 돌립니다.
sudo umount /dev/sdb1
sudo fsck -n /dev/sdb1
sudo xfs_repair -n /dev/sdb1
파괴적 명령 경고: fsck와 xfs_repair는 마운트된 파일시스템에 실행하면 손상을 일으킬 수 있습니다. 반드시 언마운트 후 실행하고, -n 옵션으로 먼저 읽기 전용 점검을 수행하세요. -n은 아무것도 고치지 않고 문제만 보고합니다.
퀴즈: 실력을 확인해 보세요
퀴즈 1: 10GB 로그 파일을 지웠는데 df의 사용량이 그대로입니다. 무슨 일이 벌어진 것인가요?
정답: 파일을 연 프로세스가 살아 있어 inode가 해제되지 않았습니다
설명: 파일 삭제는 디렉터리 엔트리를 지울 뿐입니다. 링크 수가 0이 되어도 열린 디스크립터가 남아 있으면 커널은 블록을 반환하지 않습니다.
sudo lsof -nP +L1 | head
sudo systemctl reload rsyslog
가장 안전한 해결은 프로세스에 재오픈을 알리는 것입니다. 그것이 불가능하면 디스크립터를 직접 비우는 방법도 있지만 대상 확인이 필수입니다.
sudo ls -l /proc/1234/fd/7
sudo truncate -s 0 /proc/1234/fd/7
퀴즈 2: df -h는 여유 40퍼센트인데 파일 생성이 실패합니다. 확인 명령과 대응은?
정답: df -i로 inode 사용률을 확인합니다
설명: 블록과 inode는 별개 자원입니다. 작은 파일이 수백만 개면 용량이 남아도 inode가 먼저 고갈됩니다.
df -i
sudo find /var -xdev -type f | awk -F/ '{print "/"$2"/"$3}' | sort | uniq -c | sort -nr | head
ext4는 inode 개수가 생성 시점에 고정되므로 나중에 늘릴 수 없습니다. 즉시 대응은 불필요한 소파일 정리이고, 근본 대응은 XFS 사용 검토나 inode 밀도를 높인 재생성입니다.
퀴즈 3: systemd 서비스에 limits.conf로 nofile을 65535로 설정했는데 반영되지 않습니다. 왜일까요?
정답: limits.conf는 PAM 로그인 세션에만 적용되고 systemd 서비스에는 적용되지 않습니다
설명: 서비스의 한도는 유닛 파일에서 정합니다.
[Service]
LimitNOFILE=65535
적용 후 실제 값을 확인해야 합니다.
sudo systemctl daemon-reload
sudo systemctl restart myapp
cat /proc/"$(systemctl show -p MainPID --value myapp)"/limits | grep 'open files'
ulimit -n을 셸에서 확인하는 것은 그 셸의 값일 뿐 서비스의 값이 아닙니다.
퀴즈 4: 애플리케이션이 쓰던 로그를 실수로 지웠습니다. 프로세스는 아직 살아 있습니다. 복구 가능할까요?
정답: 가능합니다. 열린 디스크립터를 통해 내용을 복사하면 됩니다
설명: 프로세스가 파일을 열고 있는 한 데이터 블록은 살아 있습니다.
sudo ls -l /proc/1234/fd | grep deleted
sudo cp /proc/1234/fd/7 /backup/recovered.log
주의할 점은 프로세스를 재기동하는 순간 복구 기회가 사라진다는 것입니다. 그래서 이 상황에서 첫 행동은 재기동이 아니라 복사여야 합니다.
퀴즈 5: 하드 링크와 심볼릭 링크의 실무적 차이를 하나만 든다면?
정답: 하드 링크는 원본을 지워도 데이터가 유지되고, 심볼릭 링크는 깨집니다
설명: 하드 링크는 같은 inode를 가리키는 또 하나의 이름입니다. 링크 수가 0이 되어야 데이터가 해제되므로, 이름 하나를 지워도 다른 이름으로 접근할 수 있습니다.
ls -li original.txt hardlink.txt symlink.txt
stat original.txt | grep Links
제약도 함께 기억하세요. 하드 링크는 같은 파일시스템 안에서만 만들 수 있고 디렉터리에는 걸 수 없습니다. 반면 심볼릭 링크는 파일시스템 경계를 넘고 디렉터리도 가리킬 수 있지만, 대상이 사라지면 깨집니다.
퀴즈 6: 세대 백업 디렉터리를 다른 서버로 복사했더니 용량이 다섯 배가 되었습니다. 원인은?
정답: 하드 링크를 보존하지 않아 각 링크가 별개 파일로 복사되었습니다
설명: --link-dest 기반 세대 백업은 변경되지 않은 파일을 하드 링크로 공유합니다. 복사 도구가 이 관계를 모르면 모든 세대의 모든 파일을 실제로 복사합니다.
find /backup -type f -links +1 | head
rsync -aH /backup/ /backup2/
-H(--hard-links)가 하드 링크를 보존합니다. -a에는 포함되지 않으므로 반드시 별도로 지정해야 합니다.
마치며
이 글의 내용을 한 문장으로 줄이면 이렇습니다. 리눅스에서 파일 이름은 실체가 아니라 실체를 가리키는 하나의 참조일 뿐입니다.
이 한 문장에서 나머지가 따라 나옵니다. 이름을 지워도 다른 참조가 남아 있으면 데이터는 살아 있습니다. 그래서 지운 파일의 용량이 안 줄어들고, 그래서 지운 파일을 복구할 수 있고, 그래서 이름을 바꿔도 프로세스는 옛 파일에 계속 씁니다.
운영 체크리스트로 옮기면 세 줄입니다. 용량 문제에서는 df -h와 df -i를 함께 봅니다. 지웠는데 안 줄면 lsof +L1을 봅니다. 한도 문제는 셸이 아니라 /proc/PID/limits에서 확인합니다. 이 세 줄이 이 글의 실전 요약입니다.
참고 자료
- lsof(8) — man7.org (2026-08-15 확인)
- proc_pid_fd(5) — man7.org (2026-08-15 확인)
- rsync(1) — man7.org (2026-08-15 확인)
- tar(1) — man7.org (2026-08-15 확인)
이어서 읽기
- 이전 글: TLS 인증서 완전 가이드
- 다음 글: 리눅스 방화벽과 접근 제어 완전 가이드
- 리눅스 장애 대응 명령어 완전 가이드 — 디스크 부족의 초기 진단
- 리눅스 커널 파라미터 튜닝 가이드 — 시스템 한도 조정
- Linux 터미널 — lsof와 stat 연습
- chmod 계산기 — 권한 비트 확인
A Complete Guide to File Descriptors and Inodes: What Happens When df and du Disagree
- Introduction
- 1. Name and substance — what an inode is
- 2. Why df and du disagree
- 3. When you delete and the space does not come back
- 4. inode exhaustion — when free space is not enough to create a file
- 5. File descriptors — what a process has open
- 6. Limits — fixing Too many open files
- 7. Diagnostic order summary
- Quiz: check your understanding
- Closing
- References
- Further reading
Introduction
Run systems long enough and you meet situations that look like they violate common sense. You deleted the logs and the usage reported by df has not moved. There is 30 percent of the volume free and the system says it cannot create a file. All you did was rename a file, and the application keeps writing to the old one. You raised ulimit -n and you still get "Too many open files".
None of the four can be explained without the same two concepts: inodes and file descriptors. And once you know those two, all four look like obvious consequences.
This article is not a filesystem textbook; it is a reference for operators. It explains the concepts only as far as it has to, and spends everything else on "in this situation, what do you check and what do you run".
The baseline is Linux kernel 5.x or later, ext4 and XFS. Where behaviour differs by filesystem type, it is called out at that point. In particular the inode characteristics of ext4 and XFS differ substantially, and that difference is exactly where it becomes a real operational problem.
The reading order goes from concepts to symptoms. The first two sections establish the relationship between inodes and directory entries, and from there each section covers a situation you meet in the field. If you are in a hurry, starting from the diagnostic order summary at the end and working back up to the section you need is also fine.
1. Name and substance — what an inode is
In Linux a file is split into two parts.
- inode: the substance of the file. It holds size, permissions, owner, timestamps, link count, and the location of the data blocks. The file name is not in here.
- directory entry: an item pairing a name with an inode number. A directory is, in the end, a list of such pairs.
This separation explains almost every peculiarity of how files behave on Linux.
ls -li /var/log/messages
stat /var/log/messages
ls -i and stat show the inode number and the link count. In stat output, Links is the link count.
Deleting a file is, precisely, deleting a directory entry. The result is that the link count drops by one. Only when the link count reaches zero and no process has the file open does the kernel release the data blocks. Those two conditions are the crux.
The difference between hard links and symbolic links falls out of this too.
echo hello > original.txt
ln original.txt hardlink.txt
ln -s original.txt symlink.txt
ls -li original.txt hardlink.txt symlink.txt
- A hard link is another name pointing at the same inode. The inode number is identical and the link count becomes 2. You can still reach the data through the hard link after deleting the original.
- A symbolic link is a separate file containing a path string. Its inode number is different, and it breaks when the original is deleted.
Hard links come with restrictions. They can only be made within the same filesystem, and they cannot be placed on directories. That is because inode numbers are only unique within a filesystem. It is also the reason generational backups built on the --link-dest option of rsync are only efficient within one filesystem.
There is one more consequence that falls out of this. Write permission on a file and permission to delete it are separate things. Deleting a file is deleting a directory entry, so what you need is write permission on the directory, not on the file. Even a read-only file can be deleted if you can write to the directory. This is where the accident of someone else's file disappearing from a shared directory comes from.
The standard fix for this problem is the sticky bit.
ls -ld /tmp
sudo chmod 1777 /srv/shared
In a directory with the sticky bit set, only the file owner, the directory owner, and root can delete a file. /tmp is protected this way. When you create a directory several users share, consider this bit along with the rest.
2. Why df and du disagree
df reports the usage tracked by the filesystem superblock; du walks a path and adds up the sizes of the files it meets. When the two disagree, the reason is usually one of three.
Reason 1 — a file that is deleted but still open. This is the most common. There is no directory entry so du cannot find it, but a process has it open so the blocks are not released.
sudo lsof -nP +L1
sudo lsof -nP +L1 | awk 'NR>1 {print $1, $2, $7, $9}' | sort -k3 -nr | head
According to the documentation, +L1 lists open files whose link count is less than 1, which is to say files that have already been unlinked.
Reason 2 — files hidden under a mount. If you put files in a directory and then mount another filesystem over it, the files underneath become invisible while still taking up space.
sudo mkdir -p /mnt/check
sudo mount --bind / /mnt/check
sudo du -x -sh /mnt/check/var/log
sudo umount /mnt/check
Mounting the root again with a bind mount makes the hidden files visible. Be sure to unmount afterwards.
Reason 3 — reserved blocks. By default ext4 reserves a certain percentage for root only. That is why the available space reported by df is smaller than total minus used.
sudo tune2fs -l /dev/sda1 | grep -i 'reserved'
Caution: the tune2fs -m command that reduces the reserved percentage is commonly suggested on the internet, but reserved blocks exist to prevent fragmentation and to guarantee root an emergency working space. Unless this is a data-only volume, do not casually set it to zero.
3. When you delete and the space does not come back
This is the situation you hit most often, so here is the procedure.
Step 1 — confirm.
df -h /var
sudo du -x -sh /var
sudo lsof -nP +L1 | head -20
Step 2 — identify which process.
sudo lsof -nP +L1 | awk 'NR>1 {print $2}' | sort -u | while read -r P; do
printf '%s\t%s\n' "$P" "$(ps -o comm= -p "$P")"
done
Step 3 — release it. Try these in order.
# 3-1. Reopen signal (safest)
sudo systemctl reload rsyslog
sudo kill -USR1 "$(cat /run/nginx.pid)"
# 3-2. Empty the file descriptor directly (the process stays up)
sudo truncate -s 0 /proc/1234/fd/7
# 3-3. Restart the service (last resort)
sudo systemctl restart myapp
Destructive command warning: truncate -s 0 /proc/PID/fd/N immediately empties the contents of the file that descriptor points at. Specify the wrong descriptor number and you wipe out a different, live file. Always verify the target link before you run it.
sudo ls -l /proc/1234/fd/7
Conversely, you can exploit this same property to recover a deleted file. If a process still has it open, the contents are alive.
sudo ls -l /proc/1234/fd | grep deleted
sudo cp /proc/1234/fd/7 /backup/recovered.log
The symbolic link for a deleted file carries a marker after the path indicating that it was deleted. Copying with cp in that state brings the contents back. This is the first thing to try when you delete a log file by mistake.
4. inode exhaustion — when free space is not enough to create a file
df -i
df -ih
If IUse% is at 100 percent, you cannot create a new file even with blocks left over. If you get a No space left on device error while df -h looks roomy, this is it.
Now find the culprit. It is the problem of finding directories with large file counts.
sudo find /var -xdev -type f 2>/dev/null | awk -F/ '{print "/"$2"/"$3}' | sort | uniq -c | sort -nr | head -20
-xdev keeps it from crossing into another filesystem. If the system is already under load, this scan is itself a burden, so narrow the scope before you run it.
The typical causes are as follows.
- PHP session files or application caches piling up without being cleaned
- A backed-up mail queue
- Batch jobs that create temporary files and never delete them
- The many small files in container image layers
On ext4, the inode count is fixed at the moment the filesystem is created. It cannot be increased later, so once it is exhausted your only options are to delete files or recreate the filesystem. You can adjust the density at creation time.
sudo mkfs.ext4 -i 8192 /dev/sdb1
sudo mkfs.ext4 -N 10000000 /dev/sdb1
-i is the number of bytes per inode (smaller means more inodes), and -N specifies the inode count directly.
XFS is different. It allocates inodes dynamically, so exhaustion is effectively a non-issue. That said, older kernels or particular configurations may have a restriction that confines inodes to a specific region, so for volumes handling large numbers of small files, considering XFS is the safer choice. Check the exact options and constraints in the mkfs man pages for the distribution you are running.
Destructive command warning: mkfs erases all data on the target device. Get one letter of the device name wrong and a production volume disappears. Always verify before running it.
lsblk -f
sudo blkid /dev/sdb1
findmnt /dev/sdb1
5. File descriptors — what a process has open
A file descriptor is an integer pointing at a file a process has opened. By convention 0, 1, and 2 are standard input, standard output, and standard error respectively.
ls -l /proc/1234/fd
ls -l /proc/1234/fd | wc -l
sudo lsof -p 1234
sudo lsof -p 1234 | wc -l
According to the documentation, each entry under /proc/PID/fd is a symbolic link pointing at the actual file. Sockets and pipes are shown as a type and an inode number instead of a file path.
Besides numbers, the FD column of lsof shows these values.
cwd: current working directoryrtd: root directorytxt: program text (the executable)mem: memory-mapped file
The letter after the number is the access mode. r is read, w is write, and u is read/write.
Here are some useful query combinations.
sudo lsof -nP -iTCP -sTCP:LISTEN
sudo lsof -nP -i :5432
sudo lsof -u appuser
sudo lsof +D /var/lib/myapp
sudo lsof -c nginx -a -u www-data
sudo lsof -t -c nginx
-acombines conditions with AND. Without it the conditions are treated as OR, which produces far more results than you expected.-tprints only PIDs, which makes it easy to pipe into another command.+Drecursively searches everything under the given directory. It is slow, so narrow the scope.
6. Limits — fixing Too many open files
The cause of this error spans four layers. Fixing only one layer often does not solve it.
Layer 1 — the per-process soft and hard limits.
ulimit -Sn
ulimit -Hn
cat /proc/1234/limits | grep 'open files'
Reading /proc/PID/limits is the reliable way, because changing ulimit in a shell does not affect processes that are already running.
Layer 2 — the login session limit. This is /etc/security/limits.conf or a file under /etc/security/limits.d/.
appuser soft nofile 65535
appuser hard nofile 65535
This configuration applies only to login sessions that go through PAM. It does not apply to services started by systemd. Not knowing this leaves you stuck for a long time on "I configured it, so why does it not work".
Layer 3 — the systemd unit setting.
[Service]
LimitNOFILE=65535
The limit for a process run as a service is decided here. Applying it requires both a daemon reload and a service restart.
sudo systemctl daemon-reload
sudo systemctl restart myapp
cat /proc/"$(systemctl show -p MainPID --value myapp)"/limits | grep 'open files'
Layer 4 — the system-wide limit.
cat /proc/sys/fs/file-nr
cat /proc/sys/fs/file-max
sysctl fs.file-max
file-nr shows three values: the number of allocated descriptors, the allocated-but-unused portion, and the maximum. When the first approaches the third, you have reached the system-wide limit. On modern systems the default file-max is large enough that hitting it here is rare.
The inotify watch limits produce problems of the same family. Development tools and log collectors that use a lot of file watching hit these often.
cat /proc/sys/fs/inotify/max_user_watches
cat /proc/sys/fs/inotify/max_user_instances
To change the values permanently, put them in a sysctl configuration file. For kernel parameter tuning in general, see the Linux kernel parameter tuning guide.
7. Diagnostic order summary
Here is what to check first, organised by symptom.
| Symptom | First command | Next check |
|---|---|---|
| Deleted but usage did not drop | lsof +L1 | Reopen signal for that process |
| Space left but file creation fails | df -i | Hunt for directories full of small files |
df and du differ a lot | lsof +L1 | Check for files hidden by a mount, using a bind mount |
| Too many open files | /proc/PID/limits | Check systemd LimitNOFILE |
| Renamed it but writes still go to the old file | ls -l /proc/PID/fd | Reopen signal or restart |
| Deleted a log file by mistake | ls -l /proc/PID/fd | Recover with cp /proc/PID/fd/N |
| Backup size larger than expected | find -links +1 | Check the hard link preservation option |
To expand on the last row a little: copying a directory full of hard links with a tool that does not understand hard links copies each link as a separate file, multiplying the size several times over.
find /backup -type f -links +1 | head
du -sh --count-links /backup/daily.0
rsync -aH /backup/ /backup2/
The -H option of rsync preserves hard links. tar also recognises hard links by default, but remember that the relationship is only kept if both files are included in the archive.
If you suspect damage to the filesystem itself, run a check.
sudo umount /dev/sdb1
sudo fsck -n /dev/sdb1
sudo xfs_repair -n /dev/sdb1
Destructive command warning: running fsck and xfs_repair on a mounted filesystem can cause corruption. Always unmount first, and perform a read-only check with the -n option before anything else. -n fixes nothing and only reports problems.
Quiz: check your understanding
Quiz 1: You deleted a 10GB log file and df usage has not moved. What happened?
Answer: The process that had the file open is still alive, so the inode was not released
Why: Deleting a file only removes the directory entry. Even when the link count reaches zero, the kernel does not release the blocks while an open descriptor remains.
sudo lsof -nP +L1 | head
sudo systemctl reload rsyslog
The safest resolution is to tell the process to reopen. If that is impossible, emptying the descriptor directly is an option, but verifying the target is mandatory.
sudo ls -l /proc/1234/fd/7
sudo truncate -s 0 /proc/1234/fd/7
Quiz 2: df -h shows 40 percent free but file creation fails. What command do you run and what do you do?
Answer: Check inode utilization with df -i
Why: Blocks and inodes are separate resources. With millions of small files, inodes run out first even with space to spare.
df -i
sudo find /var -xdev -type f | awk -F/ '{print "/"$2"/"$3}' | sort | uniq -c | sort -nr | head
On ext4 the inode count is fixed at creation time and cannot be increased later. The immediate response is to clean up unnecessary small files; the underlying response is to consider XFS or to recreate the filesystem with a higher inode density.
Quiz 3: You set nofile to 65535 in limits.conf for a systemd service and it did not take effect. Why?
Answer: limits.conf applies only to PAM login sessions and not to systemd services
Why: The limit for a service is set in the unit file.
[Service]
LimitNOFILE=65535
After applying it you have to verify the actual value.
sudo systemctl daemon-reload
sudo systemctl restart myapp
cat /proc/"$(systemctl show -p MainPID --value myapp)"/limits | grep 'open files'
Checking ulimit -n in a shell gives you that shell's value, not the service's.
Quiz 4: You accidentally deleted the log an application was writing. The process is still alive. Can you recover it?
Answer: Yes. Copy the contents out through the open descriptor
Why: As long as the process has the file open, the data blocks are alive.
sudo ls -l /proc/1234/fd | grep deleted
sudo cp /proc/1234/fd/7 /backup/recovered.log
The thing to watch out for is that the moment you restart the process the chance of recovery is gone. So in this situation the first action is a copy, not a restart.
Quiz 5: If you had to name one practical difference between a hard link and a symbolic link, what would it be?
Answer: A hard link keeps the data even when the original is deleted, while a symbolic link breaks
Why: A hard link is another name pointing at the same inode. The data is only released once the link count reaches zero, so deleting one name still leaves the other name usable.
ls -li original.txt hardlink.txt symlink.txt
stat original.txt | grep Links
Remember the restrictions along with it. Hard links can only be created within the same filesystem and cannot be placed on directories. A symbolic link, by contrast, crosses filesystem boundaries and can point at a directory, but it breaks when the target disappears.
Quiz 6: You copied a generational backup directory to another server and it came out five times larger. Why?
Answer: Hard links were not preserved, so each link was copied as a separate file
Why: Generational backups based on --link-dest share unchanged files as hard links. If the copying tool does not understand the relationship, it physically copies every file of every generation.
find /backup -type f -links +1 | head
rsync -aH /backup/ /backup2/
-H (--hard-links) preserves hard links. It is not included in -a, so you must specify it separately.
Closing
Reduce this article to one sentence and it comes out like this. On Linux a file name is not the substance, only one reference pointing at the substance.
Everything else follows from that one sentence. Delete a name and the data stays alive as long as another reference remains. That is why the space taken by a deleted file does not come back, that is why you can recover a deleted file, and that is why a process keeps writing to the old file after you rename it.
Moved into an operational checklist it is three lines. For capacity problems, look at df -h and df -i together. When a deletion does not reduce usage, look at lsof +L1. For limit problems, check /proc/PID/limits rather than the shell. Those three lines are this article's practical summary.
References
- lsof(8) — man7.org (verified 2026-08-15)
- proc_pid_fd(5) — man7.org (verified 2026-08-15)
- rsync(1) — man7.org (verified 2026-08-15)
- tar(1) — man7.org (verified 2026-08-15)
Further reading
- Previous: A Complete Guide to TLS Certificates
- Next: A Complete Guide to Linux Firewalls and Access Control
- A Complete Guide to Linux Incident Response Commands — first-pass diagnosis of a full disk
- A Guide to Tuning Linux Kernel Parameters — adjusting system limits
- Linux Terminal — practice lsof and stat
- chmod Calculator — check permission bits