QUESTION
파일을 unlink했는데 열린 fd로 계속 읽을 수 있는 이유는 무엇인가?
pathname은 directory entry가 inode 번호를 가리키는 이름이고, 열린 fd는 dentry/path를 거쳐 inode와 struct file을 참조한다. unlink는 directory에서 이름 하나를 제거할 뿐, 열린 file reference와 link count가 모두 없어질 때까지 inode data는 남는다.
rename은 같은 filesystem 안에서 namespace 관점의 원자적 이름 교체를 제공한다. 하지만 crash 뒤 데이터와 새 directory entry가 반드시 남는다는 뜻은 아니므로 file fsync와 directory fsync를 별도로 설계한다.
STRUCTURE
구조 그림
rename 이전
- config → inode A
- .tmp → inode B
- reader fd → inode A
- B: 새 내용 + fsync
renameat
- directory lock
- 이름 교체 원자성
- config entry 갱신
- directory fsync 별도
rename 이후
- config → inode B
- .tmp 이름 없음
- 기존 reader fd → inode A
- 새 open → inode B
pathname은 new inode로 바뀌지만 이미 열린 old fd는 old inode를 계속 참조한다. old inode는 open reference가 사라질 때까지 유지된다.
CALL PATH
호출 흐름
원자성은 관찰자가 old 또는 new 이름만 본다는 뜻이고, 내구성은 전원 장애 뒤 어느 상태가 남는지를 뜻한다. 두 성질을 같은 단어로 묶지 않는다.
함수 이름을 외우기 위한 그림이 아니다. 반환값, 파일 디스크립터, 메모리 매핑, 대기 큐 가운데 무엇이 다음 단계로 전달되는지 확인한다.
SOURCE COORDINATES
Linux 6.18.37 LTS 소스 위치
glibc 함수에서 멈추지 않고 syscall 구현과 커널 객체가 만나는 파일까지 내려간다. 링크는 동일한 태그의 원본 파일을 가리킨다.
| 파일 | 함수·구조체 | 여기서 볼 것 |
|---|---|---|
| fs/namei.c | do_unlinkat(), do_renameat2(), vfs_rename() | directory entry 제거와 이름 교체 |
| fs/open.c | do_sys_openat2() | pathname을 struct file 참조로 바꾸는 과정 |
| fs/sync.c | do_fsync(), vfs_fsync_range() | file과 filesystem의 durability 요청 |
COMPLETE PROGRAM
실행 예제 원본
아래 코드는 설명을 위해 중간 줄을 생략한 의사 코드가 아니다. 파일로 빌드해 실행할 수 있는 최소 예제다.
cc -std=c17 -Wall -Wextra -O2 atomic_replace.c -o atomic_replace01#define _GNU_SOURCE
02#include <fcntl.h>
03#include <stdio.h>
04#include <string.h>
05#include <unistd.h>
06
07int main(int argc, char **argv)
08{
09 if (argc != 3)
10 return 2;
11 int dirfd = open(".", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
12 if (dirfd < 0)
13 return 1;
14
15 const char *tmp = ".replace.tmp";
16 int fd = openat(dirfd, tmp, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0644);
17 if (fd < 0)
18 return 1;
19 size_t length = strlen(argv[2]);
20 if (write(fd, argv[2], length) != (ssize_t)length || fsync(fd) < 0)
21 return 1;
22 if (close(fd) < 0)
23 return 1;
24 if (renameat(dirfd, tmp, dirfd, argv[1]) < 0)
25 return 1;
26 if (fsync(dirfd) < 0)
27 return 1;
28 close(dirfd);
29 return 0;
30}
CODE NOTES
코드 조각별 설명
open(".", O_RDONLYtarget과 같은 directory fd를 잡아 임시 파일 생성, rename, directory fsync의 기준을 하나로 고정한다.
O_CREAT | O_EXCL기존 임시 이름을 덮어쓰지 않고 새 inode를 만들었다는 것을 보장한다. 충돌 시 고유 이름 전략이 필요하다.
fsync(fd)rename 전에 새 파일 내용과 inode metadata를 storage 계층으로 보낸다. write 성공만으로 crash persistence를 보장하지 않는다.
renameat(dirfd같은 mount의 같은 directory 안에서 이름을 원자적으로 교체한다. 다른 filesystem 사이 rename은 EXDEV다.
fsync(dirfd)새 이름과 이전 이름 제거라는 directory 변경을 crash 뒤에도 보존하기 위해 directory를 동기화한다.
DETAILS
세부 동작
link count와 open count는 서로 다른 참조다
hard link를 추가하면 inode link count가 늘지만 open file description 수는 변하지 않는다. unlink 뒤 link count가 0이어도 열린 fd나 mmap이 있으면 data block을 즉시 회수할 수 없다.
/proc/PID/fd에서 '(deleted)'로 보이는 파일이 disk space를 계속 차지하는 이유다.
rename 독자는 중간 파일을 보지 않는다
완성된 temp inode를 target 이름으로 바꾸면 다른 process의 pathname lookup은 old inode 또는 new inode 중 하나를 얻는다. temp 파일을 target에 직접 truncate/write할 때처럼 절반만 기록된 내용을 보지 않는다.
이미 old target을 열어 둔 fd는 rename 뒤에도 old inode를 계속 가리킨다.
오류 경로에서도 임시 이름을 정리한다
write, fsync, close, rename 중 하나가 실패하면 temp inode를 unlink해야 한다. signal-safe cleanup과 재시작 정책을 정하고, 예측 가능한 공용 temp 이름은 사용하지 않는다.
O_TMPFILE과 linkat(AT_EMPTY_PATH)을 지원하는 filesystem에서는 이름 없는 inode로 준비한 뒤 publish할 수 있다.
OBJECTS
객체와 수명
| 대상 | 언제 생기고 없어지는가 | 확인할 값 |
|---|---|---|
directory entry | link/rename에서 생기거나 바뀌고 unlink에서 제거된다 | name, parent inode, target inode |
inode | link 또는 open/mmap 참조가 있는 동안 유지된다 | i_nlink, size, timestamps |
struct file | open한 fd 수명 동안 pathname 변경과 독립적으로 inode를 참조한다 | f_path, f_pos, f_mode |
FAILURE PATH
실패 조건과 오해하기 쉬운 부분
| 겉으로 보이는 현상 | 실제 원인 후보 | 확인 방법 |
|---|---|---|
| disk space가 안 줄어듦 | 삭제된 파일을 process가 계속 open | lsof +L1, /proc/PID/fd |
| 전원 장애 뒤 새 이름 없음 | directory fsync 누락 | filesystem/documented durability와 fault test |
| rename이 EXDEV | source와 target이 다른 mount | stat st_dev와 findmnt 확인 |
LAB
직접 확인
- 큰 파일을 open한 process를 둔 뒤 unlink하고 lsof +L1과 df를 확인한다.
- reader가 target을 반복 open하는 동안 writer가 rename 교체해 중간 길이가 관찰되지 않는지 검사한다.
- write 뒤 file fsync만 한 경우와 directory fsync까지 한 경우를 가상 머신 강제 전원 차단으로 비교한다.
./atomic_replace config.txt 'new value'strace -e trace=openat,write,fsync,renameat,unlink,close ./atomic_replace config.txt 'new value'PRIMARY REFERENCES