서비스 · 프로세스 관리 15 / 50 · Part 2. 시그널·자원·세션
실습 환경: Rocky Linux 9.8 · Ubuntu 24.04.5 (systemd로 부팅한 Docker 격리 컨테이너, 테스트 계정 analyst)

1. 들어가며

"로그 파일을 지웠는데 디스크 사용률이 그대로입니다." 운영에서 매우 흔한 장애 문의다. 원인은 거의 항상 같다. 어떤 프로세스가 그 파일을 아직 열고 있다. 이 상황을 이해하려면 프로세스가 파일을 다루는 단위인 파일 디스크립터(fd) 를 알아야 한다.

이번 글에서는 lsof와 /proc/PID/fd로 프로세스가 연 파일을 확인하고, 삭제됐지만 열려 있는 파일을 재현해 찾고, 비우고, 복구하는 방법을 실습한다. 이 기법은 침해 사고에서 공격자가 지운 파일을 되살리는 포렌식 기법이기도 하다.


2. 핵심 개념

2-1. 파일 디스크립터

항목내용
정의프로세스가 연 파일·소켓·파이프를 가리키는 정수 번호
기본 fd0 = 표준입력, 1 = 표준출력, 2 = 표준에러
새 fd가장 작은 빈 번호부터 할당 (보통 3부터)
대상일반 파일, 디렉터리, 장치, 소켓, 파이프, eventfd 등 모든 것
확인ls -l /proc/PID/fd, lsof -p PID
한도ulimit -n (17편)

2-2. lsof 주요 옵션

명령용도
lsof -p PID특정 프로세스가 연 파일
lsof /path/file특정 파일을 연 프로세스
lsof +D /dir디렉터리 아래 파일을 연 프로세스 (재귀)
lsof -u USER사용자별
lsof -c NAME명령 이름별
lsof +L1링크 수가 1 미만(삭제된) 파일
lsof -i :PORT네트워크 (16편)
-n -PIP·포트 이름 변환 생략 (빠르고 정확)

2-3. lsof 출력의 FD·TYPE 열

FD의미TYPE의미
cwd작업 디렉터리REG일반 파일
rtd루트 디렉터리DIR디렉터리
txt실행 파일CHR문자 장치
mem메모리 매핑 파일unix / IPv4소켓
3r / 3w / 3ufd 3 읽기 / 쓰기 / 읽기쓰기a_inodeeventpoll 등 커널 객체
W (예: 3uW)파일 전체 쓰기 잠금FIFO파이프

3. 동작 원리

파일 디스크립터와 "삭제했는데 용량이 안 줄어드는" 이유

rm은 파일을 지우는 명령이 아니라 디렉터리에서 이름(링크)을 제거하는 명령이다(unlink). 커널은 inode의 링크 수와 열린 fd 수가 모두 0 이 될 때 데이터 블록을 해제한다.

dd → big.log 생성 (링크 1, 열린 fd 0)
tail -f big.log  → 링크 1, 열린 fd 1
rm big.log       → 링크 0, 열린 fd 1  ← 이름은 없지만 데이터는 살아 있다 (df 변화 없음)
: > /proc/1332/fd/3 → 크기 0 으로 잘라냄 → 블록 해제 (df 0%)
tail 종료       → 열린 fd 0 → inode 완전 해제

같은 원리로 01편의 실행 중 삭제된 실행 파일 도 프로세스가 inode를 잡고 있어 살아 있었다.


4. 명령어 실습

# 1) 현재 셸의 fd 와 lsof
exec 3> ~/app.log; echo "hello" >&3      # 셸에 fd 3 을 열어 둔다
lsof -p $$ | head -12
ls -l /proc/$$/fd
exec 3>&-                                  # fd 3 닫기

# 2) (root) 삭제됐지만 열린 파일 재현 — 200MB tmpfs 에서
mkdir -p /mnt/lab && mount -t tmpfs -o size=200m tmpfs /mnt/lab
dd if=/dev/zero of=/mnt/lab/big.log bs=1M count=150 status=none; df -h /mnt/lab
tail -f /mnt/lab/big.log > /dev/null &
rm /mnt/lab/big.log; ls /mnt/lab; df -h /mnt/lab
lsof +L1 | grep -e COMMAND -e /mnt/lab
ls -l /proc/$(pgrep -x tail)/fd | grep deleted
: > /proc/$(pgrep -x tail)/fd/3; df -h /mnt/lab     # 파일 비우기
kill %1; wait; umount /mnt/lab

