jongkwan.dev
개발 · Essay №030

운영체제 IO 관리와 파일 시스템

입출력 처리 방식(폴링·인터럽트·DMA)부터 디스크 스케줄링, inode와 저널링까지 한 흐름으로 정리합니다.

이종관2026년 1월 19일18 min read
Contents

디스크 한 번 읽는 동안 CPU가 무엇을 하는지, 파일 이름이 실제 데이터에 어떻게 닿는지를 한 흐름으로 정리합니다.

입출력(I/O) 관리와 파일 시스템은 운영체제가 저장 장치를 다루는 두 축입니다. 앞쪽은 데이터가 장치와 메모리 사이를 오가는 경로를 맡습니다. 뒤쪽은 그 데이터가 디스크 위에 놓이는 방식을 정합니다.

I/O를 처리하는 세 가지 방식

CPU가 장치를 다루는 방식은 CPU가 전송에 얼마나 개입하느냐로 갈립니다. 폴링은 CPU가 직접 기다리고, 인터럽트는 기다림을 없애며, 직접 메모리 접근은 전송 자체를 떼어냅니다.

방식CPU 개입데이터 전송 주체효율사용 사례
프로그래밍된 I/O (폴링)지속적 (busy-wait)CPU가 직접 전송낮음단순 임베디드
인터럽트 기반 I/O완료 시에만CPU가 직접 전송중간일반적인 I/O
DMA전송 완료 시에만DMA 컨트롤러가 전송높음대용량 전송

폴링(Polling)은 CPU가 루프를 돌며 장치 상태 레지스터를 반복 확인하는 방식입니다. 준비될 때까지 다른 일을 못 하므로 CPU를 낭비합니다. 인터럽트 기반 I/O는 장치가 작업을 마칠 때 인터럽트(하던 일을 멈추게 하는 하드웨어 신호)를 보내 알립니다. 덕분에 대기 동안 CPU가 다른 작업을 하지만, 전송 자체는 여전히 CPU가 수행합니다.

DMA(Direct Memory Access, 직접 메모리 접근)는 DMA 컨트롤러가 메모리와 장치 사이에서 데이터를 직접 옮깁니다. CPU를 거치지 않고, 전송이 끝날 때만 인터럽트로 알립니다. 대용량 전송에서 CPU 개입이 가장 적어 효율이 높습니다.

동기·비동기와 블로킹·논블로킹

두 축은 독립적인 개념이라 네 가지 조합이 나옵니다. 동기·비동기는 작업 완료를 누가 확인하고 통지하느냐의 문제입니다. 블로킹·논블로킹은 호출 시 제어권이 즉시 돌아오느냐의 문제입니다.

블로킹논블로킹
동기전통적 read(). 완료까지 스레드 대기O_NONBLOCK 소켓. 데이터 없으면 EAGAIN 반환 후 재시도
비동기드문 조합. 완료 통지는 비동기지만 확인 API가 블로킹될 수 있음aio_read(), io_uring. 요청 후 즉시 반환, 완료 시 통보

논블로킹을 단독으로 쓰면 데이터가 올 때까지 계속 재시도하므로 폴링이 됩니다. 그래서 보통은 뒤에서 다룰 I/O 멀티플렉싱과 묶어 씁니다. select(), poll(), epoll_wait()는 보통 동기 I/O 멀티플렉싱으로 분류합니다.

I/O 멀티플렉싱

I/O 멀티플렉싱은 한 스레드가 여러 파일 디스크립터를 동시에 감시하는 기법입니다. 파일 디스크립터(file descriptor, 줄여서 fd)는 열린 파일이나 소켓을 가리키는 정수 핸들입니다. 감시 대상이 수천 개로 늘면 감시 비용 자체가 성능을 좌우합니다.

메커니즘fd 감시 방식fd 개수 제한시간 복잡도지원 OS
select비트마스크보통 1024 (FD_SETSIZE)O(n)모든 유닉스
poll배열 (pollfd)메모리 한도O(n)모든 유닉스
epoll커널 이벤트 테이블메모리 한도O(1)리눅스
kqueue커널 이벤트 큐메모리 한도O(1)BSD·macOS

select는 호출마다 전체 fd 목록을 커널에 복사하고, 커널이 모든 fd를 순회하므로 O(n)입니다. 리눅스에서는 FD_SETSIZE 제한으로 보통 1024개까지만 감시합니다.

