rename
rename은 POSIX 표준의 파일 관련 시스템 콜 중 하나로 파일 이름(경로)를 변경할 때 사용한다. rename을 사용하면 기존 파일 oldpath를 새 경로 new path로 변경하고, 만약 newpath가 이미 존재하는 경우 atomically replaced 된다는 중요한 특징을 가지고 있다.
#include <stdio.h>
int rename(const char *oldpath, const char *newpath);
- oldpath : 기존 경로
- newpath : 바꾸고자 하는 경로
rename이 atomic하다는 의미
man 2 rename으로 rename 시스템 콜의 man page를 확인하면 이런 특징에 대한 설명이 나와있다.
If newpath already exists, it will be atomically replaced, so that there is no point at which another process attempting to access newpath will find it missing. However, there will probably be a window in which both oldpath and newpath refer to the file being renamed.
여기서 atomic 하다는 말은 newpath가 이미 존재하고 rename(oldpath, newpath)가 호출되어 newpath 파일이 oldpath 파일로 대체될 때 어느 시점에도 중간 상태(둘 다 존재하거나 둘 다 사라진 상태)가 외부에서 관찰되지 않는다는 뜻이다. 즉, 다른 프로세스가 동시에 파일을 열거나 읽더라도 rename() 호출 전이거나 호출 후 중 하나만 보게 된다.
사례.
- 프로세스1이 A 파일을 열어 읽고 있는 도중에 프로세스2가 A 파일의 이름을 rename으로 B로 바꿔도 프로세스1은 기존 파일을 계속 읽을 수 있다. 다만, 프로세스1이 파일을 닫고 다시 A파일을 열려고 시도하면 파일이 없다고 나올 것이다.
- 프로세스1이 A 파일을 읽고 있는 도중에 프로세스2가 B 파일의 이름을 rename으로 A로 바꿔도, 프로세스1은 계속 기존 내용을 읽을 수 있다. 다만, 프로세스1이 파일을 닫고 다시 A파일을 열면 새로운 내용(이름이 B였던 파일의 내용)이 읽힐 것이다.
이게 가능한 이유는 파일 시스템 내부 구조와 커널의 동기화 매커니즘 때문이며 이에 대해 좀 더 자세히 들여다보고자 한다.

rename의 동작 방식
rename의 동작을 이해하려면 먼저 디렉터리 엔트리와 inode의 관계, 그리고 파일 디스크립터와 inode의 참조 구조를 이해해야 한다.
1. 디렉터리 엔트리와 inode의 관계

리눅스 파일 시스템은 파일의 이름과 실제 데이터를 분리해서 관리한다. 파일의 실체(identity)는 inode이고, 이름(경로)은 inode를 가르키는 단순한 포인터라고 생각하면 된다.
1.1. 디렉터리 엔트리 (Directory Entry)
위에서 /path/to/directory라고 써있는 제일 왼쪽 그림은 디렉터리 파일의 내용을 의미한다. 즉, 디렉터리 자체도 하나의 파일이고, 디렉터리 파일은 그림과 같이 "이름과 inode 번호 간의 매핑 정보" 테이블을 가지고 있다.
1.2. Indoe (Index Node)
그림 중간의 Inode 111111이라고 써있는 박스가 실제 파일을 나타내는 메타데이터 구조체인 inode이다. inode에는 파일 이름이 저장되지 않으며, 이름은 디렉터리 엔트리 쪽에만 존재한다.
inode가 포함하는 주요 정보에는 다음과 같은 것들이 있다.
| 필드 | 설명 |
| Device ID | 파일이 속한 디바이스 (파일시스템/파티션 구분용) |
| Links count | 동일 inode를 참조하는 디렉터리 엔트리 개수 (하드링크 수) |
| User ID / Group ID | 소유자 정보 |
| File Type + Permissions | 파일 종류 (일반 파일, 디렉터리 등) 및 권한 |
| File Size | 파일의 총 크기 |
| Preferred Block Size | 블록 단위 크기 |
| Number of blocks | 실제 할당된 블록 수 |
| btime / mtime / atime / ctime | 생성, 수정, 접근, 상태변경 시간 |
| Data Block Pointers | 파일 내용이 저장된 실제 데이터 블록의 논리적 주소 |
1.3. Data Block
그림 가장 오른쪽이 Data Block으로 실제 파일 내용이 담긴 곳이다. inode는 이 데이터 블록들의 위치를 가리키는 포인터 배열을 갖고 있어서 파일 읽기/쓰기 시 커널은 inode → 블록 포인터 → 데이터 블록 순으로 접근한다.
이러한 구조 덕분에 파일 이름을 바꿔도 단지 디렉터리 엔트리의 이름을 바꾸는 것일 뿐 inode는 그대로 유지된다. 또한 하나의 inode를 가리키는 여러 하드링크가 존재할 수 있으며, 파일을 삭제(unlink)하는 행동은 디렉터리 엔트리에서 해당 항목이 제거되는 것일 뿐, 실제로는 inode의 링크 카운터가 0이 되었을 때 커널이 데이터 블록과 inode를 해제한다.
2. 파일 디스크립터와 inode의 참조 구조
다음 그림은 리눅스에서 파일을 열고 접근할 때 내부적으로 어떤 자료구조를 거쳐 inode에 도달하는가를 시각화한 것이다.

