← System Programming DUJINLABS.COM

File Descriptor / I/O · Linux userspace / kernel ABI

inode, link, unlink, atomic rename

pathname과 inode 수명을 분리하고, 열린 파일의 unlink와 임시 파일 rename을 이용한 원자적 교체·내구성 절차를 정리합니다.

Series
14 / 37
Build
cc -std=c17 -Wall -Wextra -O2 atomic_replace.c -o atomic_replace
Run
./atomic_replace config.txt 'new value'
Kernel
Linux 6.18.37 LTS

파일을 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를 별도로 설계한다.

구조 그림

그림 1. rename 전후 directory entry와 열린 fd의 대상

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가 사라질 때까지 유지된다.

호출 흐름

그림 2. 사용자 코드에서 관찰 가능한 결과까지
open temp 같은 directory
write loop 새 내용 기록
fsync file data/metadata 밀기
renameat 이름 원자 교체
fsync dir directory 변경 보존

원자성은 관찰자가 old 또는 new 이름만 본다는 뜻이고, 내구성은 전원 장애 뒤 어느 상태가 남는지를 뜻한다. 두 성질을 같은 단어로 묶지 않는다.

그림 3. 커널 내부에서 지나가는 주요 지점
filename_lookup old/new parent
vfs_rename lock과 permission
fs rename directory entry 변경
d_move dcache 갱신
fsync writeback/barrier

함수 이름을 외우기 위한 그림이 아니다. 반환값, 파일 디스크립터, 메모리 매핑, 대기 큐 가운데 무엇이 다음 단계로 전달되는지 확인한다.

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 요청

실행 예제 원본

아래 코드는 설명을 위해 중간 줄을 생략한 의사 코드가 아니다. 파일로 빌드해 실행할 수 있는 최소 예제다.

빌드cc -std=c17 -Wall -Wextra -O2 atomic_replace.c -o atomic_replace
01#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}

코드 조각별 설명

실제 코드 11행open(".", O_RDONLY

target과 같은 directory fd를 잡아 임시 파일 생성, rename, directory fsync의 기준을 하나로 고정한다.

실제 코드 16행O_CREAT | O_EXCL

기존 임시 이름을 덮어쓰지 않고 새 inode를 만들었다는 것을 보장한다. 충돌 시 고유 이름 전략이 필요하다.

실제 코드 20행fsync(fd)

rename 전에 새 파일 내용과 inode metadata를 storage 계층으로 보낸다. write 성공만으로 crash persistence를 보장하지 않는다.

실제 코드 24행renameat(dirfd

같은 mount의 같은 directory 안에서 이름을 원자적으로 교체한다. 다른 filesystem 사이 rename은 EXDEV다.

실제 코드 26행fsync(dirfd)

새 이름과 이전 이름 제거라는 directory 변경을 crash 뒤에도 보존하기 위해 directory를 동기화한다.

세부 동작

01

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를 계속 차지하는 이유다.

02

rename 독자는 중간 파일을 보지 않는다

완성된 temp inode를 target 이름으로 바꾸면 다른 process의 pathname lookup은 old inode 또는 new inode 중 하나를 얻는다. temp 파일을 target에 직접 truncate/write할 때처럼 절반만 기록된 내용을 보지 않는다.

이미 old target을 열어 둔 fd는 rename 뒤에도 old inode를 계속 가리킨다.

03

오류 경로에서도 임시 이름을 정리한다

write, fsync, close, rename 중 하나가 실패하면 temp inode를 unlink해야 한다. signal-safe cleanup과 재시작 정책을 정하고, 예측 가능한 공용 temp 이름은 사용하지 않는다.

O_TMPFILE과 linkat(AT_EMPTY_PATH)을 지원하는 filesystem에서는 이름 없는 inode로 준비한 뒤 publish할 수 있다.

객체와 수명

대상언제 생기고 없어지는가확인할 값
directory entrylink/rename에서 생기거나 바뀌고 unlink에서 제거된다name, parent inode, target inode
inodelink 또는 open/mmap 참조가 있는 동안 유지된다i_nlink, size, timestamps
struct fileopen한 fd 수명 동안 pathname 변경과 독립적으로 inode를 참조한다f_path, f_pos, f_mode

실패 조건과 오해하기 쉬운 부분

겉으로 보이는 현상실제 원인 후보확인 방법
disk space가 안 줄어듦삭제된 파일을 process가 계속 openlsof +L1, /proc/PID/fd
전원 장애 뒤 새 이름 없음directory fsync 누락filesystem/documented durability와 fault test
rename이 EXDEVsource와 target이 다른 mountstat st_dev와 findmnt 확인

직접 확인

  1. 큰 파일을 open한 process를 둔 뒤 unlink하고 lsof +L1과 df를 확인한다.
  2. reader가 target을 반복 open하는 동안 writer가 rename 교체해 중간 길이가 관찰되지 않는지 검사한다.
  3. 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'

원문