본문으로 건너뛰기
yutils

파일시스템은 어떻게 동작할까?

inode · block · VFS, open() 의 file descriptor lifecycle, journaling vs copy-on-write, ext4 vs btrfs vs ZFS, 왜 rm 은 빠른데 secure delete 는 느린지, fsync 가 실제로 하는 일 — 코드와 디스크 사이의 layer.

약 10분 읽기

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접근강점비고
ext4journaling안정, defaultLinux 대부분 default
xfsjournaling, B-tree dir대용량·고동시RHEL default
btrfsCoWsnapshot, dedup, RAID여전히 일부 RAID mode unstable
ZFSCoWdata integrity (checksum), 대용량Solaris/FreeBSD origin, Linux 도
APFSCoWSSD 최적화, clonemacOS default
NTFSjournalingACL, ADSWindows 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 가 갑자기 빨라지는가" 같은 질문에 답할 수 있다.

가이드 목록으로