CS · 기본
개발자를 위한 컴퓨터 사이언스 - 내 코드가 실제로 도는 곳
파일 시스템과 I/O 경로 - inode, 페이지 캐시와 저장 보장 구분 (CS 기초 9장)
파일 이름과 inode의 관계, 애플리케이션 버퍼에서 저장 장치로 이어지는 경로를 살펴보고 블로킹과 논블로킹·다중화의 차이를 직접 실행하며 익힌다.
개발자 · 원고 갱신
이 장에서 배우는 것
8장에서 공유 데이터의 동시성 보장을 살펴봤다. 이번 장은 그 데이터가 프로세스 밖의 파일로 나갈 때 어디서 기다리는지 다룬다. 선수지식은 프로세스와 가상 메모리의 구분이다.
- 파일 이름과 열린 파일의 관계를 설명한다.
- 버퍼 비우기와 저장 동기화가 보장하는 범위를 구분한다.
- 블로킹·논블로킹·다중화의 비용을 비교한다.
- 로그 누락과 파일 생성 실패를 서로 다른 경로의 증거로 진단한다.
개념
로그에 성공을 기록한 직후 프로세스가 종료됐는데 마지막 줄이 사라졌다고 가정한다. 같은 파일을 다시 읽으면 줄이 보였다는 기록도 있다. 이 두 관찰만으로 저장 장치 불량이라고 결론을 내릴 수 없다. 쓰기를 요청한 위치, 다시 읽은 위치, 고장 난 범위를 따로 놓아야 한다.
파일 이름과 열린 파일은 수명이 다르다
리눅스의 디렉터리 항목은 이름을 파일의 inode에 연결한다. inode에는 파일 종류, 크기, 권한 등 메타데이터가 연결되며 이름 자체를 파일의 유일한 정체성으로 삼지 않는다. 같은 파일 시스템에서 하드 링크 두 개는 같은 inode를 가리킬 수 있다. 번호는 파일 시스템 안에서 의미가 있으므로 서로 다른 장치의 번호가 같다는 이유로 같은 파일이라고 판단하지 않는다.
파일을 열어 얻은 파일 서술자는 프로세스가 사용하는 작은 정수 식별자다. 경로 문자열을 매번 다시 찾는 것과 이미 열린 대상을 읽는 것은 다르다. 로그 이름을 바꾼 뒤 새 파일을 만들어도 기존 서술자를 쥔 프로세스는 이전 대상을 계속 쓸 수 있다. 파일 회전 작업에서 경로가 바뀌었다는 사실과 기록 대상이 바뀌었다는 사실을 구분해야 한다.
이름 A ─┐
├─ 같은 파일 시스템의 inode → 파일 내용
이름 B ─┘
열린 서술자 → 열린 파일 상태 → 파일 내용버퍼를 비우는 단계와 기다리는 대상
로그 한 줄은 애플리케이션의 사용자 공간 버퍼에 먼저 머물 수 있다. 버퍼를 비우면 커널의 파일 쓰기 경로로 전달되고, 일반적인 버퍼드 I/O에서는 페이지 캐시에 반영된다. 저장 장치로 반영되는 시점은 이보다 늦을 수 있다. 다시 읽혀 보이는 것과 전원 장애 뒤에도 남는 것은 다른 조건이다.
파이썬 파일 객체의 flush는 그 객체의 버퍼를 아래 계층으로 전달한다. 이어서 파일 서술자에 fsync를 요청하면 저장 동기화를 기다린다. 리눅스에서 새 이름이나 이름 변경까지 보존하려면 상위 디렉터리의 동기화도 별도로 고려해야 한다. 이 장의 실습은 기존에 연 임시 파일의 데이터 경로를 확인하며 전원 장애 복구 시험을 대신하지 않는다.
| 방식 | 기다리는 순간 | 애플리케이션의 책임 |
|---|---|---|
| 블로킹 | 작업을 진행할 수 있을 때까지 호출 안에서 대기 | 대기 중인 스레드 수와 제한 관리 |
| 논블로킹 | 즉시 진행 가능한 만큼 처리하거나 대기 필요 반환 | 남은 데이터와 재시도 시점 보관 |
| 준비 상태 다중화 | 여러 서술자 중 준비된 대상이 생길 때 대기 | 준비 알림 뒤 실제 읽기·쓰기 수행 |
다중화는 파일 내용을 대신 읽어 주는 기능이 아니다. 읽을 가능성이 생긴 대상들을 골라 주므로 호출 횟수를 줄일 수 있지만, 이후 작업이 길면 다른 대상은 여전히 기다린다. 논블로킹 읽기가 아직 준비되지 않았다고 반환하는 경우와 더 이상 보낼 데이터가 없어 끝났다고 반환하는 경우도 구분해야 한다. 준비되지 않았을 때 쉬지 않고 반복하면 대기 시간을 CPU 사용량으로 바꾸게 된다.
기록을 읽는 프로그램과 기록하는 프로그램이 다르면 두 프로그램이 같은 파일을 열었는지도 먼저 확인한다. 경로는 같아 보여도 파일 회전 뒤에 열린 대상은 달라질 수 있다. 실패한 동작과 그 대상의 식별값을 함께 남긴다. 이렇게 기록하면 쓰기가 멈춘 것인지 새 이름만 바라보고 있었던 것인지 분리할 수 있다.
로그 데이터는 애플리케이션 버퍼, 커널 페이지 캐시, 저장 동기화 요청과 장치 완료 보고를 차례로 거친다.
직접 확인하기
임시 파일에서 flush와 fsync를 분리한다
import os
import tempfile
with tempfile.TemporaryFile(mode='w+b') as f:
f.write(b'event=ready\n')
f.flush()
os.fsync(f.fileno())
f.seek(0)
print(f.read().decode().strip())Python 3 표준 라이브러리 예제이며 임시 파일은 블록을 벗어나면 닫혀 정리된다. 출력은 저장 요청 뒤 같은 파일을 읽었다는 증거다. 성공 반환을 확인했지만 저장 장치가 실제 전원 상실을 견디는지는 이 코드로 측정하지 않았다.
출력
----
event=ready두 이름이 한 파일을 가리키는지 확인한다
import os
import tempfile
from pathlib import Path
with tempfile.TemporaryDirectory() as folder:
a = Path(folder) / 'first.log'
b = Path(folder) / 'second.log'
a.write_bytes(b'one\n')
os.link(a, b)
sa, sb = a.stat(), b.stat()
print('same file:', (sa.st_dev, sa.st_ino) ==
(sb.st_dev, sb.st_ino))
print('links:', sa.st_nlink)로컬 파일 시스템에서 하드 링크를 만들 수 있는 환경을 전제한다. 출력의 링크 수는 열린 서술자 수가 아니라 이름 연결 수다. 이름이 두 개라도 내용이 두 벌 복제됐다는 뜻은 아니다. 임시 폴더 밖의 파일에는 영향을 주지 않는다.
출력
----
same file: True
links: 2준비되지 않은 파이프를 읽고 다시 기다린다
import os
import selectors
reader, writer = os.pipe()
try:
os.set_blocking(reader, False)
try:
os.read(reader, 10)
except BlockingIOError:
print('not ready')
with selectors.DefaultSelector() as selector:
selector.register(reader, selectors.EVENT_READ)
os.write(writer, b'go')
ready = selector.select(timeout=1)
print('ready:', bool(ready))
if ready:
print(os.read(reader, 10).decode())
finally:
os.close(reader)
os.close(writer)파이프 준비 상태를 지원하는 리눅스와 macOS용이다. 쓰기 끝을 열어 둔 빈 파이프에서 첫 읽기는 아직 데이터가 없다는 뜻이다. 두 바이트를 넣은 뒤 준비 알림과 실제 읽기를 별도로 수행한다. 준비 알림은 애플리케이션 메시지 전체가 완성됐다는 보장이 아니다.
출력
----
not ready
ready: True
go관찰 판단
---------
flush 성공 사용자 공간 버퍼를 아래로 전달
fsync 성공 파일 동기화 요청이 성공 반환
다시 읽기 성공 현재 읽기 경로에서 내용 확인
전원 장애 보존 이 실습으로 판정하지 않음환경별 차이
| 환경 | 이 장의 적용 | 추가로 확인할 점 |
|---|---|---|
| 리눅스 | inode·페이지 캐시·파이프 실습 적용 | 파일 시스템과 마운트 종류 |
| macOS | 임시 파일·하드 링크·파이프 실습 적용 | 동기화의 장치 보장과 파일 시스템 문서 |
| 컨테이너 | 호스트 커널의 파일 I/O 사용 | 볼륨 위치, 쓰기 가능 계층, 저장소 제한 |
컨테이너 내부의 파일 경로와 실제 저장 장치의 위치는 같지 않을 수 있다. 볼륨으로 연결한 경로와 컨테이너 수명에 묶인 경로를 먼저 나누면 재시작 뒤 사라진 파일을 잘못 진단하지 않는다. 네트워크 파일 시스템에서는 원격 서버의 저장 약속도 함께 고려해야 한다. 로컬 임시 파일 성공 결과를 다른 저장소의 내구성 증거로 옮겨 적지 않는다.
파일 생성 실패에서는 데이터 블록 여유 외에 inode 여유, 사용자 할당량, 권한과 읽기 전용 상태를 따로 본다. 파일 시스템 종류에 따라 inode 관리 방식이 다르므로 모두 같은 고정 개수라고 설명하지 않는다. 작은 파일을 많이 만든 뒤에만 실패한다면 개수에 연결된 제한을 먼저 좁힐 수 있다. 오류 문자열을 보존하는 편이 단순히 용량을 늘리는 것보다 원인을 찾기 쉽다.
실무에서 자주 틀리는 것
1. 로그가 보이면 저장이 끝났다고 판단한다
증상은 프로세스 종료나 호스트 장애 뒤 마지막 로그가 없는 것이다. 흔한 오진은 읽기 성공을 근거로 저장 장치 불량만 의심하는 것이다. 실제로는 사용자 버퍼에 남았는지, 커널까지만 전달됐는지, 동기화 오류를 무시했는지부터 갈린다. 확인은 flush와 동기화 호출의 위치, 성공 반환, 장애 범위를 같은 시간선에 놓고 한다. 매 줄 동기화가 필요한 기록과 일부 손실을 허용하는 기록을 구분하면 성능 비용도 설명할 수 있다.
2. 디스크 용량 하나로 파일 생성 가능 여부를 결정한다
증상은 남은 용량이 보이는데 파일 생성이 실패하는 것이다. 흔한 오진은 공간 지표가 틀렸다고 생각하는 것이다. 실제 원인은 파일 개수 관련 자원, 할당량, 권한, 마운트 상태 중 하나일 수 있다. 확인은 실패한 경로가 어느 파일 시스템에 속하는지와 반환 오류를 함께 보는 것이다. 다른 경로에서 파일 하나를 만들 수 있다는 사실은 문제 경로의 정상 증거가 아니다.
3. 논블로킹이면 자동으로 처리량이 높아진다고 생각한다
증상은 연결 수를 늘린 뒤 CPU 사용량과 지연이 함께 늘어나는 것이다. 흔한 오진은 커널이 논블로킹을 제대로 지원하지 않는다는 것이다. 실제로는 준비되지 않은 대상을 쉬지 않고 재시도하거나 준비된 한 대상에서 너무 긴 일을 하고 있을 수 있다. 확인은 반복 횟수, 준비된 대상 수, 한 번의 처리 시간을 나눠 기록하는 것이다. 다중화가 줄여 주는 것은 대기 대상 관리 비용이며 모든 계산 비용이 사라지는 것은 아니다.
스스로 확인하기
- 실습의 링크 수가 2이면 파일 내용이 두 벌이라는 뜻인가. 확인할 식별값을 설명하라.
- flush 뒤에는 줄이 보였지만 호스트 장애 뒤에는 사라졌다. 읽기 성공만으로 내구성을 판정할 수 없는 이유를 설명하라.
- 데이터가 아직 없는 파이프에서 논블로킹 읽기가 실패했다. 즉시 무한 반복하는 방식의 비용과 대안을 설명하라.
해설 1. 이름 연결이 두 개다. 같은 파일 시스템인지 st_dev를 보고 inode 번호까지 함께 비교한다. 내용 복제 여부를 경로 개수로 판정하지 않는다.
해설 2. 현재 읽기 경로는 캐시에 있는 데이터를 돌려줄 수 있다. 애플리케이션 버퍼, 커널 전달, 동기화 성공과 장애 종류를 추가로 확인해야 한다.
해설 3. 반복 횟수만큼 CPU를 소비하며 다른 작업의 기회를 줄인다. 준비 상태 다중화로 기다린 뒤 실제 읽기를 시도하고 남은 상태를 관리한다.
출처·작성 안내: 본문 설명, 예제, 문제와 도식은 이 장을 위해 직접 작성했다. 아래 문서는 기술적 사실 확인에 참고했으며 원문 문장·표·그림·코드를 발췌하거나 번역하지 않았다.
참고: Linux man-pages fsync(2), Linux man-pages inode(7), Python selectors 문서
다음 10장에서는 이 I/O의 상대가 다른 컴퓨터일 때 데이터에 붙는 주소와 경로를 따라간다.
READER FEEDBACK
질문·오탈자·의견
내용에 관한 질문이나 오탈자, 더 나은 설명을 위한 의견을 남겨 주세요. 이 댓글은 원래 게시글과 같은 자리에 쌓입니다.
댓글 0
아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.