파일 시스템
파일 시스템은 저장 장치에 담긴 데이터를 이름으로 찾고 고치게 합니다. 저장 장치가 아는 것은 번호 붙은 칸뿐입니다. 파일 시스템은 그 칸들 위에 파일과 디렉토리를 세워 이름과 칸 번호를 잇습니다. 그래서 프로그램은 칸 번호를 몰라도 이름만으로 데이터를 다룹니다.
쉽고 빠른 이해
파일 시스템은 파일 이름을 저장 장치 위의 위치로 바꿉니다.
서버에서 /var/log/app.log 를 열면 파일 시스템이 그 이름을 따라가 데이터가 놓인 칸을 찾습니다.
없으면 프로그램마다 「몇 번 칸부터 몇 칸이 내 데이터」를 스스로 외워야 합니다. 그러면 한 장치를 여럿이 나눠 쓸 때 남의 데이터를 덮어써도 막을 쪽이 없습니다.
- 디렉토리에서 이름을 찾아 그 파일에 붙은 번호를 얻습니다
- 그 번호로 파일 정보를 펴서 데이터가 놓인 칸 번호를 읽습니다
- 그 칸을 읽거나 씁니다
대가는 저장 한 번이 여러 군데를 고친다는 것입니다. 그 사이에 전원이 끊기면 일부만 남을 수 있습니다. 쓰기가 끝났다고 돌아와도 데이터가 아직 장치에 없을 수도 있습니다.
디렉토리 묶음, 중간에 끊기지 않는 이름 바꾸기, 파일 일부만 고쳐 쓰기 가운데 하나라도 필요하면 파일 시스템을 씁니다. 데이터를 통째로 넣고 꺼내기만 하면 그 셋 없이도 됩니다.
상세
도서관에는 책이 꽂힌 서가와 책을 찾아 주는 목록 카드가 함께 있습니다. 서가에는 칸마다 번호만 붙어 있습니다. 책을 찾는 사람은 칸 번호를 외우지 않습니다. 목록 카드에서 책 제목을 찾으면 카드가 「3번 서가 4번 칸」이라고 알려 줍니다.
파일 시스템은 이 도서관의 책과 목록 카드를 함께 맡는 쪽입니다. 서가의 칸은 저장 장치의 칸입니다. 책 제목은 파일 이름입니다.
저장 장치는 같은 크기의 칸을 번호 순으로 늘어놓은 것으로 보입니다. 이 칸 하나가 블록입니다. 장치가 받는 요청은 기본적으로 둘입니다. 「몇 번 블록을 읽어라」와 「몇 번 블록에 써라」입니다. 장치는 이름을 모릅니다. 어느 블록들이 한 파일인지도 모릅니다.
파일 시스템은 파일들과 그 파일에 딸린 속성 일부를 모아 둔 것입니다. 도서관의 책에 해당하는 몫입니다. 파일마다 번호도 매깁니다. 그 번호에 사람이 읽을 이름을 붙이는 이름 공간을 줍니다. 목록 카드에 해당하는 몫이 이것입니다. 서버의 로그 파일 하나, 사용자가 올린 이미지 하나가 그렇게 이름을 받은 파일입니다. 저장 장치 없이 메모리에만 사는 파일 시스템도 있습니다. 아래 예시 절에서 봅니다.
파일 안이 어떻게 생겼는지는 파일 시스템이 묻지 않습니다. 파일은 바이트의 나열입니다. 그 바이트를 어떻게 읽을지는 그 파일을 쓰는 프로그램이 정합니다.
이름들은 디렉토리라는 묶음에 담깁니다. 디렉토리는 파일 이름 목록을 담은 특별한 파일입니다. 아래 소절에서 자세히 봅니다.
파일 시스템이 맡는 일은 넷으로 나눠 볼 수 있습니다.
| 맡는 일 | 무엇을 하나 |
|---|---|
| 이름 붙이기 | 파일과 디렉토리에 이름을 주고 그 이름으로 찾게 합니다 |
| 위치 기억 | 한 파일이 어느 블록들에 놓였는지 기록합니다 |
| 빈 공간 관리 | 어느 블록이 비었는지 알고 새 데이터에 내줍니다 |
| 딸린 정보 보관 | 크기, 권한 같은 정보를 파일마다 함께 둡니다 |
넷은 따로 도는 기능이 아닙니다. 파일 하나를 새로 만들면 네 가지가 한꺼번에 바뀝니다.
이름이 블록 번호가 되는 길
파일 하나의 블록은 장치 위에 흩어져 놓일 수 있습니다. 그래서 어느 블록들이 한 파일인지를 따로 기록해야 합니다.
많은 파일 시스템이 이 기록을 아이노드(inode)라는 구조에 담습니다. 아이노드는 파일 하나의 메타데이터를 담습니다. 메타데이터는 데이터를 설명하는 데이터입니다. 파일 길이, 권한, 그 파일을 이루는 블록들의 위치가 여기에 들어갑니다.
아이노드마다 번호가 하나씩 붙습니다. 이 번호가 파일의 낮은 수준 이름입니다. 한 파일 시스템 안에서는 파일마다 번호가 다릅니다. 다른 파일 시스템에는 같은 번호가 또 있을 수 있습니다.
아이노드에는 파일 이름이 없습니다. 이름은 디렉토리가 갖습니다. 디렉토리는 디렉토리 항목들을 담은 파일입니다. 디렉토리 항목 하나는 이름 하나를 파일 하나에 잇습니다. 한 디렉토리 안에서는 같은 이름의 항목이 둘일 수 없습니다.
이름과 파일을 갈라 두었으므로 한 파일에 이름을 여럿 붙일 수 있습니다. 같은 파일을 가리키는 디렉토리 항목을 하나 더 만들면 됩니다. 이 디렉토리 항목을 하드 링크라고도 부릅니다.
flowchart TD
A["이름 · app.log"] --> B["디렉토리 항목 · 이름과 아이노드 번호"]
A2["둘째 이름 · backup.log"] --> B2["다른 디렉토리의 항목"]
B --> C["아이노드 12 · 길이 · 권한 · 블록 위치"]
B2 --> C
C --> D["블록 87"]
C --> E["블록 91"]
C --> F["블록 92"]
그림에서 이름은 맨 위에만 있습니다. 아이노드는 자기 이름을 모릅니다. 자기 블록만 압니다. 이름 둘이 아이노드 하나로 모이는 것이 하드 링크입니다.
빈 블록은 비트맵으로 찾는 경우가 많습니다. 비트맵은 블록마다 비트 하나를 맞춰 둔 표입니다. 비트가 0 이면 그 블록이 비었다는 뜻입니다. 1 이면 쓰이는 중입니다. 빈 아이노드를 찾는 비트맵도 따로 두는 경우가 많습니다. 그 표에서는 비트 하나가 아이노드 하나를 가리킵니다.
디렉토리 나무와 경로
디렉토리도 파일이므로 디렉토리 안에 디렉토리를 넣을 수 있습니다. 그래서 전체가 나무 모양이 됩니다.
맨 위 디렉토리가 루트입니다. 경로에서는 / 로 적습니다.
경로는 파일을 가리키는 문자열입니다. 파일 이름들을 / 로 이어 붙인 꼴입니다.
/var/log/app.log 가 그런 경로입니다.
경로를 따라가는 규칙은 하나입니다. 각 이름은 바로 앞 이름이 가리키는 디렉토리 안에서 찾습니다.
/ 로 시작하면 첫 이름은 루트에서 찾습니다. 이런 경로가 절대 경로입니다.
/var/log/app.log 라면 루트에서 var 를 찾습니다. 그 안에서 log 를, 다시 그 안에서 app.log 를 찾습니다.
중간에 이름 하나라도 못 찾으면 경로 해석이 실패합니다.
POSIX(Portable Operating System Interface)를 따르는 운영체제에서는 장치가 여럿이어도 나무가 하나입니다. POSIX 는 유닉스 계열 운영체제가 같은 호출을 같은 뜻으로 받게 맞춘 표준입니다. 다른 장치에 있는 파일 시스템을 이 나무의 한 디렉토리에 붙여 씁니다. 이 붙이기가 마운트입니다. 붙인 디렉토리가 마운트 지점입니다. 마운트 지점은 둘 중 하나입니다. 루트이거나, 부모 디렉토리와 속한 장치가 다른 디렉토리입니다.
flowchart TD
subgraph D1["첫째 장치의 파일 시스템"]
R["/"] --> V["var"]
R --> M["mnt"]
V --> L["log"]
L --> A["app.log"]
end
subgraph D2["둘째 장치의 파일 시스템"]
S["둘째 장치의 루트"] --> P["data"]
end
M -- 마운트 --> S
그림의 mnt 아래로 내려가면 장치가 바뀝니다. 프로그램은 경로만 이어 적습니다. 장치가 바뀐 줄은 모릅니다.
프로그램과 파일 시스템 사이
프로그램은 블록 번호를 다루지 않습니다. 운영체제에 이름을 건네고 나머지를 맡깁니다. 이때 부르는 것이 시스템 콜입니다. 시스템 콜은 프로그램이 운영체제 핵심부인 커널에 일을 맡기는 호출입니다.
파일에는 open · read · write · close 같은 시스템 콜을 씁니다.
open 은 이름을 받아 파일 디스크립터라는 작은 정수를 돌려줍니다. 뒤의 읽기와 쓰기는 이름 대신 이 번호로 합니다.
한 나무 안에 종류가 다른 파일 시스템이 섞여 있을 수 있습니다. 디스크 위의 파일 시스템, 네트워크 건너편의 파일 시스템, 메모리 위에만 있는 파일 시스템이 한 나무에 함께 붙습니다.
종류가 달라도 프로그램은 같은 시스템 콜만 씁니다. 커널이 그 사이에 공통 계층을 두기 때문입니다. 이 계층이 호출을 알맞은 파일 시스템으로 넘깁니다. 이 공통 계층이 가상 파일 시스템입니다.
flowchart TD
P["프로그램"] --> S["시스템 콜 · open · read · write"]
S --> V["가상 파일 시스템 · 커널 안의 공통 계층"]
subgraph FS["종류가 다른 파일 시스템"]
A["디스크 위"]
B["네트워크 건너편"]
C["메모리 위"]
end
V --> A
V --> B
V --> C
A --> DEV["블록 장치"]
디스크 위의 파일 시스템은 그 아래 블록 장치에 놓입니다. 블록 장치는 블록 단위로 읽고 쓰는 저장 장치입니다.
배경
저장 장치는 블록 번호만 압니다. 그 위에 아무 계층도 없으면 프로그램마다 자기 데이터가 몇 번 블록부터 몇 개인지를 스스로 기억해야 합니다. 장치가 여러 대이거나 종류가 다르면 각 장치의 주소 체계까지 프로그램이 알아야 합니다. 한 장치를 여러 사용자가 나눠 쓰면 남이 쓴 블록을 덮어쓰는 일을 막을 쪽도 없습니다.
그래서 장치 위의 블록 번호를 프로그램에서 떼어 낼 계층이 필요했습니다. 사용자는 이름만 압니다. 저장 장치가 여러 대여도 블록 번호는 전부 이 계층이 맡습니다. 사용자에게는 그 번호가 보이지 않습니다. 파일을 만들거나 고치거나 지우는 일은 이 계층을 거쳐서만 할 수 있습니다.
이름은 나무 모양으로 묶었습니다. 1965년 Multics 라는 운영체제의 파일 시스템 설계에서 디렉토리는 파일의 한 종류였습니다. 루트에서 이어지는 항목 이름의 사슬이 그 파일의 경로 이름이었습니다. 뒤이은 유닉스는 디렉토리 항목에 이름과 번호 하나만 두었습니다. 그 번호가 상세에서 본 아이노드 번호입니다.
예시
같은 개념이 운영체제마다 다른 제품으로 나타납니다. 리눅스·윈도우·맥에서 실물을 하나씩 듭니다. 재는 것은 그 운영체제가 파일 시스템을 어떻게 고르고 붙이나, 그리고 제품마다 무엇이 다른가입니다.
리눅스의 /etc/fstab
리눅스는 붙일 파일 시스템을 /etc/fstab 이라는 설정 파일에 한 줄에 하나씩 적습니다.
아래는 그 파일의 전형적인 한 줄입니다.
LABEL=t-home2 /home ext4 defaults,auto_da_alloc 0 2
앞의 세 칸만 봅니다. 첫 칸은 붙일 대상입니다. 블록 장치, 원격 파일 시스템, 파일 시스템 이미지 같은 것이 옵니다.
이 줄은 t-home2 라는 라벨로 장치를 가리킵니다.
둘째 칸 /home 은 마운트 지점입니다. 셋째 칸 ext4 는 파일 시스템의 종류입니다.
셋째 칸에 올 수 있는 종류는 많습니다. ext4 · xfs · btrfs · vfat · ntfs · tmpfs · sysfs · proc · nfs · cifs 가 그 일부입니다.
이 가운데 몇은 저장 장치가 없습니다. 그런 종류는 첫 칸에 아무 문자열이나 적어도 됩니다.
흔히 proc 에는 proc 을, tmpfs 에는 mem · none · tmpfs 가운데 하나를 적습니다.
저장 장치 없는 파일 시스템 둘을 보면 개념의 폭이 보입니다.
| 종류 | 내용이 사는 곳 |
|---|---|
tmpfs |
운영체제가 프로그램에 내주는 메모리 공간인 가상 메모리에 삽니다. 대개 램(RAM, Random Access Memory)에 있으므로 파일 접근이 아주 빠릅니다 |
proc |
어디에도 저장되지 않습니다. 커널 자료구조에 닿는 창구 노릇을 하는 가짜 파일 시스템입니다. 흔히 /proc 에 붙습니다 |
커널이 지금 다룰 수 있는 종류는 /proc/filesystems 에 나옵니다. 커널에 함께 컴파일됐거나 커널 모듈로 올라온 것들입니다.
nodev 표시가 붙은 종류는 붙일 때 블록 장치가 필요 없습니다. 방금 본 가짜 파일 시스템과, 네트워크 건너편에 내용을 두는 파일 시스템에 이 표시가 붙습니다.
flowchart TD
R["/ · 루트"] --> H["/home · ext4 · 라벨 t-home2 인 장치"]
R --> P["/proc · proc · 장치 없음 · 커널 자료구조를 보인다"]
이 종류들을 한 나무에 묶는 것이 리눅스 커널의 가상 파일 시스템(Virtual File System, VFS)입니다. Virtual Filesystem Switch 라는 이름도 함께 쓰입니다.
VFS 는 커널 안의 소프트웨어 계층입니다. 커널 밖에서 도는 프로그램에 파일 시스템 인터페이스를 줍니다.
프로그램이 도는 커널 밖의 이 영역이 사용자 공간입니다.
커널 안에서는 서로 다른 파일 시스템 구현이 함께 있게 합니다. open(2) · stat(2) · read(2) · write(2) · chmod(2) 가 이 계층으로 들어오는 시스템 콜입니다.
VFS 에서 아이노드는 블록 장치 파일 시스템이면 디스크에 삽니다. 가짜 파일 시스템이면 메모리에 삽니다. 아이노드 하나를 디렉토리 항목 여럿이 가리킬 수 있습니다. 하드 링크가 그렇게 만들어집니다.
윈도우의 NTFS·exFAT·UDF·FAT32
윈도우의 주요 파일 시스템은 넷입니다. NTFS(New Technology File System), exFAT(Extended File Allocation Table), UDF(Universal Disk Format), FAT32(File Allocation Table 32)입니다. 같은 윈도우 안에서도 어느 것을 쓰느냐에 따라 한계와 기능이 갈립니다.
아래 표의 저널링은 파일 시스템 구조를 고치기 전에 고칠 내용을 먼저 다른 곳에 기록하는 방식입니다. 전원이 끊긴 뒤 그 기록을 보고 마무리합니다.
| 항목 | NTFS | exFAT | UDF | FAT32 |
|---|---|---|---|---|
| 파일 이름 최대 길이 | 유니코드 255자 | 유니코드 255자 | 유니코드 127자 또는 아스키 254자 | 유니코드 255자 |
| 파일 최대 크기 | 2^64–1 바이트 | 2^64–1 바이트 | 2^64–1 바이트 | 4 GiB |
| 메타데이터만 저널링 | 예 | 아니요 | 아니요 | 아니요 |
| 하드 링크 | 예 | 아니요 | 예 | 아니요 |
| 마운트 지점 | 예 | 아니요 | 아니요 | 아니요 |
표에서 FAT32 는 4 GiB 를 넘는 파일 하나를 담지 못합니다. 나머지 셋은 2^64–1 바이트까지 담습니다. 「메타데이터만 저널링」 줄은 저널에 적는 것이 파일 내용이 아니라 파일 시스템 구조 정보에 한정된다는 뜻입니다. 넷 가운데 NTFS 만 그 줄에 「예」입니다.
하드 링크 줄도 갈립니다. 상세에서 본 「한 파일에 이름 여럿」은 이 넷 가운데 NTFS 와 UDF 에서만 됩니다. 마운트 지점 줄은 상세에서 본 붙이기를 그 파일 시스템 위에서도 할 수 있나입니다. 다른 파일 시스템을 디렉토리에 붙이는 일은 넷 가운데 NTFS 에서만 됩니다. 개념이 허용하는 것을 모든 구현이 주지는 않습니다.
맥의 APFS
맥은 APFS(Apple File System)를 씁니다. iOS 10.3 이후와 macOS High Sierra 이후에서 HFS Plus(Hierarchical File System Plus)를 대신해 기본 파일 시스템이 됐습니다. APFS 는 클론, 스냅샷, 공간 공유, 원자적 안전 저장(atomic safe-save) 같은 기능을 더했습니다. 이 가운데 클론과 원자적 안전 저장 둘을 봅니다.
클론은 디스크 공간을 더 차지하지 않는 파일이나 디렉토리 복사본입니다. 복사하는 둘이 같은 볼륨에 있을 때 씁니다. 볼륨은 하나로 묶어 다루는 저장 공간입니다. 복사한 뒤 한쪽을 고치면 고친 데이터는 다른 곳에 새로 쓰입니다. 두 파일은 안 고친 블록을 계속 함께 씁니다.
flowchart TD
subgraph S1["클론 직후"]
A1["원본 파일"] --> K1["블록 ㄱ"]
A1 --> K2["블록 ㄴ"]
B1["클론"] --> K1
B1 --> K2
end
subgraph S2["클론의 블록 ㄴ 을 고친 뒤"]
A2["원본 파일"] --> L1["블록 ㄱ"]
A2 --> L2["블록 ㄴ"]
B2["클론"] --> L1
B2 --> L3["새 블록 · 고친 내용"]
end
S1 --> S2
이렇게 고친 내용을 제자리에 덮지 않고 다른 곳에 새로 쓰는 방식을 쓰기 시 복사(copy-on-write)라고 부릅니다. APFS 는 메타데이터를 쓰기 시 복사로 다룹니다. 그래서 시스템이 갑자기 멈추는 크래시가 나도 파일 시스템 갱신이 보호됩니다. 저널링처럼 같은 내용을 두 번 쓰는 부담은 없습니다.
원자적 안전 저장은 번들과 디렉토리를 위한 기능입니다. 번들은 맥에서 여러 파일을 한 덩어리처럼 다루는 디렉토리입니다. 이 기능은 이름 바꾸기를 트랜잭션 하나로 묶습니다. 그래서 사용자가 보기에 그 작업은 끝났거나 아예 안 일어난 것 둘 중 하나입니다. 중간 없이 이렇게 둘 중 하나로만 보이는 성질을 원자적이라고 합니다.
보장과 가정
파일 시스템이 프로그램에 약속하는 것 넷을 봅니다. 대상은 메모리 캐시를 거친 쓰기, fsync, rename, 권고형 락입니다.
약속마다 그것이 기대는 전제를 함께 적습니다. 그리고 전제가 깨지면 무엇이 사라지나를 적습니다.
fsync 는 열린 파일의 데이터를 장치로 내려 달라고 요청하는 호출입니다. rename 은 파일 이름을 바꾸는 호출입니다.
락은 여럿이 같은 파일을 한 번에 하나씩 건드리게 하는 장치입니다.
기준은 상세에서 본 POSIX 입니다. 구현이 그보다 세게 약속하는 대목은 리눅스를 함께 적습니다.
메모리 캐시를 거친 쓰기
write 가 돌아왔다고 데이터가 장치에 있는 것은 아닙니다. 파일 시스템은 메모리 캐시를 거쳐 쓸 수 있습니다.
보장 — 캐시에 머문 내용은 fsync 와 그 짝 호출인 fdatasync · aio_fsync 가 성공하면 저장 장치로 넘어갑니다.
그 밖의 때에도 저절로 넘어갈 수 있습니다. 넘어가는 단위 하나하나는 원자적입니다.
그 단위는 디렉토리 항목 하나, 파일 시스템 블록 크기에 맞춘 데이터 블록 하나, 한 파일의 속성 전부입니다.
캐시를 거쳐 써도 파일에 걸리는 규칙은 캐시 안의 내용에 그대로 걸립니다. 방금 적은 전송 단위 규칙이 그 규칙의 하나입니다.
이 보장은 표준이 캐시에 대해 정한 규칙입니다. fsync 호출 자신이 무엇을 약속하는지는 구현이 _POSIX_SYNCHRONIZED_IO 라는 표시를 내걸었는지에 걸립니다. 그 조건은 아래 「fsync 의 영속성」에 적습니다.
가정 — 그 파일 시스템이 표준을 따르는 설정으로 붙어 있습니다.
가정이 깨지면 — 파일 시스템은 마운트 옵션 등으로 이 규칙을 따르지 않는 설정도 지원할 수 있습니다. 그런 설정 위에서 도는 프로그램은 파일이 제대로 동기화된다고 기대할 수 없습니다. 위 보장이 통째로 사라집니다.
fsync 를 안 부른 쓰기에는 위 보장이 걸리지 않습니다. 크래시가 캐시 전송보다 먼저 올 수 있습니다.
그러면 앞 작업의 결과가 빠진 채 나중 작업의 결과만 장치에 남을 수 있습니다.
단위를 여럿 고치는 작업으로는 mkdir, rename, 버퍼가 큰 write, 없으면 새로 만들라는 O_CREAT 표시를 붙인 open 이 있습니다.
크래시 뒤에는 이런 작업이 반쯤만 된 것처럼 보일 수 있습니다. 된 부분의 순서도 정해져 있지 않습니다.
write 의 뒤쪽 절반만 남을 수 있습니다. rename("a","b") 도 반쯤 된 채 남을 수 있습니다. b 는 생겼습니다. a 는 지워지지 않았습니다.
flowchart TD
subgraph M["메모리 캐시 · rename(a, b) 한 번"]
U1["단위 하나 · 항목 b 를 만든다"]
U2["단위 둘 · 항목 a 를 지운다"]
end
subgraph D["크래시 직후 장치"]
V1["항목 b 가 있다"]
V2["항목 a 도 아직 있다"]
end
U1 -- 넘어갔다 --> V1
U2 -- 안 넘어갔다 --> V2
작업 하나가 단위 여럿으로 쪼개집니다. 단위는 통째로 넘어갑니다. 그래도 작업은 일부만 넘어갈 수 있습니다.
fsync 의 영속성
fsync 가 돌아오면 그 파일이 장치에 남았다고 믿고 싶어집니다. 그 믿음이 어디까지 서는지 재 봅니다.
앞 소절은 캐시 내용이 장치로 넘어가는 때를 적었습니다. 여기서 재는 것은 그 넘김이 정말 일어나는지, 그리고 어디까지 닿는지입니다.
「넘겨 달라고 요청했다」와 「장치로 넘어갔다」는 같은 말이 아닙니다. 「장치로 넘어갔다」와 「전원이 끊겨도 남는다」도 같은 말이 아닙니다.
보장 — POSIX 의 fsync 는 그 파일 디스크립터의 데이터를 모두 저장 장치로 옮겨 달라고 요청합니다.
약속하는 것은 그 요청까지입니다. 옮기는 방식은 구현이 정합니다. 옮김이 끝나거나 오류가 날 때까지 돌아오지 않습니다.
fsync 가 노리는 것은 메모리 캐시의 데이터를 물리적으로 써서 크래시 뒤에도 남기는 것입니다.
그 약속의 세기는 구현이 _POSIX_SYNCHRONIZED_IO 라는 표시를 내걸었는지에 걸립니다.
내걸지 않은 구현에서 무엇을 기대할 수 있는지는 그 구현이 함께 내놓는 적합성 문서가 말합니다.
그런 구현에서는 아무것도 하지 않는 fsync 까지 명시적으로 허용됩니다.
두 끝 사이의 구현에서 fsync 는 데이터를 정전에도 안전한 곳에 쓸 수도, 안 쓸 수도 있습니다.
그래서 앞 소절의 「fsync 가 성공하면 넘어갑니다」는 표준이 캐시 쪽에 정한 규칙으로 읽어야 맞습니다. 손에 쥐어지는 세기는 구현이 내건 표시가 가릅니다.
리눅스의 fsync 는 그보다 세게 약속합니다. 메모리 캐시에서 고쳐진 데이터를 디스크 장치로 내려서 크래시나 재부팅 뒤에도 읽을 수 있게 합니다.
장치의 쓰기 캐시가 있으면 그것까지 비웁니다. 장치가 옮김을 마쳤다고 알릴 때까지 기다립니다.
가정 하나, 장치가 캐시 비우기를 지킵니다 — 많은 저장 장치, 특히 소비자용 장치는 쓰기 캐시를 갖습니다.
이 캐시는 휘발성입니다. 휘발성은 전원이 끊기면 내용이 사라진다는 뜻입니다.
이런 장치는 데이터가 비휘발성 매체에 닿기 전에 운영체제에 완료를 알립니다.
그래서 운영체제는 fsync · sync · 마운트 해제 때 데이터를 매체까지 밀어 넣어야 합니다.
리눅스 블록 계층은 파일 시스템이 장치의 캐시 동작을 다루게 두 수단을 줍니다. 강제 캐시 비우기와, 요청에 붙이는 FUA(Force Unit Access) 표시입니다.
flowchart TD
P["프로그램"] -- write --> C["커널의 메모리 캐시"]
C -- fsync --> W["장치의 쓰기 캐시 · 휘발성"]
W -- 캐시 비우기 · FUA 표시 --> N["비휘발성 저장 매체"]
그림의 마지막 화살표가 방금 적은 첫째 가정입니다. 장치가 이 단계를 지켜야 fsync 를 거친 데이터가 비휘발성 매체에 닿습니다.
가정 둘, 디렉토리 항목은 따로 내립니다 — 리눅스에서 파일에 fsync 를 불러도 그 파일이 든 디렉토리의 항목까지 디스크에 닿는다는 보장은 없습니다.
항목까지 내리려면 디렉토리의 파일 디스크립터에도 fsync 를 불러야 합니다.
POSIX 에서도 디렉토리를 고친 프로그램은 디렉토리에 fsync 를 불러 항목과 속성을 동기화할 수 있습니다.
다만 대부분의 프로그램에는 그 호출까지 필요하지 않을 것으로 봅니다.
그래서 디렉토리에 거는 fsync 는 늘 부르는 절차가 아니라 조건이 붙는 절차입니다. 새로 만든 파일의 이름이 크래시 뒤에도 남아야 할 때 부릅니다.
가정이 깨지면 — 장치가 비우기를 안 지키면 fsync 가 돌아온 데이터도 장치의 쓰기 캐시에만 있을 수 있습니다. 전원이 끊기면 함께 사라집니다.
디렉토리를 안 내리면, 새로 만든 파일의 내용은 장치에 있어도 그 파일을 가리키는 이름이 없을 수 있습니다.
fsync 가 실패했다면 아직 끝나지 않은 입출력이 끝났다는 보장이 없습니다.
rename 의 원자성
보장 — rename(old, new) 는 파일의 이름을 바꿉니다. new 라는 이름이 이미 있으면 그 항목을 지우고 old 를 new 로 바꿉니다.
바꾸는 동안 내내 new 라는 디렉토리 항목은 다른 스레드에 보입니다. 그 항목은 작업 전의 옛 new 파일이나 old 파일 중 하나를 가리킵니다.
리눅스에서는 new 가 이미 있을 때 그 이름이 원자적으로 교체됩니다. 다른 프로세스가 new 에 접근할 때 그 이름이 없는 순간이 없습니다.
대신 old 와 new 가 둘 다 그 파일을 가리키는 틈은 아마 있을 것입니다.
이 보장 덕에 파일 하나를 안전하게 갈아 끼울 수 있습니다. 새 내용을 다른 이름으로 다 쓴 뒤 rename 으로 원래 이름을 덮습니다.
읽는 쪽은 옛 내용 전체나 새 내용 전체 하나만 봅니다.
sequenceDiagram
participant W as 쓰는 프로그램
participant FS as 파일 시스템
participant R as 읽는 프로그램
W->>FS: config.tmp 에 새 내용을 다 쓴다
R->>FS: config 를 연다
FS-->>R: 옛 내용
W->>FS: rename(config.tmp, config)
R->>FS: config 를 연다
FS-->>R: 새 내용
Note over FS,R: config 가 없거나 반쯤 쓰인 순간은 안 보인다
가정 — old 와 new 가 같은 파일 시스템에 있습니다.
가정이 깨지면 — 호출이 EXDEV 오류로 실패합니다. EXDEV 는 장치를 건너는 링크라는 뜻의 오류 이름입니다. 원자적으로 갈아 끼우는 길이 막힙니다.
POSIX 가 EXDEV 를 내는 조건은 둘입니다. 두 이름이 다른 파일 시스템에 있어야 합니다. 그 구현이 파일 시스템을 건너는 하드 링크를 지원하지 않아야 합니다.
둘이 함께 맞을 때 EXDEV 를 냅니다.
리눅스는 두 이름이 같은 마운트된 파일 시스템에 없으면 EXDEV 를 냅니다. 같은 파일 시스템을 두 마운트 지점에 붙였어도 그 둘 사이로는 rename 이 안 됩니다.
이 보장은 시스템이 도는 동안 다른 프로그램이 보는 모습에 대한 것입니다. 크래시 뒤에 무엇이 남는지는 앞의 캐시 규칙을 따릅니다.
rename 도 디렉토리 항목을 고치는 작업입니다. 그 결과를 장치에 남기려면 디렉토리에 fsync 를 불러 항목을 내립니다.
권고형 락
보장 — POSIX 의 fcntl 은 권고형 레코드 락을 둡니다. 레코드 락은 파일의 한 구간에 거는 락입니다.
일반 파일에는 반드시 지원해야 합니다. 다른 종류의 파일에는 지원할 수도 있습니다.
락을 확인하는 프로세스끼리는 그 구간을 두고 차례가 지켜집니다.
가정 — 그 파일에 접근하는 모든 프로세스가 협조해서, 입출력 전에 이 락을 씁니다.
가정이 깨지면 — 락을 확인하지 않는 프로세스가 하나라도 있으면 락이 그 프로세스를 막지 못합니다.
sequenceDiagram
participant A as 프로세스 A
participant B as 프로세스 B
participant F as 파일
A->>F: 락을 건다
A->>F: 쓴다
B->>F: 락을 확인하지 않고 쓴다
Note over B,F: 권고형 락은 확인하지 않는 쪽을 막지 못한다
네 짝을 한 표로
| 약속 | 보장 | 가정 | 가정이 깨지면 |
|---|---|---|---|
| 캐시를 거친 쓰기 | 표준은 fsync 가 성공하면 캐시 내용이 장치로 넘어간다고 적습니다. 넘어가는 단위는 원자적입니다 |
그 파일 시스템이 표준을 따르는 설정으로 붙어 있습니다 | 파일이 제대로 동기화된다고 기대할 수 없습니다 |
fsync |
fsync 자신이 약속하는 것은 그 파일의 데이터를 옮겨 달라는 요청까지입니다. 그 요청이 어디까지 닿는지는 구현이 정합니다 |
장치가 캐시 비우기를 지킵니다. 디렉토리 항목은 따로 내립니다 | 끝났다고 돌아온 쓰기가 전원과 함께 사라질 수 있습니다 |
rename |
새 이름은 내내 옛 파일이나 새 파일 하나를 가리킵니다 | 두 이름이 같은 파일 시스템에 있습니다 | 리눅스는 EXDEV 로 실패합니다 |
| 권고형 락 | 락을 확인하는 프로세스끼리는 차례가 지켜집니다 | 접근하는 모든 프로세스가 협조합니다 | 협조 안 하는 프로세스를 막지 못합니다 |
경계
오브젝트 스토리지
Amazon S3(Simple Storage Service) 같은 오브젝트 스토리지도 파일 시스템일까요. 아닙니다. 적어도 이름 공간이 평평한 버킷은 아닙니다. 오브젝트 스토리지는 데이터를 객체 단위로 넣고 꺼내는 저장소입니다. 객체는 버킷이라는 그릇에 담깁니다.
이 소절은 S3 의 범용 버킷과 Google Cloud Storage 의 평평한 이름 공간 버킷을 놓고 판정합니다. 재는 것은 셋입니다. 디렉토리 나무가 있나, 이름을 원자적으로 바꾸나, 데이터의 일부만 고칠 수 있나입니다.
디렉토리 나무가 없습니다. S3 범용 버킷은 파일 시스템 같은 계층 없이 평평한 구조입니다.
S3 콘솔이 보여 주는 폴더는 이름 앞부분이 같은 객체들을 묶어 보인 것입니다.
photos/myphoto.jpg 라는 객체에서 photos/ 는 프리픽스입니다. 프리픽스는 객체 이름의 앞부분입니다. 디렉토리가 아닙니다.
/ 도 흔히 쓰는 구분 문자일 뿐 특별한 뜻이 없습니다.
Google Cloud Storage 의 평평한 이름 공간 버킷도 같습니다. 버킷 안에 계층이 없습니다.
객체가 들어가 사는 디렉토리나 폴더도 없습니다. folder1/ 은 객체 이름의 일부입니다.
flowchart TD
subgraph FS["파일 시스템"]
R["/ · 루트 디렉토리"] --> D["photos · 디렉토리"]
D --> F["myphoto.jpg · 파일"]
end
subgraph OS["평평한 버킷"]
B["버킷"] --> O["객체 · 이름 전체가 photos/myphoto.jpg"]
end
이름을 원자적으로 못 바꿉니다. Google Cloud Storage 의 객체는 이름을 바로 바꿀 수 없습니다. 복사한 뒤 원본을 지워서 바꿉니다.
이름 앞부분으로 흉내 낸 폴더를 옮기려면 그 안의 객체를 하나씩 복사하고 지워야 합니다. 이 작업은 원자적이지 않습니다.
도중에 실패하면 일부 객체만 옮겨진 어긋난 상태로 남을 수 있습니다. 파일 시스템의 rename 이 주는 보장이 여기엔 없습니다.
데이터의 일부만 고칠 수 없습니다. Google Cloud Storage 의 객체는 불변입니다. 올린 객체는 저장되어 있는 동안 바뀌지 않습니다. 뒤에 덧붙이거나 잘라 내는 식의 부분 변경을 할 수 없습니다. 객체 전체를 바꿔 넣는 것은 됩니다. 바꿔 넣기는 원자적입니다. 새 업로드가 끝나기 전에는 옛 객체가, 끝난 뒤에는 새 객체가 읽힙니다.
이 판정에는 조건이 둘 붙습니다. 첫째, S3 쪽 근거는 범용 버킷에 대한 것입니다. 둘째, Google Cloud Storage 는 계층 이름 공간을 켜면 파일 시스템 같은 폴더 계층을 줍니다. 그런 버킷이 이름 바꾸기와 부분 변경까지 파일 시스템처럼 다루는지는 확인하지 못했습니다(미확인).
이 선은 무엇을 고를지도 가릅니다. 디렉토리 묶음, 원자적 이름 바꾸기, 파일 일부 고쳐 쓰기 중 하나라도 필요하면 이름 공간이 평평한 버킷은 그 일을 못 맡습니다. 파일 시스템이 맡습니다. 객체를 통째로 넣고 꺼내기만 하면 그 셋 없이도 됩니다.
관련 항목
파일 시스템이 관리하는 저장 장치
디스크 · 파티션 · 볼륨 · 블록 장치 · SSD · 메모리
저장 공간을 세는 단위와 낭비
파일 시스템을 이루는 내부 구조
아이노드 · 디렉토리 · 슈퍼블록 · 비트맵 · 익스텐트 · 메타데이터 · 저널
파일에 이름을 붙이고 찾아가는 수단
경로 · 하드 링크 · 심볼릭 링크 · 마운트 · 루트 디렉토리 · 네임스페이스
파일을 다루려고 부르는 시스템 콜
open · read · write · rename · fsync · fdatasync · fcntl · 시스템 콜 · 파일 디스크립터
파일 시스템을 굴리는 운영체제 구성 요소
커널 · 가상 파일 시스템 · 페이지 캐시 · 디바이스 드라이버 · 사용자 공간
파일 시스템을 품은 운영체제
운영체제 · 리눅스 · 유닉스 · 유닉스 계열 · Multics · 윈도우 · macOS
갑작스러운 중단에서 데이터를 지키는 방식
저널링 · 쓰기 시 복사 · 원자성 · 영속성 · 체크섬 · 스냅샷 · 클론 · FUA
같은 파일을 동시에 만질 때 쓰는 수단
락 · 파일 잠금 · 어드바이저리 락 · 경쟁 상태 · 동시성 · 프로세스
이 방식을 구현한 파일 시스템 제품
ext4 · XFS · Btrfs · ZFS · NTFS · exFAT · UDF · FAT · APFS · HFS Plus · tmpfs · sysfs
네트워크 너머로 파일을 내주는 방식과 기계
NFS · SMB · 네트워크 파일 시스템 · 분산 파일 시스템 · 네트워크 · 서버
파일 시스템과 헷갈리는 저장소
오브젝트 스토리지 · S3 · Google Cloud Storage · 버킷 · 프리픽스
파일 시스템에서 자주 나는 장애
디스크 부족 · 아이노드 고갈 · 단편화 · 데이터 손상 · 디스크 오류
파일 시스템이 지키는 접근 규칙과 표준
POSIX · 권한 · 접근 제어 목록 · umask · setuid
파일 시스템 위에 얹혀 도는 저장 방식
다른 이름: file system · filesystem · 파일시스템