Chapter 7

디스크와 파일시스템

  • 7.1 I/O 장치
  • 7.2 하드 디스크
  • 7.3 RAID
  • 7.4 파일과 디렉토리
  • 7.5 VSFS 구현
  • 7.6 FFS
  • 7.7 크래시 일관성
  • 7.8 로그 구조 파일시스템

영속성(persistence) 파트에 들어왔어. 여기서부터는 데이터가 전원이 꺼져도 살아남는 이야기야. 먼저 I/O 디바이스부터 시작하자.

현대 시스템에서 CPU와 메모리는 메모리 버스로, I/O 디바이스들은 PCIe 같은 I/O 버스나 USB/SATA 같은 주변 버스로 연결돼. 빠른 장치(GPU)는 CPU 가까이, 느린 장치(키보드)는 먼 버스에 넣는 계층 구조야. 디바이스는 OS가 제어하는 인터페이스(상태/명령/데이터 레지스터)와 내부 하드웨어/펌웨어로 나뉘어. OS가 디바이스 상태를 반복적으로 확인하는 **폴링(polling)**은 CPU를 낭비하고, 디바이스가 완료 시 CPU에 알리는 인터럽트는 컨텍스트 스위치 오버헤드가 있지. 빠른 디바이스에서는 인터럽트보다 폴링이 나을 수 있고, 네트워크 패킷이 폭주하면 인터럽트가 넘쳐서 라이브락이 되기도 해. 그래서 처음엔 폴링하다가 한동안 이벤트가 없으면 인터럽트로 전환하는 하이브리드나, 여러 인터럽트를 모아서 처리하는 coalescing을 써. 큰 데이터를 디바이스로 보낼 때는 **DMA(Direct Memory Access)**가 CPU 대신 메모리에서 직접 전송해서 CPU를 해방시키지. 디바이스 드라이버가 디바이스별 세부사항을 캡슐화하고 OS는 일반적인 인터페이스(read, write)만 정의하는데, OS 커널 코드의 70% 이상이 드라이버라는 통계가 있을 정도야.

**하드 디스크(HDD)**는 플래터 위의 트랙섹터(512바이트)로 나눈 구조야. 데이터에 접근하려면 디스크 헤드가 트랙으로 이동하고(탐색), 디스크가 회전해서 섹터가 헤드 밑으로 와야 하고(회전 지연), 그다음 실제 데이터를 읽지(전송). 전부 물리적 움직임이라 느려. 탐색이 평균 수 ms, 회전 지연이 7200 RPM 기준 평균 4ms, 전송은 무시할 수준. 순차 접근이 랜덤 접근보다 수십~수백 배 빠른 이유가 탐색과 회전을 안 해도 되기 때문이야. 디스크 I/O 요청이 쌓이면 디스크 스케줄링으로 순서를 최적화하는데, SSTF(가장 가까운 요청 먼저)는 탐색을 최소화하지만 기아 문제가 있고, SCAN(엘리베이터 알고리즘)은 헤드가 한쪽 끝에서 다른 쪽으로 이동하면서 처리해서 공정하지. 현대 디스크는 내부적으로 자체 스케줄링을 하기도 하고, 연속 블록 요청을 합치는 I/O 병합이나 인접 요청을 기다리는 예측적 스케줄링도 활용해.

**RAID(Redundant Array of Inexpensive Disks)**는 여러 디스크를 묶어서 용량, 성능, 신뢰성을 개선하는 기술이야. 호스트에게는 하나의 큰 디스크처럼 보여. RAID 0(스트라이핑)은 데이터를 여러 디스크에 번갈아 분배해서 성능은 최고지만 중복이 없어서 디스크 하나 고장나면 끝이야. RAID 1(미러링)은 모든 데이터를 두 디스크에 복제해서 읽기가 좋고 한 디스크 고장에도 안전하지만, 용량이 반으로 줄어. RAID 4는 스트라이핑 + 전용 패리티 디스크인데, XOR 연산으로 디스크 하나 고장을 복구할 수 있어. 근데 모든 쓰기가 패리티 디스크를 업데이트해야 하니까 **패리티 디스크가 병목(small-write problem)**이지. RAID 5는 패리티를 모든 디스크에 분산시켜서 이 병목을 해결한 거야. 용량 효율, 성능, 신뢰성 사이에서 가장 좋은 균형을 제공해서 가장 일반적으로 쓰이는 패리티 기반 RAID 레벨이야.

