Study · 운영체제 · 파일 지우기 · 파일과 디렉터리

rm 은 파일을 지우지 않는다 — 이름 하나를 뗄 뿐이다

장면

rm 으로 지웠는데 다른 프로그램이 그 파일을 읽고 있었다면? 그 프로그램은 어떻게 될까?

$ ln a.txt b.txt      # 이름을 하나 더 건다
$ ls -l a.txt         # 링크 수가 2 다
$ rm a.txt            # 이름 하나를 뗀다
$ cat b.txt           # 알맹이는 그대로다

$ echo hi > z.txt          # 이번에는 열어 둔 채 지워 본다
$ tail -f z.txt &          # z.txt 를 열어 둔 채 기다리는 프로그램을 띄운다
[1] 812                    # 그 프로그램의 번호가 812 다
hi
$ rm z.txt                 # 이름을 뗐는데
$ readlink /proc/812/fd/3  # 그 프로그램이 들고 있는 것은 그대로다
/tmp/f/z.txt (deleted)

아래 재생기의 ▶를 누르면 한 스텝씩 진행된다. 금색 테두리가 지금 이야기하는 곳이고, 캡션이 무슨 일이 일어났는지 말해 준다. 굵게 밑줄 친 말은 누르면 뜻이 나온다. 빨간 딱지는 그때 벌어지는 일의 이름이고, 캡션 바로 위 회색 글자는 지금 스텝이 속한 묶음의 이름이다.

카드 아래 회색 줄은 그 일이 실제로 적혀 있는 소스의 파일 이름 : 줄 번호와 함수 이름이다. 글 안에 (:669)처럼 줄 번호만 적힌 것은 바로 앞에 적은 파일의 그 줄이라는 뜻이다. 캡션 한가운데 박힌 긴 영어 이름과 괄호 속 파일 · 줄 번호도 같다 — 건너뛰고 한글만 읽어도 뜻이 통하게 써 두었다.

장면 · rm 은 파일을 지우지 않는다 — 이름 하나를 뗄 뿐이다 0 / 11

▶ 를 누르면 rm a.txt 가 커널 안에서 실제로 무엇을 하는지 따라간다. 커널이 알맹이를 버리려면 두 가지가 동시에 0 이어야 한다 — 이름의 개수와, 그것을 열어 둔 쪽의 개수다. rm 은 앞의 것만 하나 줄인다. 이 장면은 우분투 기본 파일 시스템인 ext4 를 기준으로 한다 — XFS 나 Btrfs 면 VFS 아래 절반이 다르다.

키보드 · ← → 한 스텝 · Space 재생/멈춤 · Home 처음

rm 은 알맹이를 지우지 않는다. 이름이 다 떨어지고 열어 둔 곳도 없어야 비로소 알맹이가 없어진다.

어떻게 확인했나

소스 어디를 보고 적었는지와 무엇을 돌려서 쟀는지를 남긴 자리다. 처음 읽을 때는 건너뛰어도 된다.

  • 예전 커널 글에 흔히 나오는 do_unlinkat 은 이 버전에 없다 — filename_unlinkat(fs/namei.c:5677)으로 이름이 바뀌었다. 지울 권한은 지우려는 파일이 아니라 그 파일이 든 디렉터리에 묻는다 — vfs_unlink(:5623)가 부르는 may_delete_dentry(:3678)의 inode_permission(idmap, dir, MAY_WRITE | MAY_EXEC)(:3697). 소스에서 확인했다.
  • vfs_unlink 자신은 링크 수를 줄이지 않고 dir->i_op->unlink(fs/namei.c:5649)로 넘긴다. ext4 쪽 __ext4_unlink(fs/ext4/namei.c:3228)가 이름 한 줄을 ext4_delete_entry(:3272)로 빼고, 그 다음에야 drop_nlink(inode)(:3287)로 i_nlink 를 줄인다. 0 이 되면 ext4_orphan_add(:3289)가 미아 목록에 올린다.
  • 세는 값이 둘이다 — i_nlink(디스크에 적힌 이름 수, include/linux/fs.h:791)와 i_count(커널 안에서 붙잡은 수, :840). filename_unlinkat 은 시작 전에 ihold(inode)(fs/namei.c:5715)로 붙잡았다가 디렉터리 잠금을 푼 뒤 iput(inode)(:5723, 주석이 truncate the inode here)로 놓는다. iput(fs/inode.c:2027)이 줄이는 것은 i_count(:2055)뿐이다.
  • 알맹이를 버릴지는 iput_final(fs/inode.c:1971)의 inode_generic_drop(:1983 → 함수는 include/linux/fs.h:2974)이 !inode->i_nlink 로 정한다. 열어 둔 프로세스가 있으면 i_count 가 0 이 아니라 iput 이 여기까지 오지 않는다. 블록과 inode 번호가 실제로 풀리는 곳은 그 뒤 evict(fs/inode.c:799) → ext4_evict_inode(fs/ext4/inode.c:169)의 ext4_truncate(:276)와 ext4_free_inode(:319)다.
  • lsof 가 보여 주는 (deleted) 는 d_path(fs/d_path.c:265)가 d_unlinked(path->dentry)(:288)일 때 붙이는 글자다. 그 표시는 vfs_unlink 가 부른 d_delete_notify(fs/namei.c:5664) → d_delete(fs/dcache.c:2687)에서 생긴다.
  • z.txt 를 연 채 지우니 /proc/<pid>/fd/3 이 /tmp/f/z.txt (deleted) 로 남았고 lsof 도 같은 줄을 보여 줬다. ls 로는 없는 파일인데 그 프로세스는 계속 쓸 수 있다. 하드 링크 쪽도 함께 쟀다 — 이름 둘 중 하나를 지우니 링크 수가 2 에서 1 로 줄고 내용은 그대로였다.
‹ 이전 목차로 ›