# 3) (Ubuntu) 서비스가 연 파일
lsof -c cron | awk 'NR==1 || $4 ~ /[0-9]/'
lsof -nP -u systemd-resolve -a -d 0-20 | head -8   # -a: 조건 AND, -d: fd 범위

실습은 운영 디스크가 아닌 tmpfs 에서 한다. 운영 서버에서 : > /proc/PID/fd/N은 해당 프로세스가 쓰던 로그를 잘라내는 작업이므로, 로그 보존 정책을 확인한 뒤 수행한다.


5. 실행 결과

실제 실행 결과 — Rocky Linux 9.8 · analyst@rocky9-lab — lsof 로 프로세스가 연 파일 보기

실제 실행 결과 — Rocky Linux 9.8 · root@rocky9-lab — 삭제됐지만 열려 있는 파일이 디스크를 잡고 있다

실제 실행 결과 — Ubuntu 24.04.5 · root@ubuntu-lab — 서비스가 연 파일과 소켓 (lsof -c, -u)

텍스트 원본(실제 출력):

[analyst@rocky9-lab ~]$ exec 3> ~/app.log; echo "hello" >&3
[analyst@rocky9-lab ~]$ lsof -p $$ | head -12
COMMAND  PID    USER   FD   TYPE DEVICE SIZE/OFF   NODE NAME
bash    1266 analyst  cwd    DIR   0,42     4096 951186 /home/analyst
bash    1266 analyst  rtd    DIR   0,42     4096 984222 /
bash    1266 analyst  txt    REG   0,42  1389024 795509 /usr/bin/bash
bash    1266 analyst  mem    REG   0,42   346132 796773 /usr/lib/locale/C.utf8/LC_CTYPE
bash    1266 analyst  mem    REG   0,42       50 796780 /usr/lib/locale/C.utf8/LC_NUMERIC
bash    1266 analyst  mem    REG   0,42     3360 796783 /usr/lib/locale/C.utf8/LC_TIME
bash    1266 analyst  mem    REG   0,42     1406 796772 /usr/lib/locale/C.utf8/LC_COLLATE
bash    1266 analyst  mem    REG   0,42      270 796778 /usr/lib/locale/C.utf8/LC_MONETARY
bash    1266 analyst  mem    REG   0,42       48 796777 /usr/lib/locale/C.utf8/LC_MESSAGES/SYS_LC_MESSAGES
bash    1266 analyst  mem    REG   0,42       34 796781 /usr/lib/locale/C.utf8/LC_PAPER
bash    1266 analyst  mem    REG   0,42       62 796779 /usr/lib/locale/C.utf8/LC_NAME
[analyst@rocky9-lab ~]$ ls -l /proc/$$/fd
total 0
lrwx------ 1 analyst analyst 64 Sep 24 11:50 0 -> /dev/pts/0
lrwx------ 1 analyst analyst 64 Sep 24 11:50 1 -> /dev/pts/0
lrwx------ 1 analyst analyst 64 Sep 24 11:50 10 -> /dev/pts/0
lrwx------ 1 analyst analyst 64 Sep 24 11:50 2 -> /dev/pts/0
lr-x------ 1 analyst analyst 64 Sep 24 11:50 255 -> /tmp/run_15_1.sh
l-wx------ 1 analyst analyst 64 Sep 24 11:50 3 -> /home/analyst/app.log
[analyst@rocky9-lab ~]$ exec 3>&-
[root@rocky9-lab ~]# mkdir -p /mnt/lab && mount -t tmpfs -o size=200m tmpfs /mnt/lab
[root@rocky9-lab ~]# dd if=/dev/zero of=/mnt/lab/big.log bs=1M count=150 status=none; df -h /mnt/lab
Filesystem      Size  Used Avail Use% Mounted on
tmpfs           200M  150M   50M  75% /mnt/lab
[root@rocky9-lab ~]# tail -f /mnt/lab/big.log > /dev/null & sleep 0.3
[root@rocky9-lab ~]# rm /mnt/lab/big.log; ls /mnt/lab; df -h /mnt/lab
Filesystem      Size  Used Avail Use% Mounted on
tmpfs           200M  150M   50M  75% /mnt/lab
[root@rocky9-lab ~]# lsof +L1 | grep -e COMMAND -e /mnt/lab
COMMAND    PID USER   FD   TYPE DEVICE  SIZE/OFF NLINK NODE NAME
tail      1332 root    3r   REG   0,80 157286400     0    2 /mnt/lab/big.log (deleted)
[root@rocky9-lab ~]# ls -l /proc/$(pgrep -x tail)/fd | grep deleted
lr-x------ 1 root root 64 Sep 24 11:50 3 -> /mnt/lab/big.log (deleted)
[root@rocky9-lab ~]# : > /proc/$(pgrep -x tail)/fd/3; df -h /mnt/lab
tail: /mnt/lab/big.log: file truncated
Filesystem      Size  Used Avail Use% Mounted on
tmpfs           200M     0  200M   0% /mnt/lab
[root@rocky9-lab ~]# kill %1; wait; umount /mnt/lab && echo unmounted
unmounted
root@ubuntu-lab:~# lsof -c cron | awk 'NR==1 || $4 ~ /[0-9]/'
COMMAND PID USER   FD   TYPE             DEVICE SIZE/OFF   NODE NAME
cron     80 root    0r   CHR                1,3      0t0     27 /dev/null
cron     80 root    1u  unix 0xffff88811d841f80      0t0   5209 type=STREAM (CONNECTED)
cron     80 root    2u  unix 0xffff88811d841f80      0t0   5209 type=STREAM (CONNECTED)
cron     80 root    3uW  REG               0,65        3    146 /run/crond.pid
cron     80 root    4u  unix 0xffff8881257b2d00      0t0   5228 type=DGRAM (CONNECTED)
root@ubuntu-lab:~# lsof -nP -u systemd-resolve -a -d 0-20 | head -8
COMMAND   PID            USER   FD      TYPE             DEVICE SIZE/OFF       NODE NAME
systemd-r  67 systemd-resolve    0r      CHR                1,3      0t0         27 /dev/null
systemd-r  67 systemd-resolve    1u     unix 0xffff88813a46db00      0t0       4436 type=STREAM (CONNECTED)
systemd-r  67 systemd-resolve    2u     unix 0xffff88813a46db00      0t0       4436 type=STREAM (CONNECTED)
systemd-r  67 systemd-resolve    3u     unix 0xffff88813a46f180      0t0       4461 type=DGRAM (CONNECTED)
systemd-r  67 systemd-resolve    4u  a_inode               0,16        0       1037 [eventpoll:5,6,7,8,9,10,11,13,14,15,16,17,18,19,20]
systemd-r  67 systemd-resolve    5u  a_inode               0,16        0       1037 [signalfd]
systemd-r  67 systemd-resolve    6u  a_inode               0,16        0       1037 [timerfd]