사용자가 영속적 데이터를 다루는 인터페이스가 파일과 디렉토리야. 파일은 바이트의 연속이고 각각 inode 번호라는 저수준 이름을 가져. 디렉토리는 (이름, inode 번호) 쌍의 리스트이고, 디렉토리 안에 디렉토리를 넣으면 트리 구조가 돼. open()으로 **파일 디스크립터(fd)**를 얻고, read()/write()로 데이터를 다루는데, 오프셋이 자동으로 전진해서 순차 접근이 자연스러워. lseek()으로 랜덤 접근도 가능하고. write()는 버퍼에만 쓰고 리턴할 수 있어서 영속성을 보장하려면 **fsync()**가 필수야. 하드 링크는 같은 inode를 가리키는 다른 이름이고 참조 카운트가 0이 되면 파일이 삭제돼. 심볼릭 링크는 경로명을 저장하는 파일이라 원본이 삭제되면 댕글링 참조가 되지. mount()로 여러 파일 시스템을 하나의 디렉토리 트리에 부착할 수 있어.

**VSFS(Very Simple File System)**로 파일 시스템 내부 구현을 파볼게. 디스크를 보통 4KB 블록으로 나눠서, 수퍼블록(파일 시스템 메타데이터), inode/데이터 비트맵(할당 상태), inode 테이블, 데이터 영역으로 구성해. inode는 파일의 메타데이터(크기, 소유자, 권한, 타임스탬프)와 데이터 블록 위치를 저장하는데, 위치 저장 방식이 영리해 — 직접 포인터(12~15개)로 작은 파일은 바로 가리키고, 간접 포인터(단일/이중/삼중)로 큰 파일을 지원하는 다단계 인덱스 구조야. 대부분의 파일이 4KB 이하로 작으니까 직접 포인터만으로 충분한 경우가 많고, 불균형 트리로 큰 파일도 커버하는 거지. /foo/bar를 여는 과정을 추적하면, 루트 inode → 루트 데이터 블록에서 foo 찾기 → foo의 inode → foo 데이터에서 bar 찾기 → bar의 inode — 경로가 깊을수록 I/O가 많아져. 파일에 블록 하나를 쓰면 비트맵 읽기/쓰기 + 데이터 쓰기 + inode 읽기/쓰기로 5번의 I/O가 필요할 수 있어. 캐싱(페이지 캐시)과 write buffering으로 이 오버헤드를 줄이지만, 버퍼의 데이터를 잃을 위험은 있어.

**FFS(Fast File System)**는 원래 유닉스 FS가 시간이 지나면서 데이터가 흩어져서 순차 접근도 랜덤처럼 느려지는 문제를 해결했어. 핵심은 관련 데이터를 디스크에서 가까이 배치하는 거야. 디스크를 여러 **실린더 그룹(블록 그룹)**으로 나누고, 각 그룹에 자체 수퍼블록/비트맵/inode/데이터 영역을 둬. 파일은 자기 디렉토리와 같은 그룹에, 새 디렉토리는 디렉토리가 적고 빈 inode가 많은 그룹에 배치해서 관련 데이터는 가까이, 무관한 데이터는 멀리 둬. 큰 파일이 하나의 그룹을 독점하면 안 되니까 여러 그룹에 청크 단위로 분산시키는데, 청크를 충분히 크게 잡으면 그룹 간 탐색 비용을 순차 전송 시간으로 상쇄할 수 있어. FFS가 "디스크 인식" 파일 시스템의 시초이고, ext2/3/4, XFS 등이 전부 FFS의 영향을 받았지.

