파일 시스템 구현
연속·연결·인덱스 블록 할당과 빈 공간 관리를 비교하고 FAT·inode·NTFS가 파일 위치를 추적하는 방식을 익힙니다.
사용자가 파일을 저장하면 디스크 어딘가에 데이터가 기록됩니다.
그런데 하나의 파일이 여러 디스크 블록에 걸쳐 저장될 수 있습니다.
파일 시스템은 이 블록들을 어떻게 할당하고, 어떤 블록이 어떤 파일에 속하는지 어떻게 추적할까요?
이 질문에 대한 대답이 파일 시스템 구현의 핵심입니다.
슈퍼블록은 파일 시스템 전체 구조를, inode는 개별 파일의 메타데이터와 블록 매핑을 설명합니다. 빈 공간 비트맵과 복구 로그는 할당과 복구를 보조합니다.
디스크 블록 할당 방식
연속 할당 (Contiguous Allocation)
파일의 모든 블록을 디스크에서 연속된 위치에 저장합니다.
시작 블록과 길이만 기록하면 되므로 메타데이터가 간단합니다.
장점: 순차 접근이 최적입니다.
디스크 헤드가 한 방향으로 쭉 읽으면 됩니다.
직접 접근도 빠릅니다 — n번째 블록은 시작 + n 위치에 있습니다.
단점: 파일 크기가 커지면 연속 공간을 찾기 어려워지고, 파일 삭제 후 외부 단편화가 심해집니다.
추가 연속 공간을 확보하지 못하면 확장 시 재배치나 별도 처리 전략이 필요합니다.
CD-ROM이나 DVD처럼 파일 크기가 고정되고 읽기 전용인 환경에 적합합니다.
연결 할당 (Linked Allocation)
각 블록이 다음 블록의 주소를 포함합니다.
연결 리스트처럼 동작합니다.
장점: 외부 단편화가 없고, 파일 크기가 동적으로 늘어날 수 있습니다.
단점: 직접 접근이 매우 느립니다.
파일의 100번째 블록에 접근하려면 처음부터 99개의 블록을 따라가야 합니다.
또한 포인터가 손상되면 나머지 파일 전체를 잃습니다.
각 블록에서 포인터가 차지하는 공간(예: 4바이트)만큼 데이터 용량이 줄어듭니다.
인덱스 할당 (Indexed Allocation)
각 파일에 인덱스 블록을 할당하여, 파일의 모든 데이터 블록 주소를 한 곳에 모아 저장합니다.
장점: 직접 접근이 빠르고(인덱스 블록에서 바로 n번째 주소를 읽음) 외부 단편화도 없습니다.
단점: 작은 파일도 인덱스 블록 하나를 차지합니다(공간 낭비).
큰 파일은 인덱스 블록 하나로는 모든 데이터 블록을 가리킬 수 없어 다단계 인덱스가 필요합니다.
#include <stdio.h>
/* 4KB 블록, 4바이트 포인터 기준 최대 파일 크기 비교 */
int main() {
long long block_size = 4096;
long long ptr_size = 4;
long long ptrs_per_block = block_size / ptr_size; /* 1024 */
/* 인덱스 할당: 인덱스 블록 하나 */
long long single_index = ptrs_per_block * block_size;
printf("인덱스 1단계: %lld MB\n", single_index / (1024*1024)); /* 4 MB */
/* inode 방식: 12직접 + 단일간접 + 이중간접 + 삼중간접 */
long long direct = 12LL * block_size;
long long single = ptrs_per_block * block_size;
long long dbl = ptrs_per_block * ptrs_per_block * block_size;
long long triple = ptrs_per_block * ptrs_per_block * ptrs_per_block * block_size;
long long total = direct + single + dbl + triple;
printf("inode 방식: ~ %lld TB\n", total / (1024LL*1024*1024*1024)); /* ~4 TB */
return 0;
}빈 공간 관리
파일이 삭제되면 해당 블록을 재사용 가능 상태로 표시해야 합니다.
비트맵(Bitmap): 각 블록에 1비트를 대응시킵니다.
0이면 비어 있고 1이면 사용 중입니다.
1TiB 공간을 4KiB 블록과 블록당1비트로 표시하면 비트맵은32MiB입니다.
연속된 빈 블록을 빠르게 찾을 수 있어 현대 파일 시스템에서 주로 사용됩니다.
연결 리스트: 빈 블록들을 연결 리스트로 연결합니다.
공간 효율은 좋지만 연속 공간을 찾으려면 리스트를 순회해야 합니다.
그룹화: 첫 번째 빈 블록에 n개의 빈 블록 주소를 저장하고, 마지막 주소가 다음 그룹을 가리킵니다.
연결 리스트의 개선판입니다.
FAT 파일 시스템
FAT(File Allocation Table)은 연결 할당의 변형입니다.
각 블록의 "다음 블록" 포인터를 블록 자체가 아닌 별도의 테이블에 모아둡니다.
이 테이블이 FAT입니다.
FAT를 메모리에 캐시하면 다음 블록 포인터를 읽기 위한 디스크 I/O가 줄어 접근이 빨라집니다.
다만 파일의 중간 위치로 가려면 체인을 따라가는 구조 자체는 남습니다.
FAT 테이블은 보통 두 벌 저장합니다(하나가 손상되면 복원을 위해).
| 계열 | 구조·용도·크기 조건 |
|---|---|
| FAT12 | 12비트 FAT 엔트리; 플로피 등 작은 볼륨. 최대 용량은 섹터·클러스터 구성에 의존 |
| FAT16 | 16비트 FAT 엔트리; 고전적인 2GiB 구성 등이 사용됐으나 구현·클러스터 크기를 구분 |
| FAT32 | 32비트 엔트리 중28비트 사용; 단일 파일은 최대4GiB−1바이트. 볼륨 한도는 섹터·클러스터·도구 제한에 의존 |
| exFAT | 파일 길이는64비트 필드, FAT 엔트리는32비트. 대용량 파일·이동식 장치에 사용하며 파일 길이 필드와 실제 볼륨 한도는 별개 |
FAT32의 4GiB−1바이트 파일 크기 제한 때문에 대용량 영상 파일을 USB에 복사하지 못하는 경험이 이 때문입니다.
exFAT은 이 제한을 해결했습니다.
FAT은 구조가 단순하여 USB 메모리, SD 카드, 임베디드 시스템에서 널리 사용됩니다.
거의 모든 운영체제가 FAT을 지원하므로 장치 간 호환성이 뛰어납니다.
inode 기반 파일 시스템 (ext4)
inode(Index Node)는 Unix 계열 파일 시스템의 핵심 자료 구조입니다.
각 파일과 디렉토리에 고유한 inode가 할당되며, 파일의 메타데이터와 데이터 블록 위치를 저장합니다.
inode에는 파일 이름이 포함되지 않습니다.
파일 이름은 디렉토리 엔트리에 저장됩니다.
하나의 inode에 여러 이름(하드 링크)이 연결될 수 있는 이유입니다.
전통적 inode 블록 포인터 구조
직접 포인터와 간접 포인터의 참조 경로
4KiB 블록·4바이트 포인터 모델입니다. 이중·삼중 간접 경로는 각각 포인터 블록 2·3단계를 거칩니다. 그림의 블록은 크기 비례가 아닙니다.
전통적인 Unix 계열 inode 모델에서는 작은 파일(4KB 블록 기준 48KB 이하)이 직접 포인터만으로 충분합니다.
대용량 파일은 단일/이중/삼중 간접 포인터를 사용하지만, 포인터 블록을 추가로 읽어야 합니다.
현대 ext4는 기본적으로 전통적인 블록별 직접/간접 포인터 방식보다 익스텐트를 사용해 연속된 블록 범위를 더 적은 메타데이터로 표현합니다.
그래도 직접/간접 포인터 구조는 inode가 작은 파일과 큰 파일을 단계적으로 다루는 원리를 이해하는 데 유용합니다.
ext4의 익스텐트 (Extent)
전통적인 블록별 포인터는 큰 파일에서 많은 포인터를 관리해야 합니다.
ext4는 기본적으로 익스텐트(Extent)라는 개선된 방식을 사용합니다.
익스텐트는 파일 안의 논리 시작 블록, 장치의 물리 시작 블록, 길이 등으로 연속된 매핑 범위를 표현합니다.
100개의 연속 블록 = 블록별 포인터 100개 vs 익스텐트 1개.
큰 파일에서 메타데이터가 크게 줄어듭니다.
4KiB 블록의 extent 파일은 최대16TiB를 표현할 수 있습니다. 48비트 물리 extent 주소 범위는 약1EiB에 해당하지만 실제 볼륨 한도는 커널·도구·기능 설정을 별도로 확인합니다.
NTFS
NTFS(New Technology File System)는 Windows의 기본 파일 시스템입니다.
MFT(Master File Table)가 모든 파일의 메타데이터를 관리합니다.
각 파일은 MFT 레코드(보통 1KB)를 가집니다.
데이터 속성이 MFT 레코드의 남은 공간에 들어가는 작은 파일은 레코드 안에 저장될 수 있습니다(resident attribute). 한도는 다른 속성의 크기에 따라 달라져700바이트로 고정되지 않습니다.
별도의 데이터 블록 할당 없이 MFT만 읽으면 파일 내용까지 얻을 수 있어 매우 빠릅니다.
NTFS의 주요 기능
- ACL: 사용자/그룹별 세밀한 권한(읽기, 쓰기, 실행, 삭제, 속성 변경 등)
- EFS: 파일 단위 투명 암호화
- 압축: 파일/폴더 단위 투명 압축
- ADS: 하나의 파일에 여러 데이터 스트림을 포함(메타데이터 저장 등)
- 트랜잭션: TxF로 파일 연산의 원자성 보장(deprecated)
저널링
파일을 쓰는 중간에 전원이 꺼지면 데이터 블록은 기록되었는데 메타데이터는 갱신되지 않은 불일치 상태가 될 수 있습니다.
저널링(Journaling)은 이 문제를 해결합니다.
저널링 동작 방식
- 변경할 내용을 먼저 저널(로그) 영역에 기록합니다. (A블록의 비트맵을 1로, inode X의 크기를 1024로 바꿀 것)
- 저널 기록이 완료되면 커밋 블록을 씁니다.
- 저널에 기록한 변경을 본래 메타데이터·데이터 위치에 반영합니다(체크포인트).
- 반영 완료 후 해당 저널 공간을 재사용할 수 있게 합니다.
비정상 종료가 발생하면
- 유효한 커밋 기록이 없으면: 완성되지 않은 저널 트랜잭션을 재생하지 않습니다.
- 유효한 커밋 기록이 있으면: 필요한 변경을 재생(replay)하여 해당 구조를 복구합니다.
메타데이터 저널은 애플리케이션의 모든 데이터가 변경 전 상태로 돌아간다는 보장이 아닙니다.
저널링 모드
메타데이터 저널링 (ordered): 메타데이터만 저널에 기록합니다.
데이터는 메타데이터 커밋 전에 먼저 기록합니다.
ext4의 기본 모드입니다.
성능과 안정성의 균형입니다.
전체 저널링 (journal): 데이터와 메타데이터 모두 저널에 기록합니다.
데이터까지 저널과 본 위치에 기록하므로 쓰기 비용이 늘어날 수 있습니다. 실제 성능 비율은 워크로드와 장치에 따라 달라집니다.
쓰기 후 저널 (writeback): 메타데이터만 저널에 기록하고, 데이터 쓰기 순서를 보장하지 않습니다.
데이터 쓰기 순서 제약을 줄이지만, 비정상 종료 뒤 메타데이터와 파일 내용의 시점이 어긋날 수 있습니다.
아래 코드는 Linux의 마운트 옵션 문자열을 읽는 예시입니다. 옵션이 명시되지 않았다고 실제 설정을 확정할 수는 없으며, 실행·마운트 검증 결과가 아닙니다.
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}")일반적인 buffered write는 페이지 캐시를 더럽히고, 블록 할당과 extent·저널 갱신을 거칩니다. 지연 할당 때문에 write() 반환과 블록 배치·커밋 완료는 같은 시점이 아닙니다.
COW 파일 시스템 (Copy-on-Write)
Btrfs와 ZFS는 핵심 저장 구조에 COW(Copy-on-Write)를 사용합니다. 동기 쓰기 복구를 위한 로그도 사용할 수 있으므로 COW와 로그를 서로 배타적인 방식으로 구분하지 않습니다.
데이터를 수정할 때 원본 블록을 직접 수정하지 않고, 새 블록에 수정된 내용을 쓴 후 포인터를 변경합니다.
원본이 보존되므로 스냅샷을 효율적으로 만들 수 있고, 메타데이터 일관성 관리에도 유리합니다.
실제 안정성은 구현 방식, 체크섬, 디스크 장애 유형에 따라 달라집니다.
다음 절에서는 파일 디스크립터, 버퍼 캐시, RAID 등 파일 시스템의 실무적 측면을 살펴보겠습니다.