2.1. File Descriptor Table (프로세스 별)
각 프로세스마다 고유한 file descriptor table이 존재하며 int fd = open("out.txt", O_RDONLY) 등을 호출하면 커널은 새로운 open file description 구조체를 만들고 현재 프로세스의 fd 테이블에 인덱스를 할당한다.
2.2. Open File Description Table (시스템 전역)
open() 시스템 콜을 통해 생성되는 open file description 객체를 추적하는 테이블이다. open file description에는 파일의 읽기/쓰기 위치, 파일 open 시의 플래그, refcount(몇 개의 fd가 이 파일을 참조하는지) 및 inode 포인터 등의 정보가 들어있으며, 여러 fd가 하나의 open file description을 공유할 수도(dup(), dup2() 함수 등을 통해) 있다.
2.3. Inodes (시스템 전역)
inode는 파일의 identity 자체로 파일시스템 레벨에서 관리되며, 여러 open file description이 같은 inode를 가리킬 수 있다.
따라서 한 프로세스가 파일을 열고 있는 동안, 다른 프로세스가 rename()으로 파일명을 바꿔도 inode 자체는 여전히 유효하며 정상 동작한다.
하지만 rename()으로 인해 해당 inode를 참조하는 디렉토리 엔트리의 개수가 0가 되었다면 해당 inode는 삭제 예약 상태가 될 것이고, open file description 닫히는 순간 refcount도 0이 되어 커널이 inode와 데이터 블록을 삭제할 것이다.
3. rename의 동작
rename(oldpath, newpath) 호출 시 다음과 같은 동작이 이루어진다.
- oldpath 이름을 가진 디렉터리 엔트리를 찾고, 해당 엔트리가 어떤 inode를 가리키는지 확인한다.
- newpath 이름을 가진 디렉터리 엔트리가 이미 존재하면, 그 엔트리를 삭제(unlink)한다.
- oldpath 엔트리를 삭제하고, 같은 inode를 newpath이름으로 엔트리를 추가한다.
위의 1~3 과정은 커널 내부에서 락(lock)으로 보호되어 중간 상태가 노출되지 않는다.
rename이 언제 유용하게 사용될 수 있을까?
rename()의 atomic 특성은 기존 파일의 내용을 직접 수정하지 않고 완성된 새 파일로 교체하는 방식을 통해 atomic update를 구현할 수 있게 한다.
Case 1. 파일의 일관성 보장
여러 프로세스가 동시에 같은 설정 파일(settings.conf)을 읽는 상황을 생각해보자. 단순히 이 파일을 fopen("settings.conf", "w")로 열고 내용을 덮어쓰면, 쓰기 도중 다른 프로세스가 절반만 작성된 깨진 파일을 읽을 수 있다.
이때 아래처럼 rename()을 사용하면 하면 동시에 접근하는 프로세스들이 항상 논리적으로 완전한 상태의 설정 파일만 보게 된다.
int fd = open("/etc/myapp/settings.conf.tmp", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0) return -1;
write(fd, new_config_data, strlen(new_config_data));
fsync(fd);
close(fd);
rename("/etc/myapp/settings.conf.tmp", "/etc/myapp/settings.conf");
Case 2. 파일의 무결성 보장
시스템 장애나 전원 차단이 발생하더라도 데이터가 손상되지 않고 보존되어야하는 성격의 데이터 파일 관리에서도 rename()은 유용하다. 예를 들어, 프로그램이 상태 정보를 /var/lib/myapp/state.json 에 저장한다고 하자. 만약 이 파일을 직접 수정하는 도중 전원이 꺼지면 파일이 깨질 수 있다.
하지만, rename() 방식을 사용하면 중간에 전원이 나가더라도 최소한 이전 버전은 보존되며, 절대로 반쯤 바뀐 상태가 존재하지 않는다.
int fd = open("/var/lib/myapp/state.json.tmp", O_WRONLY | O_CREAT | O_TRUNC, 0644);
write(fd, data, len);
fsync(fd);
close(fd);
rename("/var/lib/myapp/state.json.tmp", "/var/lib/myapp/state.json");
'프로그래밍 > 리눅스 시스템 프로그래밍' 카테고리의 다른 글
| 뮤텍스(Mutex)와 세마포어(Semaphore)의 차이 (0) | 2025.12.03 |
|---|---|
| Linux ] 파일 append는 정말 atomic 할까? (0) | 2025.11.12 |
| Linux ] dup2() 함수를 사용한 표준입출력 리다이렉션 (0) | 2025.11.03 |
| Linux ] fork()를 통한 프로세스 생성 (0) | 2025.11.01 |
| Linux에서 현재 프로세스가 모니터가 연결된 GUI 세션인지 확인하는 법 (0) | 2025.10.24 |