epoll은 epoll_ctl()로 fd를 커널의 레드-블랙 트리에 한 번 등록합니다. 이후 epoll_wait()는 준비된 fd만 돌려주므로, 이벤트 수에 비례하는 O(1)에 가깝습니다. 매번 목록을 복사하지 않으니 대규모 연결에서 차이가 벌어집니다.

디스크 스케줄링

하드 디스크 드라이브(HDD)는 기계 장치라 탐색 시간(seek time, 헤드가 목표 트랙으로 이동하는 시간)이 성능을 좌우합니다. 스케줄링은 대기 중인 요청의 처리 순서를 바꿔 헤드 이동 거리를 줄입니다.

아래 수치는 Operating System Concepts(Silberschatz 외)에 실린 표준 디스크 스케줄링 예제의 총 헤드 이동량입니다. 헤드 시작 위치는 53, 트랙 범위는 0~199, 요청 큐는 98·183·37·122·14·124·65·67입니다.

알고리즘설명총 헤드 이동단점
FCFS요청 순서대로 처리640비효율적 헤드 이동
SSTF가장 가까운 요청 먼저236기아(starvation) 가능
SCAN한 방향 끝까지 이동 후 반대236양 끝 대기 불균등
C-SCAN한 방향만 서비스, 끝에서 처음으로 복귀382복귀 이동 낭비
LOOK요청 있는 곳까지만 이동208-
C-LOOK요청 있는 곳까지만, 처음으로 복귀322-

SSTF는 가까운 요청만 처리해 빠르지만, 멀리 있는 요청이 영원히 밀리는 기아가 생길 수 있습니다. SCAN은 엘리베이터처럼 한 방향으로 훑어 기아를 막습니다. SSD는 랜덤 접근 비용이 균일해 이런 스케줄링의 효과가 작습니다.

버퍼링과 페이지 캐시

버퍼링은 장치 속도와 처리 속도의 차이를 메우려고 데이터를 잠시 모아 두는 기법입니다. 구조에 따라 세 가지로 나뉩니다.

전략구조설명
단일 버퍼[buf]I/O와 처리를 교대로 수행
이중 버퍼[A][B]한쪽을 채우는 동안 다른 쪽을 처리
순환 버퍼[0][1]...[n-1]생산자-소비자 패턴, 큐처럼 동작

페이지 캐시(page cache)는 커널이 디스크 블록을 메모리에 보관해 재접근을 빠르게 하는 영역입니다. 쓰기 정책은 캐시에만 먼저 쓰고 나중에 디스크에 반영하는 Write-Back이 기본입니다. 캐시와 디스크에 동시에 쓰는 Write-Through는 안전하지만 느립니다. 순환 버퍼는 io_uring이나 네트워크 드라이버의 송수신 링에서 자주 씁니다.

파일 시스템 디스크 레이아웃

전통적인 유닉스 파일 시스템(ext 계열)은 디스크를 네 영역으로 나눕니다. 부팅 코드, 메타 정보, inode 배열, 실제 데이터가 순서대로 놓입니다.

영역역할
부트 블록부트스트랩 코드 저장. 부팅 시 최초 실행
슈퍼블록블록 크기, 총 블록 수, inode 수 등 메타 정보
inode 테이블모든 inode를 배열로 저장
데이터 블록파일 내용과 디렉터리 엔트리 저장

ext4는 디스크를 블록 그룹(block group)으로 나누고, 그룹마다 슈퍼블록 사본과 비트맵, inode 테이블을 둡니다. 이렇게 분산하면 관련 데이터가 한 그룹에 모여 지역성이 좋아지고, 사본 덕에 손상 복구에도 유리합니다.

inode와 블록 포인터

inode(Index Node)는 파일의 메타데이터와 데이터 블록 위치를 담는 자료구조입니다. 파일 이름은 inode에 들어 있지 않고, 디렉터리 엔트리의 (이름 → inode 번호) 매핑에 저장됩니다. 그래서 같은 inode를 여러 이름이 가리킬 수 있습니다.

전통적인 ext2/ext3 inode는 블록 포인터 15개를 가집니다. 12개는 데이터 블록을 직접 가리키고, 나머지 3개는 간접 블록을 거칩니다.

블록 4KB, 포인터 4B를 가정하면 각 간접 단계가 커버하는 용량은 다음과 같습니다. 작은 파일은 직접 블록만으로 빠르게 닿고, 큰 파일만 간접 단계 비용을 치릅니다.

