[CS300 #114] 파일 시스템 구조 — 블록 더미 위에 세운 이름과 계층
컴퓨터공학 300 주제 시리즈의 114번째 글이다. 전체 지도는 여기.
한 줄 요약
파일 시스템은 번호로만 읽고 쓰는 블록 장치 위에 슈퍼블록, 비트맵, 아이노드 테이블, 데이터 블록이라는 자료구조를 깔아 “이름 붙은 바이트 열”과 디렉터리 계층을 만들어 내는 소프트웨어이며, 리눅스에서는 VFS 가 여러 파일 시스템을 같은 인터페이스로 묶는다.
왜 필요한가
디스크나 SSD 가 운영체제에게 보여 주는 것은 “블록 0번부터 N번까지”뿐이다. 거기에 다음을 얹는 것이 파일 시스템의 일이다.
- 이름: 블록 번호 대신
/var/log/syslog같은 경로로 찾는다. - 가변 크기: 파일이 자라고 줄어도 알아서 블록을 더 주고 회수한다.
- 메타데이터: 소유자, 권한, 시각, 크기를 기록한다.
- 공간 관리: 어느 블록이 비었는지 안다.
- 일관성: 전원이 나가도 구조가 망가지지 않게 한다(117번 글).
“디스크가 꽉 찼다”는 경보가 블록이 아니라 아이노드 고갈 때문일 수 있고, 지운 로그 파일이 공간을 계속 차지할 수 있다. 파일 시스템 구조를 알면 이런 현상이 바로 설명된다.
핵심 개념
계층 구조
애플리케이션: open("/data/a.txt"), read(), write()
|
------+---------------- 시스템 콜 ----------------
v
VFS (가상 파일 시스템): 공통 객체 - superblock, inode, dentry, file
| | | |
ext4 xfs tmpfs nfs / overlayfs ...
| |
페이지 캐시 (파일 내용을 메모리에 캐시)
|
블록 계층 (I/O 스케줄링, 요청 병합)
|
장치 드라이버 -> 디스크 / SSD
VFS 는 모든 파일 시스템이 구현해야 하는 연산 표(함수 포인터 묶음)를 정의한다. 그래서 cat 은 ext4 파일이든, 메모리에만 있는 tmpfs 파일이든, 네트워크 너머 NFS 파일이든 같은 read() 로 읽는다. /proc, /sys 처럼 디스크가 아예 없는 파일 시스템도 같은 방식으로 붙는다.
디스크 위의 배치
단순화한 유닉스형 파일 시스템(OSTEP 의 vsfs, ext 계열의 기본 형태)이다.
| 슈퍼블록 | 아이노드 비트맵 | 데이터 비트맵 | 아이노드 테이블 | 데이터 블록들 ...... |
블록 0 블록 1 블록 2 블록 3~7 블록 8~
| 영역 | 내용 |
|---|---|
| 슈퍼블록 | 파일 시스템 전체 정보: 블록 크기, 전체·빈 블록 수, 아이노드 수, 각 영역 위치, 마운트 상태 |
| 아이노드 비트맵 | 아이노드 번호마다 1비트: 사용 중인가 |
| 데이터 비트맵 | 데이터 블록마다 1비트: 사용 중인가 |
| 아이노드 테이블 | 파일마다 하나씩, 메타데이터와 데이터 블록 위치 |
| 데이터 블록 | 파일 내용, 디렉터리 내용 |
ext4 는 이 구조를 디스크 전체에 한 벌만 두지 않고 블록 그룹 여러 개로 나눠 각 그룹에 비트맵과 아이노드 테이블을 둔다. 커널 문서의 설명대로 관련 데이터를 가까이 모아 탐색 시간을 줄이려는 것이다. 슈퍼블록은 손상에 대비해 여러 그룹에 사본을 둔다.
데이터 블록을 어떻게 찾나: 할당 방식
| 방식 | 구조 | 장점 | 단점 | 예 |
|---|---|---|---|---|
| 연속 할당 | 시작 블록 + 길이 | 순차 읽기 최고 | 외부 단편화, 크기 변경 어려움 | CD-ROM(ISO 9660) |
| 연결 할당 | 블록마다 다음 블록 번호 | 단편화 없음 | 임의 접근이 느림 | FAT(링크를 표로 모음) |
| 인덱스 할당 | 아이노드에 블록 번호 목록 | 임의 접근 빠름 | 큰 파일은 간접 블록 필요 | ext2/ext3 |
| 익스텐트 | (시작 블록, 길이) 쌍의 목록 | 큰 연속 파일을 짧게 표현 | 구조가 복잡 | ext4, XFS, btrfs |
ext2/ext3 의 아이노드는 직접 블록 포인터 12개와 단일·이중·삼중 간접 포인터를 가진다. ext4 는 기본적으로 익스텐트를 쓴다. 연속된 블록 수천 개를 “어디서부터 몇 개” 한 항목으로 표현하므로 큰 파일의 메타데이터가 작아진다.
블록 크기와 내부 단편화
파일 시스템은 블록 단위로만 공간을 준다. 블록이 4KB 면 1바이트 파일도 4KB 를 차지한다. 블록이 크면 큰 파일에 유리하고 작은 파일이 많으면 낭비가 크다.
페이지 캐시
리눅스는 파일 내용을 메모리의 페이지 캐시에 캐시한다. read 는 캐시에 있으면 디스크를 건드리지 않고, write 는 보통 캐시에만 쓰고(dirty) 나중에 커널이 디스크로 내보낸다(writeback). 그래서 write 가 성공했다고 디스크에 기록된 것은 아니며, 확실히 하려면 fsync 가 필요하다.
직접 해 보기
os.statvfs 로 슈퍼블록 정보를 읽고, 논리 크기와 실제 할당 크기가 다른 세 경우를 만들어 본다. ext4 에서의 결과다.
import os, tempfile
d = tempfile.mkdtemp()
st = os.statvfs(d)
print(f"블록 크기 {st.f_bsize} B, 전체 블록 {st.f_blocks}, 빈 블록 {st.f_bfree}")
print(f"아이노드 전체 {st.f_files}, 빈 아이노드 {st.f_ffree}, 이름 최대 길이 {st.f_namemax}")
def show(path):
s = os.stat(path)
# st_blocks 는 512바이트 단위로 센다 (POSIX 관례)
print(f"{os.path.basename(path):10s} 논리 크기 {s.st_size:>10,} B 실제 할당 {s.st_blocks * 512:>10,} B")
p = os.path.join(d, "tiny"); open(p, "wb").write(b"x") # 1바이트
show(p)
p = os.path.join(d, "exact"); open(p, "wb").write(b"x" * 8192) # 정확히 8KB
show(p)
p = os.path.join(d, "sparse") # 구멍 난 파일
with open(p, "wb") as f:
f.seek(100 * 1024 * 1024) # 100MB 지점으로 건너뛴 뒤
f.write(b"end") # 끝에만 3바이트 쓴다
show(p)
블록 크기 4096 B, 전체 블록 103063044, 빈 블록 52628442
아이노드 전체 26214400, 빈 아이노드 23488645, 이름 최대 길이 255
tiny 논리 크기 1 B 실제 할당 4,096 B
exact 논리 크기 8,192 B 실제 할당 8,192 B
sparse 논리 크기 104,857,603 B 실제 할당 4,096 B
1바이트 파일이 블록 하나(4KB)를 차지한다(내부 단편화). 반대로 100MB 짜리 희소 파일(sparse file) 은 실제로 4KB 만 쓴다. 건너뛴 구간에는 블록이 할당되지 않고, 읽으면 0 이 돌아온다. 아이노드 수가 파일 시스템을 만들 때 정해진 고정값이라는 점도 보인다. 파일이 아주 많으면 블록이 남아도 아이노드가 먼저 바닥날 수 있다.
현업에서는
df와df -i: 디스크가 남는데No space left on device가 나면df -i로 아이노드를 본다. 작은 파일 수백만 개(세션 파일, 캐시 조각)가 흔한 원인이다.du와df가 다를 때: 삭제했지만 아직 어떤 프로세스가 열고 있는 파일은 디렉터리에서 사라져du에는 안 보이지만 블록은 그대로다(115번 글).lsof +L1로 찾고 해당 프로세스를 재시작하거나 로그를 다시 열게 한다.- 희소 파일과 복사: VM 디스크 이미지나 DB 파일은 희소 파일인 경우가 많다. 일반 복사 도구가 구멍을 0 으로 채워 쓰면 용량이 폭증한다.
cp --sparse=always,rsync --sparse같은 옵션을 확인한다. - 컨테이너의 overlayfs: 컨테이너 루트 파일 시스템은 읽기 전용 이미지 층 위에 쓰기 가능한 층을 겹친 overlayfs 다. 큰 파일의 일부만 바꿔도 파일 전체가 위층으로 복사(copy-up)되므로, DB 데이터처럼 자주 쓰는 경로는 볼륨으로 빼는 것이 원칙이다.
확인 문제
- VFS 가 하는 일을 한 문장으로 설명하라.
- 슈퍼블록, 비트맵, 아이노드 테이블이 각각 담는 정보는?
- 인덱스 할당과 익스텐트 방식의 차이는? 익스텐트가 큰 파일에 유리한 이유는?
- 위 실험에서 희소 파일의 논리 크기와 실제 할당 크기가 크게 다른 이유는?
write()가 성공을 반환했는데 직후 전원이 꺼지면 데이터가 사라질 수 있는 이유는?
풀이
- 서로 다른 파일 시스템 구현들을 공통 객체와 연산 표로 추상화해, 사용자 프로그램이 같은 시스템 콜로 어느 파일 시스템이든 다룰 수 있게 한다.
- 슈퍼블록은 파일 시스템 전체의 크기·블록 크기·영역 위치 등, 비트맵은 아이노드와 데이터 블록 각각의 사용 여부, 아이노드 테이블은 파일별 메타데이터와 데이터 위치를 담는다.
- 인덱스 할당은 블록마다 번호를 하나씩 기록하고, 익스텐트는 연속 구간을 (시작, 길이) 한 항목으로 기록한다. 큰 연속 파일을 아주 적은 항목으로 표현할 수 있어 메타데이터가 작고 탐색이 빠르다.
- 건너뛴 구간에는 데이터 블록을 할당하지 않기 때문이다. 실제로 쓴 마지막 3바이트를 담은 블록 하나만 할당된다.
write는 보통 페이지 캐시에만 기록하고 반환하며, 디스크 반영은 나중에 일어난다. 반영 전에 전원이 꺼지면 사라진다. 확실히 하려면fsync를 호출해야 한다.
더 읽을거리 (References)
- OSTEP, File System Implementation (PDF)
- Linux kernel documentation, Overview of the Linux Virtual File System
- Linux kernel documentation, ext4 Data Structures and Algorithms — High Level Design
- Python 문서, os.statvfs, os.stat