사전 파일 시스템
개념

파일 시스템

gabury1

파일 시스템은 저장 장치에 쌓인 데이터를 이름으로 찾아 쓰게 합니다. 저장 장치는 번호 붙은 칸만 압니다. 파일 시스템이 그 사이에서 이름과 번호를 잇습니다. 파일을 만들고 늘리고 지우는 일, 빈 공간을 내주는 일도 이 층이 맡습니다.

쉽고 빠른 이해

파일 시스템은 저장 장치에 담긴 데이터를 이름 붙은 파일로 다루게 하는 소프트웨어입니다. 서버에서 /var/log/app.log 를 열 때 그 이름을 장치 위의 위치로 바꾸는 것이 파일 시스템입니다.

없으면 프로그램이 「몇 번 칸부터 몇 칸까지가 내 데이터다」를 스스로 외워야 합니다. 프로그램마다 제 방식으로 외우면 한 장치를 나눠 쓸 수 없습니다. 하나가 잘못 쓰면 남의 데이터를 덮습니다.

어떻게 도는가:

  1. 이름을 받아 그 파일의 정보가 담긴 구조를 찾습니다
  2. 그 구조에 적힌 번호로 데이터가 놓인 칸을 알아냅니다
  3. 그 칸을 읽거나 쓰고, 달라진 크기와 시각을 다시 적습니다

대가는 한 번의 저장이 여러 군데를 고치게 된다는 것입니다. 이름 목록, 파일 정보, 빈 공간 표시, 데이터가 각각 바뀌어야 합니다. 그 중간에 전원이 끊기면 반만 반영된 상태가 남습니다.

상세

창고에 번호만 붙은 칸이 줄지어 있다고 해 봅시다. 물건을 넣을 때마다 「우산은 3번과 4번 칸」이라고 장부에 적어 두면, 다음부터는 번호를 몰라도 이름으로 찾아올 수 있습니다.

비유에서 창고는 저장 장치, 장부는 파일 시스템입니다. 번호 붙은 칸 하나가 블록입니다. 이름을 대면 어느 칸인지 아는 쪽이 파일 시스템입니다.

파일 시스템은 저장 장치가 주는 번호 붙은 블록의 나열을 이름 붙은 파일과 디렉토리로 바꾸는 층입니다. 어젯밤 로그 파일 하나, 사용자가 올린 이미지 하나가 그런 파일입니다.

디스크는 「몇 번 블록을 달라」는 요청만 받습니다. 이름도 폴더도 모르고 무엇이 한 파일인지도 모릅니다. 그 위에 이름과 묶음을 얹는 일을 파일 시스템이 합니다.

그래서 파일 시스템이 맡는 일은 크게 넷입니다.

맡는 일 무엇을 하나
이름 붙이기 파일과 디렉토리에 이름을 주고 그 이름으로 찾게 합니다
위치 기억 한 파일이 어느 블록들에 흩어져 있는지 기록합니다
빈 공간 관리 어느 블록이 비었는지 알고 새 파일에 내줍니다
딸린 정보 보관 크기, 만든 시각, 소유자, 권한을 함께 저장합니다

표의 넷은 따로 도는 기능이 아닙니다. 파일 하나를 만들 때 네 가지가 한꺼번에 일어납니다.

이름이 블록 번호로 바뀌는 과정

파일 하나는 저장 장치에서 이어진 한 덩어리가 아닙니다. 흩어진 블록 여럿으로 이루어집니다. 그래서 어느 블록들이 한 파일인지를 따로 적어 두어야 합니다.

이 목록을 담은 구조를 유닉스 계열에서는 아이노드라고 부릅니다. 아이노드에는 파일 크기, 만든 시각, 소유자, 권한, 그리고 블록 번호 목록이 들어 있습니다. 파일 이름은 들어 있지 않습니다.

이름은 디렉토리가 갖습니다. 디렉토리는 「이름 → 아이노드 번호」 짝을 담은 파일입니다.

이름과 내용을 갈라 두면 한 파일에 이름을 둘 이상 붙일 수 있습니다. 같은 아이노드 번호를 가리키는 이름을 다른 디렉토리에 하나 더 만들면 됩니다. 이렇게 만든 둘째 이름이 하드 링크입니다.