레벨커버 용량
직접 (12개)48 KB
단일 간접4 MB
이중 간접4 GB
삼중 간접4 TB

ext4는 간접 블록 대신 익스텐트(Extent)를 씁니다. 연속된 블록을 (시작 블록, 길이)로 표현해, 대용량 파일의 메타데이터 오버헤드를 줄입니다.

파일 디스크립터의 3단계 테이블

open()이 돌려주는 fd 번호 하나가 실제 inode에 닿기까지는 세 단계를 거칩니다. fd 테이블은 프로세스마다 따로 있고, 열린 파일 테이블과 inode 테이블은 커널이 공유합니다.

읽기·쓰기 위치를 뜻하는 오프셋(offset)은 fd가 아니라 열린 파일 테이블 엔트리에 있습니다. 같은 파일을 두 번 open()하면 엔트리가 둘 생겨 오프셋이 서로 독립합니다. 반대로 fork()dup()로 fd를 복제하면 같은 엔트리를 공유해 오프셋도 함께 움직입니다.

파일 할당 방식

데이터 블록을 디스크에 어떻게 배치하느냐에 따라 직접 접근 성능과 단편화가 달라집니다. 세 방식의 절충점이 분명합니다.

기준연속 할당연결 할당인덱스 할당
직접 접근O(1)O(n) 순회O(1)
외부 단편화발생없음없음
크기 변경어려움용이용이
실제 사용CD-ROMFAT유닉스·ext 계열

연속 할당은 시작 블록과 길이만 알면 바로 접근하지만, 파일이 커지면 재배치가 필요합니다. 연결 할당은 각 블록에 다음 블록 포인터를 두어 크기 변경은 쉽지만, 직접 접근이 느립니다. FAT(File Allocation Table)는 연결 할당의 변형으로, next 포인터를 별도 테이블에 모아 직접 접근을 개선했습니다. 유닉스 inode는 인덱스 할당에 해당합니다.

저널링과 충돌 복구

저널링(journaling)은 데이터를 제자리에 쓰기 전, 변경 내용을 먼저 저널 영역(로그)에 기록하는 기법입니다. 이 선기록 방식을 Write-Ahead Logging(WAL)이라 부릅니다. 쓰기 도중 전원이 꺼져도 저널을 보고 일관성을 복구할 수 있습니다.

복구 기준은 커밋 시점입니다. 커밋(TxEnd) 이전에 충돌하면 저널을 무시해 변경이 없던 것으로 두고, 이후에 충돌하면 저널을 리플레이해 제자리에 다시 씁니다. 저널에 무엇을 기록하느냐에 따라 모드가 갈립니다.

모드저널 대상성능안전성
Journal (Full)메타데이터 + 데이터가장 느림가장 높음
Ordered (ext4 기본)메타데이터만, 데이터 먼저 기록 보장중간높음
Writeback메타데이터만, 순서 무보장가장 빠름낮음

Ordered는 데이터 블록을 먼저 디스크에 쓴 뒤 메타데이터를 커밋하므로, 메타데이터가 가리키는 데이터가 항상 유효합니다. Writeback은 순서를 보장하지 않아, 충돌 시 메타데이터가 아직 안 쓰인 블록을 가리켜 쓰레기 데이터가 노출될 수 있습니다. 한편 Btrfs나 ZFS 같은 Copy-on-Write(CoW) 파일 시스템은 제자리를 덮어쓰지 않고 새 블록에 써서, 저널 없이도 일관성을 지킵니다. 다만 작은 수정도 새 블록을 할당해 쓰기 증폭이 생깁니다.

정리

I/O 관리의 핵심은 CPU가 전송에서 얼마나 빠지느냐입니다. 폴링은 CPU가 직접 기다리고, 인터럽트는 기다림을 없애며, DMA는 전송 자체를 컨트롤러에 넘깁니다. 같은 맥락에서 epoll은 fd를 한 번 등록해 준비된 것만 받아 대규모 연결을 O(1)에 가깝게 감시합니다.

파일 시스템 쪽은 이름과 데이터를 떼어 놓은 설계가 중심입니다. 디렉터리 엔트리가 이름을 inode 번호로 바꾸고, inode가 직접·간접 블록 포인터로 데이터에 닿으며, 저널이 충돌 뒤 일관성을 지킵니다. 두 영역을 함께 보면, 파일 한 줄을 읽는 동안 스케줄링·캐시·블록 포인터·저널이 어떻게 맞물리는지가 한눈에 잡힙니다.