운영체제 학습 노트 목차

파일 시스템 — 디스크를 이름으로 다루다

디스크는 사실 번호가 매겨진 블록들의 거대한 배열일 뿐이다 — "3829번 섹터에 써라" 같은 식이다. 그런데 우리는 /home/user/report.txt라는 이름으로 읽고 쓴다. 이 블록 덩어리를 이름·디렉터리·권한을 가진 파일로 추상화하는 게 파일 시스템이다. OS의 추상화 중에서도 — *영구 저장(persistence)*을 다루기에, 전원이 갑자기 나가도 데이터가 망가지지 않게 하는 게 핵심 과제다. 그 추상과 무결성 보장을 본다.

파일은 무엇으로 이뤄지나 — inode와 디렉터리

파일 하나는 데이터만이 아니라 메타데이터도 갖는다 — 크기, 소유자, 권한, 수정 시각, 그리고 실제 데이터가 디스크의 어느 블록에 있는지. 이 메타데이터를 담는 게 **inode**다. 흥미롭게도 — 파일 이름은 inode에 없다. 이름은 디렉터리가 갖는데, 디렉터리란 사실 "이름 → inode 번호" 매핑의 표다.

다이어그램 로딩 중…

이 분리 덕에 한 파일에 여러 이름(하드 링크)을 줄 수 있고, 이름을 바꿔도 데이터는 그대로다(inode는 안 변하니까).

전원이 갑자기 나가면 — 크래시 일관성과 저널링

여기서 영구 저장의 어려움이 드러난다. 파일 하나를 만드는 것조차 — 디렉터리에 항목 추가 + inode 초기화 + 빈 블록 비트맵 갱신, 즉 디스크의 여러 곳을 써야 한다. 그런데 그 중간에 전원이 나가면?디렉터리는 파일을 가리키는데 inode는 비어 있거나, 블록은 쓰였다고 표시됐는데 어느 파일도 안 가리키는 불일치가 남는다. (출처: Crash Consistency.)

해법이 **저널링(journaling)**이다 — *실제 위치에 쓰기 전에, "이러이러하게 바꾸겠다"를 먼저 *저널(로그)에 기록하고, 저널이 안전히 쓰인 뒤 실제 구조를 갱신한다. 만약 중간에 죽으면 — 재부팅 때 *저널을 다시 적용(redo)*해 일관성을 회복한다.

왜 이게 보편적인가 — "실제 변경 전에 의도를 로그에 먼저 기록한다"는 **선행 기록 로그(write-ahead log, WAL)**는 파일 시스템만의 것이 아니다. 데이터베이스의 트랜잭션 로그(그래서 DB가 죽어도 커밋된 트랜잭션을 복구)도, 로그 기반 메시지 시스템도 같은 원리다. 원자적으로 못 하는 여러 단계를, 로그에 먼저 한 줄로 기록해 *"전부 되거나 전부 안 되거나"*를 흉내 내는 것 — 이것이 영속성과 신뢰성의 보편적 기법이다.

디스크는 느리다 — 페이지 캐시

디스크는 메모리보다 수만 배 느리다. 그래서 OS는 읽은 디스크 블록을 메모리(페이지 캐시)에 보관해 — 다음에 같은 걸 읽으면 디스크 대신 메모리에서 준다. 쓰기도 일단 페이지 캐시에 모았다가 나중에 디스크로(write-back) 내보내 빠르게 응답한다. 다만 — write-back은 위험하다. 캐시에만 있고 디스크에 안 내려간 데이터는 전원이 나가면 사라진다. 그래서 반드시 보존돼야 할 데이터는 **fsync**로 *"지금 디스크에 강제로 내려라"*를 호출한다(DB가 커밋 시 fsync로 내구성을 보장하는 게 이것이다). 그리고 여러 종류의 파일 시스템(ext4·XFS·NFS…)을 *같은 인터페이스(open/read/write)*로 쓰게 해 주는 게 VFS(가상 파일 시스템) — 또 하나의 추상화 계층이다.

왜 빠른가 — 이 페이지 캐시가 고성능 저장·전송 시스템의 비결이다. 디스크에 순차로 쓰되, 읽는 쪽이 따라잡고 있으면 페이지 캐시에서 바로 내보내면(디스크를 안 읽음) 엄청나게 빨라진다 — 다음 편에서 볼 zero-copy가 이 페이지 캐시를 네트워크로 직행시키는 기법이다. 작은 I/O를 버퍼에 모아 시스템 콜·디스크 접근을 줄이는 버퍼링도 같은 발상이다.