flowchart TD
    A["이름 · app.log"] --> B["디렉토리 · 이름과 아이노드 번호의 짝"]
    A2["둘째 이름 · backup.log"] --> B2["다른 디렉토리"]
    B --> C["아이노드 12 · 크기 · 권한 · 블록 번호 목록"]
    B2 --> C
    C --> D["블록 87"]
    C --> E["블록 91"]
    C --> F["블록 92"]

그림에서 이름은 맨 위에만 있습니다. 아이노드는 자기 이름을 모르고 자기 블록 번호만 압니다. 이름 둘이 같은 아이노드로 모일 수 있는 까닭도 이것입니다.

디렉토리가 이루는 나무 모양

디렉토리도 파일입니다. 담는 내용이 사용자 데이터가 아니라 이름 목록일 뿐입니다.

디렉토리 안에 디렉토리를 넣을 수 있어서 전체가 나무 모양이 됩니다. 맨 위 하나를 루트라고 합니다. 경로에서는 / 로 적습니다. /var/log/app.log 처럼 루트에서 내려오는 이름을 이어 붙인 것이 경로입니다.

찾아가는 순서가 곧 이름입니다. 루트에서 시작해 한 칸씩 내려갑니다. 한 칸마다 다음 이름의 아이노드 번호를 얻습니다. 마지막에 닿는 것이 그 파일의 아이노드입니다.

장치가 여럿이어도 나무는 하나입니다. 다른 장치의 파일 시스템을 이 나무의 한 디렉토리에 붙여 씁니다. 이 붙이기가 마운트입니다.

flowchart TD
    subgraph D1["첫째 장치"]
        R["/"] --> V["var"]
        R --> E["etc"]
        R --> M["mnt"]
        V --> L["log"]
        L --> A["app.log"]
    end
    subgraph D2["둘째 장치"]
        S["둘째 장치의 뿌리"] --> P["data"]
    end
    M --> S

빈 공간을 내주고 거둬들이는 일

새 파일을 쓰려면 비어 있는 블록을 찾아야 합니다. 블록 하나에 비트 하나를 맞춰 두고 0 과 1 로 쓰임을 적는 표를 비트맵이라고 합니다. 파일 시스템은 어느 블록이 쓰였는지를 여기에 적습니다.

파일을 지우면 그 블록들을 다시 비었다고 표시합니다. 블록 안에 있던 값은 그대로 남아 있습니다. 표시만 바뀌기 때문입니다. 지운 파일을 되살리는 도구가 있는 까닭이 이것입니다.

block-beta
columns 5
  H0["블록"] a["87 · 값 있음"] b["88 · 값 있음"] c["89 · 빈 칸"] d["90 · 빈 칸"]
  H1["지우기 전 표시"] e["1"] f["1"] g["0"] h["0"]
  H2["지운 뒤 표시"] i["0"] j["0"] k["0"] l["0"]

그림에서 위 줄은 지우기 전과 뒤가 같습니다. 바뀐 것은 아래 두 줄의 표시뿐입니다.

블록은 크기가 정해져 있습니다. 한 글자짜리 파일도 블록 하나를 차지합니다. 작은 파일이 많으면 쓰지 못하는 공간이 쌓입니다. 이 낭비를 내부 단편화라고 합니다.

반대쪽 문제도 있습니다. 쓰고 지우기를 오래 되풀이하면 한 파일의 블록들이 멀리 떨어집니다. 떨어진 블록을 모아 읽는 일은 이어진 블록을 읽는 일보다 시간이 더 듭니다.

프로그램이 파일 시스템을 만나는 통로

프로그램은 블록 번호를 다루지 않습니다. 운영체제에 이름을 건네고 나머지를 맡깁니다. 이때 부르는 것이 시스템 콜입니다.

파일을 열고 읽고 닫는 가장 짧은 형태는 이렇습니다.

C
int fd = open("app.log", O_RDONLY);  // 3
read(fd, buf, 100);                  // 100
close(fd);                           // 0

여는 호출이 돌려준 3 이 파일 디스크립터입니다. 파일을 가리키는 번호표입니다. 뒤의 읽기와 닫기는 이름 대신 이 번호로 합니다. 읽기가 돌려준 100 은 읽어 온 바이트 수입니다. 닫기의 0 은 잘 닫혔다는 뜻입니다.