6. 결과 해석

관찰의미
cwd, rtd, txt, mem 행셸이 연 것은 파일만이 아니다. 작업 디렉터리, 루트, 실행 파일, 로케일 파일까지 모두 보인다
/proc/$$/fd의 3 -> /home/analyst/app.log (l-wx)exec 3>로 연 쓰기 전용 fd
0 1 2 10 -> /dev/pts/0표준 입출력과 bash가 내부적으로 복제한 터미널 fd(10)
255 -> /tmp/run_15_1.shbash가 실행 중인 스크립트 파일 을 fd 255로 열어 두고 한 줄씩 읽는다
rm 후 ls 결과 없음, df 150M 그대로이름은 사라졌지만 데이터 블록은 해제되지 않았다
lsof +L1 → tail 1332 ... NLINK 0 ... big.log (deleted)링크 수 0인데 열려 있는 파일. 용량을 잡고 있는 범인 을 바로 찾았다. SIZE 157286400 = 150MB
/proc/1332/fd/3 -> ... (deleted)같은 정보를 /proc에서 확인했다
: > /proc/.../fd/3 후 df 0%fd를 통해 파일 크기를 0으로 잘라 블록을 해제했다. tail은 "file truncated"를 알렸지만 계속 실행된다
Ubuntu cron 3uW /run/crond.pidcron이 PID 파일에 쓰기 잠금(W) 을 걸어 중복 실행을 막는다
systemd-resolve unix, a_inode [eventpoll], [signalfd], [timerfd]서비스는 소켓과 이벤트 처리용 커널 객체를 fd로 들고 있다

7. 보안 관점

