파일 시스템 구현
사용자가 파일을 저장하면 디스크 어딘가에 데이터가 기록됩니다. 그런데 하나의 파일이 여러 디스크 블록에 걸쳐 저장될 수 있습니다. 파일 시스템은 이 블록들을 어떻게 할당하고, 어떤 블록이 어떤 파일에 속하는지 어떻게 추적할까요? 이 질문에 대한 대답이 파일 시스템 구현의 핵심입니다.
디스크 블록 할당 방식
연속 할당 (Contiguous Allocation)
파일의 모든 블록을 디스크에서 연속된 위치에 저장합니다. 시작 블록과 길이만 기록하면 되므로 메타데이터가 간단합니다.
장점: 순차 접근이 최적입니다. 디스크 헤드가 한 방향으로 쭉 읽으면 됩니다. 직접 접근도 빠릅니다 — n번째 블록은 시작 + n 위치에 있습니다.
단점: 파일 크기가 커지면 연속 공간을 찾기 어려워지고, 파일 삭제 후 외부 단편화가 심해집니다. 파일 생성 시 최종 크기를 미리 알아야 합니다.
CD-ROM이나 DVD처럼 파일 크기가 고정되고 읽기 전용인 환경에 적합합니다.
연결 할당 (Linked Allocation)
각 블록이 다음 블록의 주소를 포함합니다. 연결 리스트처럼 동작합니다.
장점: 외부 단편화가 없고, 파일 크기가 동적으로 늘어날 수 있습니다.
단점: 직접 접근이 매우 느립니다. 파일의 100번째 블록에 접근하려면 처음부터 99개의 블록을 따라가야 합니다. 또한 포인터가 손상되면 나머지 파일 전체를 잃습니다. 각 블록에서 포인터가 차지하는 공간(4바이트)만큼 데이터 용량이 줄어듭니다.
인덱스 할당 (Indexed Allocation)
각 파일에 인덱스 블록을 할당하여, 파일의 모든 데이터 블록 주소를 한 곳에 모아 저장합니다.
장점: 직접 접근이 빠르고(인덱스 블록에서 바로 n번째 주소를 읽음) 외부 단편화도 없습니다.
단점: 작은 파일도 인덱스 블록 하나를 차지합니다(공간 낭비). 큰 파일은 인덱스 블록 하나로는 모든 데이터 블록을 가리킬 수 없어 다단계 인덱스가 필요합니다.
#include <stdio.h>
/* 4KB 블록, 4바이트 포인터 기준 최대 파일 크기 비교 */
int main() {
long block_size = 4096;
long ptr_size = 4;
long ptrs_per_block = block_size / ptr_size; /* 1024 */
/* 인덱스 할당: 인덱스 블록 하나 */
long single_index = ptrs_per_block * block_size;
printf("인덱스 1단계: %ld MB\n", single_index / (1024*1024)); /* 4 MB */
/* inode 방식: 12직접 + 단일간접 + 이중간접 + 삼중간접 */
long direct = 12L * block_size;
long single = ptrs_per_block * block_size;
long dbl = ptrs_per_block * ptrs_per_block * block_size;
long triple = ptrs_per_block * ptrs_per_block * ptrs_per_block * block_size;
long total = direct + single + dbl + triple;
printf("inode 방식: ~ %ld TB\n", total / (1024L*1024*1024*1024)); /* ~4 TB */
return 0;
}빈 공간 관리
파일이 삭제되면 해당 블록을 재사용 가능 상태로 표시해야 합니다.
비트맵(Bitmap): 각 블록에 1비트를 대응시킵니다. 0이면 비어 있고 1이면 사용 중입니다. 1TB 디스크(4KB 블록)의 비트맵 크기는 약 32MB입니다. 연속된 빈 블록을 빠르게 찾을 수 있어 현대 파일 시스템에서 주로 사용됩니다.
연결 리스트: 빈 블록들을 연결 리스트로 연결합니다. 공간 효율은 좋지만 연속 공간을 찾으려면 리스트를 순회해야 합니다.
그룹화: 첫 번째 빈 블록에 n개의 빈 블록 주소를 저장하고, 마지막 주소가 다음 그룹을 가리킵니다. 연결 리스트의 개선판입니다.
FAT 파일 시스템
FAT(File Allocation Table)은 연결 할당의 변형입니다. 각 블록의 "다음 블록" 포인터를 블록 자체가 아닌 별도의 테이블에 모아둡니다. 이 테이블이 FAT입니다.
FAT를 메모리에 캐시하면 직접 접근이 빨라집니다. FAT 테이블은 두 벌 저장합니다(하나가 손상되면 복원을 위해).
| 버전 | 비트 수 | 최대 볼륨 | 최대 파일 | 주 용도 |
|---|---|---|---|---|
| FAT12 | 12 | 32MB | 32MB | 플로피 |
| FAT16 | 16 | 2GB | 2GB | 초기 HDD |
| FAT32 | 28(실제) | ~2TB | 4GB | USB, SD카드 |
| exFAT | 64 | 128PB | 128PB | 대용량 SD, 호환용 |
FAT32의 4GB 파일 크기 제한 때문에 대용량 영상 파일을 USB에 복사하지 못하는 경험이 이 때문입니다. exFAT은 이 제한을 해결했습니다.
FAT은 구조가 단순하여 USB 메모리, SD 카드, 임베디드 시스템에서 널리 사용됩니다. 거의 모든 운영체제가 FAT을 지원하므로 장치 간 호환성이 뛰어납니다.
inode 기반 파일 시스템 (ext4)
inode(Index Node)는 Unix 계열 파일 시스템의 핵심 자료 구조입니다. 각 파일과 디렉토리에 고유한 inode가 할당되며, 파일의 메타데이터와 데이터 블록 위치를 저장합니다.
inode에는 파일 이름이 포함되지 않습니다. 파일 이름은 디렉토리 엔트리에 저장됩니다. 하나의 inode에 여러 이름(하드 링크)이 연결될 수 있는 이유입니다.
inode 블록 포인터 구조
inode
├── 직접 포인터 0~11 (12개 × 4KB = 48KB)
├── 단일 간접 포인터 → [1024개 포인터] → 데이터 블록 (4MB)
├── 이중 간접 포인터 → [1024 포인터] → [각각 1024 포인터] → 데이터 (4GB)
└── 삼중 간접 포인터 → 포인터³ → 데이터 (4TB)작은 파일(48KB 이하)은 직접 포인터만으로 충분합니다. 대부분의 파일이 이 범위에 들어가므로 빠르게 접근 가능합니다. 대용량 파일은 간접 포인터를 사용하지만, 추가 디스크 읽기가 필요합니다.
ext4의 익스텐트 (Extent)
전통적인 블록별 포인터는 큰 파일에서 많은 포인터를 관리해야 합니다. ext4는 익스텐트(Extent)라는 개선된 방식을 사용합니다. 익스텐트는 시작 블록, 길이의 쌍으로, 연속된 블록들을 하나의 엔트리로 표현합니다.
100개의 연속 블록 = 블록별 포인터 100개 vs 익스텐트 1개. 큰 파일에서 메타데이터가 크게 줄어듭니다.
ext4는 최대 1EB(엑사바이트) 볼륨과 16TB 파일을 지원합니다.
NTFS
NTFS(New Technology File System)는 Windows의 기본 파일 시스템입니다. MFT(Master File Table)가 모든 파일의 메타데이터를 관리합니다. 각 파일은 MFT 레코드(보통 1KB)를 가집니다.
작은 파일(약 700바이트 이하)은 MFT 레코드 안에 직접 데이터를 저장합니다(resident attribute). 별도의 데이터 블록 할당 없이 MFT만 읽으면 파일 내용까지 얻을 수 있어 매우 빠릅니다.
NTFS의 주요 기능
- ACL: 사용자/그룹별 세밀한 권한(읽기, 쓰기, 실행, 삭제, 속성 변경 등)
- EFS: 파일 단위 투명 암호화
- 압축: 파일/폴더 단위 투명 압축
- ADS: 하나의 파일에 여러 데이터 스트림을 포함(메타데이터 저장 등)
- 트랜잭션: TxF로 파일 연산의 원자성 보장(deprecated)
저널링
파일을 쓰는 중간에 전원이 꺼지면 데이터 블록은 기록되었는데 메타데이터는 갱신되지 않은 불일치 상태가 될 수 있습니다. 저널링(Journaling)은 이 문제를 해결합니다.
저널링 동작 방식
- 변경할 내용을 먼저 저널(로그) 영역에 기록합니다. (A블록의 비트맵을 1로, inode X의 크기를 1024로 바꿀 것)
- 저널 기록이 완료되면 커밋 블록을 씁니다.
- 실제 데이터 영역에 변경 사항을 반영합니다.
- 반영 완료 후 저널 엔트리를 삭제합니다.
비정상 종료가 발생하면
- 커밋 블록이 없으면: 저널 엔트리를 무시합니다(변경 전 상태).
- 커밋 블록이 있으면: 저널을 재생(replay)하여 일관성을 복구합니다.
저널링 모드
메타데이터 저널링 (ordered): 메타데이터만 저널에 기록합니다. 데이터는 메타데이터 커밋 전에 먼저 기록합니다. ext4의 기본 모드입니다. 성능과 안정성의 균형입니다.
전체 저널링 (journal): 데이터와 메타데이터 모두 저널에 기록합니다. 가장 안전하지만 모든 데이터를 두 번 쓰므로 성능이 절반 이하로 떨어집니다.
쓰기 후 저널 (writeback): 메타데이터만 저널에 기록하고, 데이터 쓰기 순서를 보장하지 않습니다. 성능은 가장 좋지만 비정상 종료 시 파일 내용이 손상될 수 있습니다.
import subprocess
# 현재 파일 시스템의 저널링 모드 확인
result = subprocess.run(
["cat", "/proc/mounts"],
capture_output=True, text=True
)
for line in result.stdout.splitlines():
parts = line.split()
if len(parts) >= 4 and "ext4" in parts[2]:
device, mount_point, fs_type, options = parts[0], parts[1], parts[2], parts[3]
if "data=journal" in options:
mode = "전체 저널링"
elif "data=writeback" in options:
mode = "writeback"
else:
mode = "ordered (기본)"
print(f"{device} → {mount_point}: {mode}")COW 파일 시스템 (Copy-on-Write)
Btrfs와 ZFS는 저널링 대신 COW(Copy-on-Write) 방식을 사용합니다. 데이터를 수정할 때 원본 블록을 직접 수정하지 않고, 새 블록에 수정된 내용을 쓴 후 포인터를 변경합니다. 원본이 항상 보존되므로 스냅샷이 거의 무료입니다. 손상 위험도 저널링보다 낮습니다.
다음 절에서는 파일 디스크립터, 버퍼 캐시, RAID 등 파일 시스템의 실무적 측면을 살펴보겠습니다.