한 기계 안에 서로 다른 파일 시스템이 여럿 있을 수 있습니다. 디스크 위의 파일 시스템, 네트워크 건너편의 파일 시스템, 메모리에만 있는 파일 시스템이 한 나무 안에 섞여 있습니다.

종류가 달라도 프로그램은 위와 같은 호출 하나만 씁니다. 운영체제의 핵심부인 커널이 그 사이에 공통 층을 두고 호출을 알맞은 파일 시스템으로 넘기기 때문입니다. 이 공통 층을 가상 파일 시스템이라고 합니다.

flowchart TD
    P["프로그램"] --> S["시스템 콜 · open · read · close"]
    S --> V["가상 파일 시스템 · 공통 층"]
    subgraph FS["파일 시스템 여럿"]
        A["디스크 위"]
        B["네트워크 건너편"]
        C["메모리 위"]
    end
    V --> A
    V --> B
    V --> C
    A --> DEV["블록 장치"]

전원이 갑자기 끊길 때 남는 것

파일 하나를 새로 만드는 일은 여러 군데를 고칩니다. 디렉토리에 이름을 넣고, 아이노드를 하나 쓰고, 빈 공간 표시를 바꾸고, 데이터를 씁니다.

이 넷 사이에서 전원이 끊기면 앞의 것만 반영된 상태가 남습니다. 이름은 있는데 아이노드가 없거나, 쓰였다고 표시된 블록을 아무 파일도 안 가리키는 식입니다. 이런 상태를 파일 시스템이 깨졌다고 말합니다.

전원이 끊겼을 때 잃을 수 있는 것은 하나 더 있습니다. 운영체제는 받은 쓰기를 메모리에 모아 두었다가 나중에 장치로 내려보냅니다. 그래서 프로그램이 쓰기를 끝냈어도 그 내용은 아직 장치에 없을 수 있습니다. 반드시 남겨야 하는 데이터는 fsync 를 불러 내려갔는지 확인합니다.

깨짐을 막는 방법 하나가 저널링입니다. 바꿀 내용을 먼저 한곳에 기록해 둡니다. 그 뒤에 실제 구조를 고칩니다. 다 고치면 그 기록을 지웁니다.

다시 켜졌을 때 기록이 남아 있으면 중간에 끊긴 것입니다. 남은 기록을 보고 마무리합니다.

flowchart TD
    S1["디렉토리에 이름을 넣는다"] --> S2["아이노드를 쓴다"]
    S2 --> S3["빈 공간 표시를 바꾼다"]
    S3 --> S4["데이터를 쓴다"]
    S1 -. 여기서 전원이 끊기면 .-> X["이름만 남고 아이노드가 없다"]
    X --> R["다시 켤 때 저널에 남은 기록을 읽는다"]
    R --> S2

덮어쓰지 않는 방법도 있습니다. 쓰기 시 복사는 바뀐 내용을 빈 블록에 새로 씁니다. 다 쓴 뒤에 가리키는 번호만 새 블록으로 옮깁니다. 번호를 옮기는 일은 나뉘지 않아서 중간 상태가 안 남습니다.

flowchart TD
    subgraph B1["바꾸기 전"]
        I1["아이노드"] --> O1["옛 블록 · 옛 내용"]
    end
    subgraph B2["다 쓴 뒤"]
        I2["아이노드"] --> N2["새 블록 · 새 내용"]
        O2["옛 블록 · 옛 내용 그대로"]
    end
    B1 --> B2

파일 하나를 통째로 갈아 끼울 때 쓰는 방법도 같은 생각입니다. 새 내용을 임시 파일에 다 쓴 뒤 원래 이름으로 바꿉니다. 같은 파일 시스템 안에서 이름을 바꾸는 일은 나뉘지 않습니다.

이름 바꾸기가 나뉘지 않는 것은 리눅스와 맥이 POSIX(Portable Operating System Interface, 이식 가능 운영체제 인터페이스)를 따르기 때문입니다. POSIX 는 여러 운영체제가 파일을 같은 방식으로 다루자고 맞춘 약속입니다. 그래서 읽는 쪽은 옛 내용 전체나 새 내용 전체 중 하나만 봅니다. 원자성은 이렇게 중간이 안 보이는 성질을 말합니다.

같은 파일을 여럿이 만질 때

