컴퓨터공학 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 데이터처럼 자주 쓰는 경로는 볼륨으로 빼는 것이 원칙이다.

확인 문제

  1. VFS 가 하는 일을 한 문장으로 설명하라.
  2. 슈퍼블록, 비트맵, 아이노드 테이블이 각각 담는 정보는?
  3. 인덱스 할당과 익스텐트 방식의 차이는? 익스텐트가 큰 파일에 유리한 이유는?
  4. 위 실험에서 희소 파일의 논리 크기와 실제 할당 크기가 크게 다른 이유는?
  5. write() 가 성공을 반환했는데 직후 전원이 꺼지면 데이터가 사라질 수 있는 이유는?

풀이

  1. 서로 다른 파일 시스템 구현들을 공통 객체와 연산 표로 추상화해, 사용자 프로그램이 같은 시스템 콜로 어느 파일 시스템이든 다룰 수 있게 한다.
  2. 슈퍼블록은 파일 시스템 전체의 크기·블록 크기·영역 위치 등, 비트맵은 아이노드와 데이터 블록 각각의 사용 여부, 아이노드 테이블은 파일별 메타데이터와 데이터 위치를 담는다.
  3. 인덱스 할당은 블록마다 번호를 하나씩 기록하고, 익스텐트는 연속 구간을 (시작, 길이) 한 항목으로 기록한다. 큰 연속 파일을 아주 적은 항목으로 표현할 수 있어 메타데이터가 작고 탐색이 빠르다.
  4. 건너뛴 구간에는 데이터 블록을 할당하지 않기 때문이다. 실제로 쓴 마지막 3바이트를 담은 블록 하나만 할당된다.
  5. write 는 보통 페이지 캐시에만 기록하고 반환하며, 디스크 반영은 나중에 일어난다. 반영 전에 전원이 꺼지면 사라진다. 확실히 하려면 fsync 를 호출해야 한다.

더 읽을거리 (References)