주제내용
삭제 파일 복구공격자가 도구·로그를 rm해도 프로세스가 열고 있으면 cp /proc/PID/fd/N 증거로 복구할 수 있다. 프로세스를 먼저 죽이면 이 기회가 사라진다
로그 삭제 흔적rsyslog 등 로그 데몬이 연 로그 파일이 (deleted)로 보이면 누군가 로그 파일을 삭제 했다는 뜻이다. 데몬 메모리에 아직 기록이 남아 있을 수 있다
숨은 파일 활동lsof -u www-data로 웹 서비스 계정이 연 파일 중 웹 루트 밖(/tmp, /dev/shm) 파일이 있는지 확인한다
디스크 고갈 DoS큰 파일을 열어 둔 채 삭제하면 du로는 찾을 수 없는 용량 고갈을 만들 수 있다. du와 df가 크게 다르면 lsof +L1을 본다

8. 보안관제 관점

[Detection]  /var 사용률 경보, 그러나 du -sh /var/* 합계와 df 가 크게 다름
     ↓
[확인]       lsof +L1 -nP | awk '$7 > 100000000'        ← 100MB 이상 삭제된 열린 파일
     ↓
[분류]       ① 로그 로테이션 후 reload 안 한 서비스 (정상 운영 이슈)
             ② /var/log/secure 등 보안 로그가 (deleted) → 로그 삭제 의심
     ↓
[증거 ②]     cp /proc/<rsyslog PID>/fd/<N> /evidence/secure.recovered
             sha256sum /evidence/secure.recovered
     ↓
[Response]   ① 서비스 reload / ② 삭제 시각·주체 조사, 복구본 분석
명령목적
lsof +L1삭제됐지만 열린 파일 전체
find /proc/*/fd -lname '*(deleted)' 2>/dev/nulllsof 없이 같은 확인
lsof +D /tmp/tmp 파일을 쓰는 프로세스

9. 실무에서 자주 발생하는 실수

실수결과예방
큰 로그를 rm으로 삭제용량이 해제되지 않음: > file로 비우거나 logrotate의 copytruncate/reload 사용
용량 부족 시 du만 확인삭제된 열린 파일을 못 찾음df와 비교, lsof +L1
침해 서버에서 의심 프로세스부터 kill열린 삭제 파일(증거) 소실fd 복사 후 종료
lsof 이름 변환으로 느려짐대형 서버에서 수십 초 소요-n -P 사용
lsof를 일반 계정으로 실행다른 사용자 프로세스가 빠짐root로 실행

10. 실습 체크리스트

[ ] lsof -p 와 /proc/PID/fd 로 프로세스가 연 파일을 확인했다
[ ] FD 열의 cwd, txt, mem, 3w 의미를 설명할 수 있다
[ ] 열린 파일을 rm 해도 df 가 줄지 않는 것을 재현했다
[ ] lsof +L1 로 삭제됐지만 열린 파일을 찾았다
[ ] /proc/PID/fd/N 을 통해 파일을 비워 용량을 회수했다
[ ] 삭제된 파일을 /proc/PID/fd 로 복구할 수 있다는 것을 이해했다

11. 핵심 정리

  • 파일 디스크립터는 프로세스가 연 모든 대상(파일·소켓·파이프)을 가리키는 번호다.
  • rm은 이름만 제거한다. 링크 수와 열린 fd가 모두 0이어야 공간이 해제된다.
  • lsof +L1은 삭제됐지만 열린 파일을 찾는다 — 용량 장애와 로그 삭제 탐지 모두에 쓴다.
  • /proc/PID/fd/N으로 삭제된 파일을 비우거나 복구 할 수 있다.
  • 침해 대응에서는 fd 증거를 먼저 수집하고 프로세스를 종료한다.

12. 다음 편 예고

다음 글 「16. 프로세스와 네트워크 소켓 (ss -p)」 에서는 fd 중에서도 소켓 에 집중한다. 어떤 프로세스가 어떤 포트를 열고 누구와 연결되어 있는지 ss -tnp, lsof -i로 확인하고, 도구 없이 /proc/net/tcp의 inode로 프로세스를 역추적한다.


참고 자료


시리즈 이동

profile
코드에 숨겨진 위협을 읽고 AI로 보안의 미래를 설계합니다. 프론트엔드 개발 경험을 자산 삼아 더 견고하고 지능적인 보안 운영 시스템을 구축해 나가는 과정을 기록합니다

0개의 댓글