한 파일을 여러 프로그램이 동시에 여는 것은 막히지 않습니다. 둘이 같은 파일에 쓰면 내용이 서로 섞입니다. 차례를 정하려면 프로그램끼리 락을 걸어야 합니다.

파일 시스템이 주는 락은 대개 강제가 아닙니다. 락을 확인하고 쓰는 프로그램끼리만 차례가 지켜집니다. 확인하지 않는 프로그램은 락이 걸린 파일에도 그냥 씁니다.

sequenceDiagram
    participant A as 프로그램 A
    participant B as 프로그램 B
    participant F as 파일
    A->>F: 앞부분을 쓴다
    B->>F: 같은 곳에 쓴다
    A->>F: 뒷부분을 쓴다
    Note over A,F: 차례를 안 정하면 두 쓰기가 섞인다
    A->>F: 락을 걸고 쓴다
    B->>F: 락을 확인하지 않고 그냥 쓴다
    Note over B,F: 확인 안 하는 쪽은 락이 못 막는다

이 이름이 가리키는 범위

이 낱말은 두 가지를 함께 가리킵니다. 하나는 지금까지 설명한 방식 자체입니다. 다른 하나는 그 방식을 구현한 각각의 물건입니다. 리눅스가 흔히 쓰는 ext4 나 Btrfs 가 뒤쪽 뜻입니다.

저장 장치가 없는 것도 파일 시스템이라고 부릅니다. 리눅스의 /proc 아래 파일들은 어디에도 저장되어 있지 않고 읽을 때마다 커널이 만듭니다. 프로그램이 보기에 파일처럼 열리고 읽히면 파일 시스템이라고 부릅니다.

네트워크 건너편에 있는 것도 마찬가지입니다. 다른 기계의 디렉토리를 자기 나무에 붙여 놓으면 프로그램은 자기 장치의 파일처럼 다룹니다. 이런 방식을 네트워크 파일 시스템이라고 합니다.

파일 시스템을 쓸지 말지도 같은 기준으로 가릅니다. 이름과 디렉토리로 나눠 두는 것이 자연스러운 데이터라면 파일 시스템이 맞습니다. 한 덩어리를 통째로 넣고 꺼내기만 하거나, 파일 시스템이 주는 이름과 락이 어느 쪽도 필요 없다면 다른 저장 방식을 고릅니다.

관련 항목

파일 시스템이 관리하는 저장 장치

디스크 · 블록 · 파티션 · 볼륨 · 블록 장치 · SSD

파일 시스템을 이루는 내부 구조

아이노드 · 디렉토리 · 슈퍼블록 · 비트맵 · 익스텐트 · 메타데이터 · 저널

파일에 이름을 붙이고 찾아가는 수단

경로 · 하드 링크 · 심볼릭 링크 · 마운트 · 루트 디렉토리 · 네임스페이스

파일을 다루려고 부르는 시스템 콜

open · read · write · rename · fsync · 시스템 콜 · 파일 디스크립터

파일 시스템을 굴리는 운영체제 구성 요소

커널 · 가상 파일 시스템 · 페이지 캐시 · 디바이스 드라이버 · 사용자 공간 · 운영체제

갑작스러운 중단에서 데이터를 지키는 방식

저널링 · 쓰기 시 복사 · 원자성 · 영속성 · 체크섬 · 스냅샷

같은 파일을 동시에 만질 때 쓰는 수단

락 · 파일 잠금 · 경쟁 상태 · 동시성 · 프로세스

이 방식을 구현한 파일 시스템 제품

ext4 · XFS · Btrfs · ZFS · NTFS · APFS · FAT · tmpfs

네트워크 너머로 파일을 내주는 방식

NFS · SMB · 네트워크 파일 시스템 · 분산 파일 시스템 · 오브젝트 스토리지

파일 시스템에서 자주 나는 장애

디스크 부족 · 아이노드 고갈 · 단편화 · 데이터 손상 · 디스크 오류

파일 시스템이 지키는 접근 규칙과 표준

POSIX · 권한 · 접근 제어 목록 · umask · setuid

파일 시스템 위에 얹혀 도는 저장 방식

데이터베이스 · 선행 기록 로그 · 백업과 복구 · 캐싱 · 로그

다른 이름: file system · filesystem · 파일시스템