QUESTION
readdir로 받은 이름과 metadata는 한 시점의 일관된 snapshot인가?
getdents64는 directory file position부터 여러 linux_dirent64 record를 buffer에 채운다. glibc readdir는 이 buffer를 사용자에게 한 entry씩 보여준다. 호출마다 filesystem directory iterator가 진행하지만 다른 process의 생성·삭제·rename을 잠그지 않는다.
d_type은 편의 정보이며 DT_UNKNOWN일 수 있다. 보안 또는 정확성이 필요한 코드는 이름 문자열을 다시 절대 경로로 붙이기보다 directory fd와 fstatat/openat을 사용한다.
STRUCTURE
구조 그림
각 record의 d_reclen을 따라 다음 항목을 찾는다. d_name 길이만 더하면 alignment padding을 건너뛰지 못한다.
CALL PATH
호출 흐름
directory stream은 이름 목록의 immutable snapshot이 아니다. 각 entry 처리 시점에 필요한 객체를 dirfd-relative syscall로 다시 확정한다.
함수 이름을 외우기 위한 그림이 아니다. 반환값, 파일 디스크립터, 메모리 매핑, 대기 큐 가운데 무엇이 다음 단계로 전달되는지 확인한다.
SOURCE COORDINATES
Linux 6.18.37 LTS 소스 위치
glibc 함수에서 멈추지 않고 syscall 구현과 커널 객체가 만나는 파일까지 내려간다. 링크는 동일한 태그의 원본 파일을 가리킨다.
| 파일 | 함수·구조체 | 여기서 볼 것 |
|---|---|---|
| fs/readdir.c | iterate_dir(), getdents64(), filldir64() | directory callback과 userspace record 작성 |
| fs/namei.c | vfs_statx(), filename_lookup() | entry 이름을 실제 path와 metadata로 다시 확인 |
| include/uapi/linux/dirent.h | linux_dirent64 | record length, type, offset ABI |
COMPLETE PROGRAM
실행 예제 원본
아래 코드는 설명을 위해 중간 줄을 생략한 의사 코드가 아니다. 파일로 빌드해 실행할 수 있는 최소 예제다.
cc -std=c17 -Wall -Wextra -O2 list_dir.c -o list_dir01#define _DEFAULT_SOURCE
02#include <dirent.h>
03#include <errno.h>
04#include <stdio.h>
05#include <sys/stat.h>
06
07int main(int argc, char **argv)
08{
09 const char *path = argc > 1 ? argv[1] : ".";
10 DIR *dir = opendir(path);
11 if (dir == NULL)
12 return 1;
13 int dfd = dirfd(dir);
14
15 errno = 0;
16 struct dirent *entry;
17 while ((entry = readdir(dir)) != NULL) {
18 struct stat st;
19 if (fstatat(dfd, entry->d_name, &st, AT_SYMLINK_NOFOLLOW) < 0) {
20 if (errno == ENOENT)
21 continue;
22 perror("fstatat");
23 break;
24 }
25 printf("%-24s inode=%llu mode=%o\n", entry->d_name,
26 (unsigned long long)st.st_ino, st.st_mode & 07777);
27 }
28 if (entry == NULL && errno != 0)
29 perror("readdir");
30 return closedir(dir) != 0;
31}
CODE NOTES
코드 조각별 설명
DIR *dir = opendirDIR은 fd뿐 아니라 getdents buffer와 현재 위치를 가진 libc 객체다. 같은 stream을 여러 thread가 동시에 순회하는 방식은 피한다.
int dfd = dirfdpathname join 없이 같은 directory object를 기준으로 fstatat을 호출한다. directory가 rename돼도 기준 객체는 유지된다.
errno = 0readdir의 NULL은 EOF와 오류를 모두 뜻하므로 loop 시작 전에 errno를 0으로 만들고 종료 뒤 구분한다.
AT_SYMLINK_NOFOLLOWentry가 symlink이면 target이 아니라 symlink inode metadata를 읽는다. d_type만 믿지 않는다.
errno == ENOENTreaddir와 fstatat 사이 다른 process가 entry를 삭제한 정상 경쟁을 허용하고 다음 항목으로 진행한다.
DETAILS
세부 동작
record 길이는 고정이 아니다
linux_dirent64의 d_reclen으로 다음 record를 찾는다. name 길이와 alignment 때문에 sizeof(struct dirent)만큼 더하는 raw parser는 잘못된다.
glibc readdir를 쓰면 ABI parsing을 직접 할 필요가 없지만 반환 pointer는 다음 readdir 호출에서 덮어쓸 수 있다.
directory 변경 중 결과는 중복·누락될 수 있다
filesystem은 iterator cookie와 hash/tree position을 사용한다. 순회 도중 rename이나 insertion이 있으면 이미 본 항목이 다시 보이거나 새 항목을 못 볼 수 있으며 portable snapshot 보장은 없다.
정확한 inventory가 필요하면 변경을 막는 상위 protocol, filesystem snapshot, 반복 검증 중 하나가 필요하다.
recursive walk는 fd 수와 symlink policy를 관리한다
각 하위 directory를 openat한 뒤 stack/queue에 넣으면 cwd를 바꾸지 않고 순회할 수 있다. 너무 많은 directory fd를 동시에 유지하면 RLIMIT_NOFILE에 닿으므로 depth-first close 정책을 정한다.
mount crossing, symlink follow, bind mount cycle 정책을 dev/inode pair와 openat2 resolve flag로 명시한다.
OBJECTS
객체와 수명
| 대상 | 언제 생기고 없어지는가 | 확인할 값 |
|---|---|---|
DIR stream | opendir에서 생성되고 closedir에서 내부 fd와 buffer가 해제된다 | fd, buffer, position |
dirent record | readdir 반환부터 다음 stream 호출 전까지만 유효할 수 있다 | d_name, d_ino, d_type |
directory fd | dirfd가 빌려 주는 번호이며 closedir가 소유한다 | 별도 close 금지, dup 필요 여부 |
FAILURE PATH
실패 조건과 오해하기 쉬운 부분
| 겉으로 보이는 현상 | 실제 원인 후보 | 확인 방법 |
|---|---|---|
| 항목이 가끔 사라짐 | 순회 중 concurrent rename/unlink | 변경 source와 snapshot 요구 확인 |
| 파일 종류가 UNKNOWN | filesystem이 d_type 미제공 | fstatat으로 확인 |
| readdir 오류를 EOF로 처리 | errno 구분 누락 | loop 전 errno=0 패턴 확인 |
LAB
직접 확인
- 큰 directory를 순회하는 동안 다른 process가 파일을 생성·rename해 결과 중복과 누락 가능성을 기록한다.
- strace에서 한 번의 getdents64가 여러 readdir 출력으로 분해되는지 확인한다.
- recursive walker를 openat/fstatat만으로 작성하고 symlink와 mount crossing policy를 option으로 분리한다.
./list_dir .strace -e trace=openat,getdents64,newfstatat,close ./list_dir .PRIMARY REFERENCES