실제로 눈으로 보기

inode 를 직접 본다

ls -li /etc/hosts
# 1049234 -rw-r--r-- 1 root root 220 Aug 17 03:12 /etc/hosts
# └ inode 번호

stat /etc/hosts
#   File: /etc/hosts
#   Size: 220        Blocks: 8          IO Block: 4096   regular file
# Device: 8,1  Inode: 1049234  Links: 1
# Access: 2026-08-17 03:12:44   Modify: 2026-08-01 09:20:11

이름은 inode 안에 없다. 이름은 디렉터리가 갖고 있고, 디렉터리는 이름 → inode 번호 표일 뿐이다. 그래서 하드 링크가 성립한다.

ln /etc/hosts /tmp/hosts-link
ls -li /etc/hosts /tmp/hosts-link
# 1049234 ... Links: 2 /etc/hosts        ← inode 가 같다
# 1049234 ... Links: 2 /tmp/hosts-link

같은 inode 를 가리키는 이름이 둘이다. rm이 하는 일도 파일을 지우는 것이 아니라 이름을 지우고 링크 수를 하나 줄이는 것이다. 0이 되어야 실제로 회수된다.

inode 가 먼저 바닥날 수 있다

df -h /       # 용량은 40% 만 썼는데
# Filesystem  Size  Used Avail Use% Mounted on
# /dev/sda1    50G   20G   28G  42% /

df -i /       # inode 는 다 썼다
# Filesystem  Inodes  IUsed IFree IUse% Mounted on
# /dev/sda1    3.2M    3.2M     0  100% /

"디스크가 남았는데 No space left on device" 라는 혼란스러운 오류가 여기서 나온다. inode 는 포맷할 때 개수가 정해지므로, 아주 작은 파일을 수백만 개 만들면 용량보다 먼저 고갈된다. 세션 파일·캐시 조각을 안 지우는 시스템에서 흔하다.

지웠는데 공간이 안 늘어난다

lsof +L1
# COMMAND  PID  USER  FD  TYPE  DEVICE  SIZE/OFF  NLINK  NODE NAME
# myapp   8123  app   3w  REG   8,1   4823102912      0  1049300 /var/log/app.log (deleted)
#                                                    └ NLINK 0 인데 열려 있다

이름은 지웠지만 누군가 파일을 열고 있으면 회수되지 않는다. 링크 수가 0이어도 열린 디스크립터가 남아 있으면 커널이 못 지운다. 로그 파일을 rm 했는데 디스크가 그대로인 전형적인 상황이고, 해법은 프로세스를 재시작하거나 로그를 잘라내는(truncate) 것이다.

페이지 캐시가 있어서 빠르다

echo 3 > /proc/sys/vm/drop_caches      # 캐시를 비우고
time cat bigfile > /dev/null            # real 2.481s   ← 디스크에서 읽는다
time cat bigfile > /dev/null            # real 0.092s   ← 캐시에서 읽는다

27배 차이가 난다. free에서 buff/cache가 큰 것이 낭비가 아니라 정상인 이유다.

썼다고 디스크에 있는 것이 아니다

write(fd, buf, n);        // 페이지 캐시까지만 갔다. 전원이 나가면 사라진다
fsync(fd);                // 여기서야 디스크에 확정된다
strace -e trace=write,fsync,fdatasync ./myapp

write만 있고 fsync가 없다면 그 프로그램은 "썼다"고 믿지만 보장은 없다. DB의 WAL, 저널링 파일 시스템이 이 구분 위에 서 있다 — 먼저 기록하고 확정한 뒤에 본 데이터를 고친다.

정리하면, 파일 시스템은 번호 매겨진 블록을 이름·메타데이터를 가진 파일로 추상화한다 — *inode*가 메타데이터·블록 위치를, 디렉터리가 이름→inode 매핑을 갖는다. 영구 저장의 핵심 과제는 크래시 일관성인데, *저널링(WAL)*으로 실제 변경 전에 로그를 먼저 써 복구 가능하게 한다(DB 트랜잭션 로그와 같은 원리). 느린 디스크는 페이지 캐시로 가리되 내구성은 fsync로 보장하고(zero-copy의 토대), VFS로 여러 파일 시스템을 한 인터페이스로 묶는다. 파일이 저장된 데이터라면 — 장치와 데이터를 실제로 주고받는 법, I/O가 다음이다.

메모리 관리 — 단편화·교체·스래싱·OOMI/O — 폴링/인터럽트·DMA·blocking/async