파일 시스템이 디스크에 데이터를 쓰는 도중에 크래시하면 크래시 일관성 문제가 발생해. 파일에 블록을 추가하려면 데이터 블록, inode, 데이터 비트맵 세 가지를 업데이트해야 하는데, 일부만 반영되고 크래시하면 inode가 쓰레기 데이터를 가리키거나, 공간 누수가 생기거나, 온갖 일관성 문제가 터져. 초기 접근인 FSCK는 크래시 후 전체 디스크를 스캔해서 복구하는데, TB 단위 디스크에서 수십 분~수 시간 걸려서 현실적이지 않아. 현대 해법은 저널링(journaling) — 실제 데이터를 쓰기 전에 "무슨 작업을 할 건지"를 **저널(로그)**에 먼저 기록하는 거야. 크래시 나면 저널만 확인해서 미완성 트랜잭션을 재실행하거나 무시하면 돼. 데이터 저널링은 메타데이터와 데이터 모두 저널에 써서 완벽하지만 쓰기가 두 배고, 메타데이터 저널링(ext3/4, NTFS, XFS의 기본값)은 메타데이터만 저널에 쓰고 데이터는 직접 최종 위치에 써서 오버헤드가 적어. 단, 데이터를 먼저 쓰고 메타데이터를 나중에 저널에 써야 순서가 맞아. COW 파일 시스템(ZFS, btrfs)은 기존 데이터를 수정하지 않고 새 위치에 쓰고 포인터를 전환하는 방식으로 저널이 필요 없어.

**LFS(Log-structured File System)**는 완전히 다른 발상이야. 메모리가 커지면서 읽기 대부분을 캐시가 처리하니까, 디스크 트래픽의 대부분은 쓰기야. 디스크의 순차 쓰기가 랜덤보다 수십 배 빠르다는 걸 극한까지 활용해서, 모든 업데이트를 하나의 연속된 로그로 디스크에 써. 여러 쓰기를 모아서 큰 세그먼트를 만들고 한 번에 순차적으로 쓰니까 탐색이 없어. 근데 inode가 고정 위치에 없으니까 **inode map(imap)**이 inode 번호를 최신 디스크 주소로 매핑하고, imap 자체도 로그에 함께 쓰고, **체크포인트 영역(CR)**이라는 고정 위치의 작은 영역이 최신 imap 위치를 가리켜. 접근 경로는 CR → imap → inode → 데이터. 가장 큰 과제는 가비지 컬렉션 — 파일을 수정하면 옛 버전이 디스크에 남거든. 세그먼트 클리닝으로 유효한 데이터만 옮기고 빈 세그먼트를 회수하는데, 핫/콜드 분리(자주 바뀌는 데이터와 안 바뀌는 데이터를 다른 세그먼트에)가 성능에 큰 영향을 미쳐. LFS의 아이디어가 btrfs, ZFS 같은 현대 COW 파일 시스템으로 발전한 거야.


정리

7장 읽고 기억할 거 세 가지:

  1. 디스크 I/O의 느림은 물리적 움직임(탐색+회전) 때문이야. 순차 접근이 랜덤보다 수십 배 빠르고, DMA가 CPU를 데이터 전송에서 해방시켜. RAID 5는 패리티 분산으로 용량-성능-신뢰성의 균형을 잡아
  2. inode의 다단계 인덱스가 작은 파일부터 큰 파일까지 효율적으로 지원하고, FFS의 실린더 그룹이 관련 데이터를 가까이 배치해서 성능을 높여. 파일 경로 탐색은 디렉토리를 하나씩 따라가는 거라 경로가 깊을수록 I/O가 많아져
  3. 크래시 일관성은 저널링(메타데이터 저널링이 표준)으로 해결하고, LFS는 모든 쓰기를 순차적으로 해서 쓰기 성능을 극대화해. 가비지 컬렉션이 LFS의 핵심 과제이고, 이 아이디어가 현대 COW 파일 시스템으로 이어져