icon

안동민 개발노트

10장 : 파일 시스템

파일 시스템 구현


사용자가 파일을 저장하면 디스크 어딘가에 데이터가 기록됩니다. 그런데 하나의 파일이 여러 디스크 블록에 걸쳐 저장될 수 있습니다. 파일 시스템은 이 블록들을 어떻게 할당하고, 어떤 블록이 어떤 파일에 속하는지 어떻게 추적할까요? 이 질문에 대한 대답이 파일 시스템 구현의 핵심입니다.


디스크 블록 할당 방식

연속 할당 (Contiguous Allocation)

파일의 모든 블록을 디스크에서 연속된 위치에 저장합니다. 시작 블록과 길이만 기록하면 되므로 메타데이터가 간단합니다.

장점: 순차 접근이 최적입니다. 디스크 헤드가 한 방향으로 쭉 읽으면 됩니다. 직접 접근도 빠릅니다 — n번째 블록은 시작 + n 위치에 있습니다.

단점: 파일 크기가 커지면 연속 공간을 찾기 어려워지고, 파일 삭제 후 외부 단편화가 심해집니다. 파일 생성 시 최종 크기를 미리 알아야 합니다.

CD-ROM이나 DVD처럼 파일 크기가 고정되고 읽기 전용인 환경에 적합합니다.

연결 할당 (Linked Allocation)

각 블록이 다음 블록의 주소를 포함합니다. 연결 리스트처럼 동작합니다.

장점: 외부 단편화가 없고, 파일 크기가 동적으로 늘어날 수 있습니다.

단점: 직접 접근이 매우 느립니다. 파일의 100번째 블록에 접근하려면 처음부터 99개의 블록을 따라가야 합니다. 또한 포인터가 손상되면 나머지 파일 전체를 잃습니다. 각 블록에서 포인터가 차지하는 공간(4바이트)만큼 데이터 용량이 줄어듭니다.

인덱스 할당 (Indexed Allocation)

각 파일에 인덱스 블록을 할당하여, 파일의 모든 데이터 블록 주소를 한 곳에 모아 저장합니다.

장점: 직접 접근이 빠르고(인덱스 블록에서 바로 n번째 주소를 읽음) 외부 단편화도 없습니다.

단점: 작은 파일도 인덱스 블록 하나를 차지합니다(공간 낭비). 큰 파일은 인덱스 블록 하나로는 모든 데이터 블록을 가리킬 수 없어 다단계 인덱스가 필요합니다.

allocation_comparison.c
#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 테이블은 두 벌 저장합니다(하나가 손상되면 복원을 위해).

버전비트 수최대 볼륨최대 파일주 용도
FAT121232MB32MB플로피
FAT16162GB2GB초기 HDD
FAT3228(실제)~2TB4GBUSB, SD카드
exFAT64128PB128PB대용량 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)은 이 문제를 해결합니다.

저널링 동작 방식

  1. 변경할 내용을 먼저 저널(로그) 영역에 기록합니다. (A블록의 비트맵을 1로, inode X의 크기를 1024로 바꿀 것)
  2. 저널 기록이 완료되면 커밋 블록을 씁니다.
  3. 실제 데이터 영역에 변경 사항을 반영합니다.
  4. 반영 완료 후 저널 엔트리를 삭제합니다.

비정상 종료가 발생하면

  • 커밋 블록이 없으면: 저널 엔트리를 무시합니다(변경 전 상태).
  • 커밋 블록이 있으면: 저널을 재생(replay)하여 일관성을 복구합니다.

저널링 모드

메타데이터 저널링 (ordered): 메타데이터만 저널에 기록합니다. 데이터는 메타데이터 커밋 전에 먼저 기록합니다. ext4의 기본 모드입니다. 성능과 안정성의 균형입니다.

전체 저널링 (journal): 데이터와 메타데이터 모두 저널에 기록합니다. 가장 안전하지만 모든 데이터를 두 번 쓰므로 성능이 절반 이하로 떨어집니다.

쓰기 후 저널 (writeback): 메타데이터만 저널에 기록하고, 데이터 쓰기 순서를 보장하지 않습니다. 성능은 가장 좋지만 비정상 종료 시 파일 내용이 손상될 수 있습니다.

journal_mode_check.py
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)

BtrfsZFS는 저널링 대신 COW(Copy-on-Write) 방식을 사용합니다. 데이터를 수정할 때 원본 블록을 직접 수정하지 않고, 새 블록에 수정된 내용을 쓴 후 포인터를 변경합니다. 원본이 항상 보존되므로 스냅샷이 거의 무료입니다. 손상 위험도 저널링보다 낮습니다.

다음 절에서는 파일 디스크립터, 버퍼 캐시, RAID 등 파일 시스템의 실무적 측면을 살펴보겠습니다.

목차