open("/etc/passwd") 한 줄이 OS 내부에서 무엇을 호출하는지, 왜 rm 은 거대 파일도 즉시 끝나는데 secure delete 는 분 단위 걸리는지, fsync 가 실제로 무엇을 보장하는지 — 파일시스템은 코드와 디스크 사이의 두꺼운 layer 다. 이 가이드는 그 layer 의 내부를 정리한다.
파일시스템의 핵심 개념 — inode
디렉토리 entry: "passwd" → inode #1024
inode #1024:
├── 권한 (rwx)
├── owner / group
├── size, atime, mtime, ctime
├── link count (몇 개의 디렉토리가 이 inode 가리키나)
└── 데이터 블록 pointer 들:
direct[0] → block 5000
direct[1] → block 5001
...
direct[11] → block 5011
indirect → block (그 안에 256개 block pointer)
double_indirect → ...
triple_indirect → ...파일명은 디렉토리(또 다른 inode + 데이터)에만 있고, inode 는 번호만 있다. hard link = 같은 inode 를 가리키는 두 디렉토리 entry. 그래서 hard link 해제는 inode 의 link count 만 감소 — 0 되면 실제 해제.
block — 디스크 IO 의 단위
파일시스템은 디스크를 block(보통 4 KB) 단위로 다룬다. 1 byte 파일도 4 KB 점유. 큰 파일은 여러 block 의 시퀀스.
100 byte 파일:
inode size = 100
direct[0] = block 5000 (전체 4 KB 중 100 byte 만 사용, 나머지 낭비)
10 MB 파일:
inode size = 10485760
direct[0..11] = block 5000..5011 (48 KB)
indirect → block 6000 → [pointer × 256] → 1 MB
double_indirect → ...open() 의 lifecycle
int fd = open("/etc/passwd", O_RDONLY);
내부:
1. path resolve: "/" → root inode → "etc" → etc inode → "passwd" → passwd inode
(각 디렉토리마다 entry 검색 = O(N) 또는 hashtree)
2. permission check
3. 프로세스의 file descriptor table 에 entry 추가
→ 그 entry 가 system-wide open file table 의 row 가리킴
→ 그 row 가 inode 가리킴
4. fd (작은 정수) 반환
read(fd, buf, 4096):
1. fd → open file entry → 현재 offset 확인 (예: 0)
2. inode → block pointer → 디스크 IO
3. offset 갱신 (0 → 4096)
4. buf 에 데이터 복사VFS — Virtual File System
Linux 의 모든 파일시스템 (ext4, btrfs, xfs, NFS, fuse...) 은 통일된 VFS interface 구현. 그래서 cat /proc/cpuinfo 도 cat /etc/passwd 도 같은 syscall.
/dev/sda1 (ext4) /dev/sda2 (btrfs) NFS server procfs (in-memory)
│ │ │ │
└────────────────┴────────────────┴─────────────┘
│
VFS layer
│
syscall (open/read/write/close)
│
application왜 rm 은 빠른가
100 GB 파일 rm 도 즉시 끝남. 데이터를 안 지우기 때문.
unlink("/foo/bar"):
1. /foo 디렉토리에서 "bar" entry 제거
2. inode 의 link count -- (다른 hard link 없으면 0)
3. link count 0 + open 한 process 없으면 → inode 해제 + block free list 에 추가
4. 실제 디스크 block 의 데이터는 그대로 남음 (덮어쓰기 없음)Secure delete (shred) 는 block 의 모든 byte 를 random 으로 덮어씀 — 그래서 느림.
그래서 실수로 rm 한 파일을 file recovery 도구 가 복원할 수 있음 (block 이 새 파일에 덮이기 전까지).
fsync — 진짜로 디스크에 도달했나
write(fd, buf, 4096); // page cache 에만 (RAM)
// 디스크는 아직 안 봤음
// 시스템 crash 면 데이터 손실 가능.
fsync(fd); // page cache → 실제 디스크 flush
// 이 호출이 return 해야 disk 에 보장.Database 의 commit / WAL flush 는 모두 fsync 의존. 그래서 SQLite 의 synchronous=OFF 가 빠르지만 위험.
SSD 의 write barrier · disk controller cache 의 lying about flush 문제 등 detail 이 더 있지만, OS 입장에서는 fsync return = 보장 약속.
Journaling — crash consistency
crash 시점에 multi-step write 가 중간이면?
전통 ext2: 디렉토리 entry 추가 + inode 갱신 + block alloc 중 crash
→ 디렉토리에 entry 있는데 inode link count 안 맞음 → fsck 시간
ext3/4 (journaling):
1. journal 에 "이 변경들 적용 예정" 박음 (sequential write, 빠름)
2. 실제 변경 적용
3. journal 에 commit 마크
crash 후 mount:
- journal 의 commit 된 변경 → replay
- commit 안 된 → 무시
→ fsck 시간 ↓, consistency 보장.Copy-on-Write — btrfs / ZFS / APFS
전통 (in-place):
block 5000 의 데이터 수정 → 같은 block 에 새 데이터 덮어쓰기
Copy-on-Write:
block 5000 → 새 block 6000 에 복사 + 수정
metadata 가 5000 → 6000 으로 pointer 갱신 (atomic)
장점:
- crash 가운데여도 metadata 가 old 또는 new 한쪽 — corruption 0
- snapshot 비용 0 (metadata 의 pointer 그대로 두기만)
- dedup 자연스러움
단점:
- fragmentation
- free space 계산 복잡주요 파일시스템 비교
| FS | 접근 | 강점 | 비고 |
|---|---|---|---|
| ext4 | journaling | 안정, default | Linux 대부분 default |
| xfs | journaling, B-tree dir | 대용량·고동시 | RHEL default |
| btrfs | CoW | snapshot, dedup, RAID | 여전히 일부 RAID mode unstable |
| ZFS | CoW | data integrity (checksum), 대용량 | Solaris/FreeBSD origin, Linux 도 |
| APFS | CoW | SSD 최적화, clone | macOS default |
| NTFS | journaling | ACL, ADS | Windows default |
대용량 디렉토리 함정
디렉토리 entry 가 linear list 면 100 만 파일의 lookup = O(N).
ext4: htree (hashed B-tree) → O(log N)
xfs: B+ tree → O(log N)
ext2 (legacy): linear list → O(N), 100 만 파일에서 ls 가 분 단위
→ /var/spool 같은 큰 디렉토리는 modern FS 사용 필수.흔한 함정
- Inode 고갈 — block 은 남았는데 inode 다 씀.
df -i로 확인. 작은 파일 수백만 개 만들면 잘 발생. - fsync 안 함 — power loss 시 데이터 손실. Database / 중요 데이터는 fsync 필수.
- O_DIRECT 오해 — page cache 우회. 대부분의 application 은 default 가 더 빠름. DB / 캐싱 직접 관리할 때만.
- mmap 의 SIGBUS — mmap 한 파일이 truncate 되면 그 영역 접근 시 SIGBUS. 명시적 munmap 필요.
- 대량 small file write 의 metadata overhead — 파일 1만개 만들기 = 디렉토리 entry 1만 + inode 1만 + block allocator 1만. tar 로 묶으면 훨씬 빠름.
마무리
파일시스템은 단순한 "데이터 저장" 이상이다 — concurrency 관리, crash consistency, 효율적 lookup, 안전한 권한, 모든 것을 동시에. 그 layer 를 이해하면 "왜 우리 backup 이 이렇게 느린가" / "왜 SQLite 가 갑자기 빨라지는가" 같은 질문에 답할 수 있다.