리눅스 커널의 이해
12

ch16. accessing files

linuxfilesystemkernel
ACCESSING FILES · CH.16
LECTURE NOTE — LINUX KERNEL, CHAPTER 16

파일 접근: 커널 전체가
유기적으로 맞물리는 과정

단순해 보이는 read(fd, buf, 100) 한 줄이 실행될 때, 커널 내부에서는 VFS, 페이지 캐시, 블록 계층, 메모리 매핑, 비동기 I/O가 복합적으로 상호작용합니다. 이 노트에서는 데이터가 디스크에서 사용자 공간으로, 혹은 그 반대로 이동하는 다섯 가지 접근 경로를 심도 있게 추적합니다.

사용자 공간
페이지 캐시
블록 계층
디스크
canonical / O_SYNC read·write mmap · writeback O_DIRECT · AIO
01

큰 그림: 커널의 핵심 서브시스템이 총동원되는 작업FIVE ACCESS MODES

read(fd, buf, 100) 시스템 콜을 호출하면, 커널은 정말 단순히 '디스크에서 100바이트를 읽어오는' 작업만 수행할까요?

그렇지 않습니다. 파일 접근은 VFS부터 블록 디바이스 계층, 페이지 캐시, 메모리 매핑, Direct I/O, 비동기 I/O에 이르기까지 커널의 주요 시스템이 결합된 거대한 데이터 흐름입니다. 이 장에서 다루는 '파일'의 범주에는 /home/a.txt 같은 일반 파일(Regular File)뿐만 아니라 /dev/sda 같은 블록 디바이스 특수 파일도 포함됩니다. 우리의 출발점은 이미 VFS가 해당 파일의 readwrite 메서드를 호출한 직후입니다.

파일 접근 방식의 다섯 경로
CANONICAL read() / write() — 기본 동기화 I/O

read()는 데이터가 사용자 공간으로 온전히 복사될 때까지 대기합니다. 반면, write()는 데이터를 페이지 캐시에 복사하고 '더티(Dirty)' 상태로 마킹만 한 뒤 즉시 반환할 수 있습니다.

SYNCHRONOUS O_SYNC / fcntl()

write() 호출 시, 데이터를 페이지 캐시에 '접수'하는 데 그치지 않고, 실제 디스크 장치에 '기록 완료'될 때까지 스레드를 대기 상태로 유지하는 플래그입니다.

MMAP mmap() — 메모리 매핑

파일의 특정 영역을 프로세스의 가상 주소 공간에 매핑합니다. 시스템 콜 대신 포인터 연산으로 접근하며, 실제 물리 메모리 적재는 페이지 폴트(Page Fault)를 통해 지연 처리(Lazy-loading)됩니다.

DIRECT O_DIRECT — 페이지 캐시 우회

내부적으로 자체 버퍼 캐시를 운영하는 RDBMS 등에서 활용되며, 커널의 페이지 캐시를 건너뛰고 사용자 공간 버퍼와 디스크 간에 데이터를 직접 전송(DMA)합니다.

ASYNC AIO — 비동기 I/O

I/O 요청만 커널에 전달한 뒤 호출 스레드는 블로킹 없이 즉시 반환됩니다. 이후 콜백이나 이벤트 폴링을 통해 작업 완료 여부를 확인하는 고성능 I/O 모델입니다.

전체 과정을 관통하는 가장 중요한 질문은 단 하나입니다. "이 데이터는 현재 사용자 버퍼 공간에 있는가, 커널의 페이지 캐시에 머물러 있는가, 아니면 디스크에 물리적으로 기록되었는가?"

단순히 write() 콜이 성공을 반환했다고 해서, 갑작스러운 전원 차단 시 데이터 유실이 없다고 장담할 수 없습니다. 데이터의 현재 체류 계층을 정확히 파악하는 것이 파일 I/O 성능 병목 현상과 데이터 내구성(Durability) 이슈를 해결하는 핵심 열쇠입니다.

핵심 O/X 퀴즈

1. 이 장에서 다루는 "파일"에는 일반 파일뿐 아니라 블록 디바이스 파일도 포함된다.
O/dev/sda 같은 블록 디바이스 파일도 범위 안에 포함됩니다.
2. 일반적인 write()는 항상 데이터가 실제 디스크에 기록된 뒤에 반환한다.
X기본 write()는 메모리 상의 페이지 캐시에 복사하고 더티 마킹을 한 뒤 지연 쓰기(Delayed Write)를 위해 반환할 수 있습니다.
3. O_DIRECT는 페이지 캐시를 우회하려는 접근 방식이다.
O불필요한 메모리 복사를 줄이기 위해 사용자 버퍼와 디스크 사이에서 직접 전송을 수행합니다.
02

일반적인 읽기 흐름: generic_file_read()에서 시작되는 데이터 탐색READ ENTRY POINT

사용자가 read()를 호출하면 VFS 계층을 거쳐 해당 파일 객체에 매핑된 특정 파일시스템의 read 메서드가 호출됩니다. 디스크 기반 파일시스템 대부분과 블록 디바이스 파일의 읽기 메서드는 공통 인터페이스인 generic_file_read()를 통해 구현됩니다.

커널의 파일 읽기 작업은 사용자 관점에서는 바이트(Byte) 단위로 보이지만, 내부적으로는 항상 페이지(Page, 보통 4KB) 단위로 처리됩니다. 사용자가 단 10바이트만 요청하더라도, 해당 데이터가 메모리에 없다면 커널은 데이터가 포함된 4KB 페이지 전체를 디스크에서 읽어옵니다. 도서관에서 문장 하나만 필요하더라도 책의 해당 페이지 전체를 복사기 위에 올려두고 필요한 부분만 발췌하는 것과 같은 이치입니다.

generic_file_read() — 주요 인자 구조
filp
접근할 파일 객체를 가리키는 포인터. 파일의 현재 상태와 메타데이터를 포함합니다.
buf
데이터를 수신할 사용자 공간(User Space)의 버퍼 주소입니다.
count
사용자가 요청한 읽기 바이트 수입니다.
ppos
파일 내 읽기 시작 오프셋 위치. 완료 시 파일 객체의 현재 위치(f_pos)를 갱신하는 데 사용됩니다.

이 함수는 우선 두 가지 중요한 I/O 설명자를 준비합니다. iovec은 사용자 버퍼의 베이스 주소와 길이를 명시하는 벡터 구조체로, 복사 대상 목적지를 정의합니다. kiocb는 I/O 작업의 컨텍스트(동기/비동기 여부 등)를 추적하는 제어 블록 구조체입니다. 일반적인 동기 읽기의 경우 init_sync_kiocb 매크로를 통해 초기화됩니다.

준비가 끝나면 코어 함수인 __generic_file_aio_read()를 호출합니다. 명칭에 aio가 포함되어 있으나 동기화 및 비동기 읽기 경로 모두에서 공통으로 사용되는 핵심 함수입니다. 진입 시 가장 먼저 access_ok() 매크로를 실행하여 전달받은 사용자 버퍼 주소가 메모리 보호 구역을 침범하지 않는지 검증합니다. 잘못된 주소 공간 접근 시 시스템 크래시를 방지하기 위해 즉시 -EFAULT 에러를 반환합니다.

커널 입장에서 바라본 read()의 생명주기: 사용자 주소 공간 검증 → I/O 상태 제어 블록 초기화 → 페이지 캐시 히트 여부 확인 → 디스크 I/O를 통한 페이지 확보(미스 시) → 커널 공간에서 사용자 공간으로의 메모리 복사. 사용자에게는 단순한 한 줄의 시스템 콜이지만, 커널 내부에서는 정교하게 설계된 파이프라인을 거칩니다.

핵심 O/X 퀴즈

1. generic_file_read()는 사용자 버퍼의 주소와 길이를 iovec 형태로 정리한다.
Oiovec 구조체가 타겟 메모리의 주소와 범위를 나타내는 디스크립터 역할을 합니다.
2. 커널은 사용자가 요청한 바이트 수가 작으면 반드시 그 바이트 수만큼만 디스크에서 읽는다.
X페이지 캐시 아키텍처로 인해, 1바이트 요청도 최소 단위인 1개의 페이지(4KB) I/O를 유발할 수 있습니다.
3. access_ok()는 사용자 공간 버퍼가 유효한지 확인하는 단계다.
O보안 및 안정성을 위해 잘못된 주소 공간일 경우 즉각 -EFAULT를 반환합니다.
03

페이지 캐시 탐색: 히트(Hit)하면 복사하고, 미스(Miss)하면 읽어온다PAGE CACHE LOOKUP

do_generic_file_read()는 실질적인 데이터 읽기 로직이 구동되는 심장부입니다. VFS로부터 "특정 파일의 오프셋부터 N바이트를 복사하라"는 요청을 받으면, 이 함수는 우선 메모리의 페이지 캐시를 탐색하고, 캐시 미스 시 디스크 스케줄러에 I/O를 요청하며, 최종적으로 사용자 영역으로 데이터를 복사(Copy-to-User)합니다.

1
address_space 획득. 파일 객체 구조체의 f_mapping 포인터를 통해 해당 파일의 페이지 캐시 트리를 관리하는 address_space 객체를 가져옵니다. 일반 파일은 해당 파일의 inode가, 블록 디바이스는 bdev 특수 파일시스템의 inode가 소유자가 됩니다.
2
오프셋의 페이지 인덱스 변환. 요청된 바이트 기반 위치(*ppos)를 커널의 관리 단위인 페이지 인덱스(index)와 페이지 내 오프셋(offset)으로 분리 연산합니다.
3
Read-ahead 휴리스틱 적용. 파일 접근 패턴이 순차적(Sequential)이라고 판단되면 page_cache_readahead()를 호출하여 이후에 요청될 페이지들을 디스크로부터 미리 캐싱합니다.
4
find_get_page() 기반 캐시 탐색. 요구된 페이지가 캐시에 존재하고 최신 상태(PG_uptodate)라면 디스크 접근 없이 즉시 복사 단계로 넘어갑니다. 부재 시 물리 페이지 프레임을 새로 할당받아 캐시 트리에 삽입(add_to_page_cache())한 후, readpage 콜백을 통해 실제 디스크 읽기를 유발합니다.
5
file_read_actor() 실행. 타겟 페이지가 하이 메모리 영역에 있다면 kmap()으로 임시 매핑을 생성하고, __copy_to_user() 매크로를 이용해 사용자 공간으로 실제 데이터를 전송한 뒤 kunmap()으로 해제합니다. 마지막으로 파일 포인터를 전진시키고 접근 시간(atime)을 갱신합니다.

단순한 관점에서는 read()가 디스크와 사용자 간의 직접 통신처럼 보이지만, 운영체제 설계상 거의 항상 디스크 스토리지 → 커널 페이지 캐시 → 사용자 가상 메모리 공간이라는 다단계 복사 경로를 거칩니다. 요구된 페이지가 캐시에 이미 존재하는 경우(Cache Hit) 디스크 I/O 단계를 생략할 수 있으며, 이 차이가 애플리케이션 I/O 성능의 절대적인 지표가 됩니다.

작업 완료 전 mark_page_accessed()가 호출되어 "이 페이지는 활발히 사용 중"이라는 시그널을 LRU(Least Recently Used) 페이지 회수 알고리즘에 전달합니다. 이를 통해 해당 캐시 라인이 조기에 스왑 아웃(Swap out)되거나 삭제되는 것을 방지합니다.

핵심 O/X 퀴즈

1. do_generic_file_read()는 파일 위치를 페이지 인덱스와 페이지 내부 오프셋으로 나누어 처리한다.
O커널의 메모리 관리 최소 단위가 페이지(Page) 단위이기 때문입니다.
2. 페이지가 페이지 캐시에 있고 PG_uptodate이면 반드시 디스크에서 다시 읽어야 한다.
X최신 상태임이 보장된 캐시 페이지는 값비싼 디스크 트랜잭션 없이 즉각 복사(Zero-Disk-I/O)가 가능합니다.
3. 최종적으로 사용자 버퍼에 복사할 때 __copy_to_user()가 사용된다.
O커널 링 보호 수준(Ring 0)에서 사용자 공간(Ring 3)으로 안전하게 데이터를 전송하는 전용 인터페이스입니다.
04

readpage: 일반 파일과 블록 디바이스의 상이한 디스크 접근 방식READPAGE INTERNALS

캐시 미스 상황에서 do_generic_file_read()가 위임하는 readpage 콜백은 실질적으로 디스크와 통신하는 레이어입니다. 여기서부터는 파일시스템이 관리하는 일반 파일과 하드웨어를 직접 추상화하는 블록 디바이스 파일의 처리 메커니즘이 확연히 갈라집니다.

일반 파일의 경우, readpage는 마운트된 특정 파일시스템(예: ext4, xfs)의 구현체로 연결됩니다. Ext 계열의 경우 내부적으로 mpage_readpage(page, ext3_get_block) 구조를 따릅니다. 여기서 핵심은 ext3_get_block과 같은 논리-물리 주소 변환 함수입니다. 파일 내부의 논리적인 "N번째 데이터 블록"이 실제 스토리지의 어느 물리적 LBA(Logical Block Address)에 매핑되어 있는지는 해당 파일시스템의 아이노드 구조와 블록 비트맵을 해석해야만 알 수 있기 때문입니다.

mpage_readpage() — 두 가지 최적화 경로
연속 블록 매핑 경로 (Contiguous)
메모리 페이지 내에 포함된 파일 블록들이 디스크 상에서도 물리적으로 연속된 섹터에 배치되어 있다면, 이를 단일 I/O 요청 블록(bio)으로 병합하여 submit_bio()를 호출합니다. 디스크 탐색(Seek) 비용을 극단적으로 줄이는 최적화입니다.
비연속 블록 경로 (Fragmented)
파일의 데이터 블록들이 디스크 곳곳에 파편화되어 있거나 중간에 빈 공간(File Hole)이 존재하는 경우, block_read_full_page()를 통해 각 블록을 개별 버퍼 헤드(Buffer Head) 단위로 분리하여 하나씩 스케줄링합니다.

반면, 블록 디바이스 파일(`/dev/sda` 등)의 readpage는 파일시스템 계층을 우회합니다. 범용 함수인 blkdev_readpage()가 호출되며, 내부에서는 block_read_full_page(page, blkdev_get_block) 구조를 사용합니다. 블록 디바이스 환경에서는 가상 파일의 오프셋 자체가 장치의 물리적 섹터 위치와 1:1로 직결되므로, 주소 변환을 담당하는 blkdev_get_block의 로직은 거의 항등 함수(Identity Function)에 가깝게 단순합니다.

일반 파일 시스템은 복잡하게 암호화된 "파일 논리 주소 → 디스크 물리 주소 변환 사전"을 지속적으로 조회해야 합니다. 하지만 원시(Raw) 블록 디바이스 파일은 그 번역 과정이 생략된 투명한 거울과 같아서, 논리적 요청 주소를 그대로 장치의 물리적 주소로 하향 전달합니다.

비동기 I/O 컨트롤러에 의해 디스크 읽기가 완료되면 하드웨어 인터럽트가 발생하고, 완료 콜백인 mpage_end_io_read()가 실행됩니다. 이 함수는 방금 채워진 메모리 페이지의 플래그를 PG_uptodate로 변경하고 페이지 락(Lock)을 해제합니다. 이 신호에 의해 슬립(Sleep) 상태로 대기 중이던 읽기 프로세스의 스케줄링 큐가 깨어나 사용자 복사 로직을 재개하게 됩니다.

핵심 O/X 퀴즈

1. 일반 파일의 readpage는 파일시스템별 get_block 함수를 이용해 파일 블록을 디스크 블록으로 번역한다.
O파일시스템의 고유한 메타데이터(예: inode의 extent 트리)를 순회해야만 정확한 섹터 위치를 산출할 수 있습니다.
2. 블록 디바이스 파일에서는 파일 블록 번호와 장치 논리 블록 번호가 완전히 무관하다.
X블록 디바이스 파일에서는 파일 내 오프셋이 곧 물리 스토리지의 블록 어드레스를 직접적으로 의미합니다.
3. mpage_readpage()는 블록들이 연속적이면 하나의 bio로 묶어 읽을 수 있다.
O연속 배치된 물리적 블록들을 하나의 거대한 I/O 페이로드로 묶어 처리량(Throughput)을 극대화합니다.
05

Read-ahead: 미리 읽기는 어떻게 I/O 지연을 숨기는가PREDICTIVE PREFETCH

비디오 재생이나 대용량 로그 분석처럼 애플리케이션이 파일의 첫 오프셋부터 마지막 오프셋까지 선형적으로 스캔하고 있다면, 운영체제는 다음 번에 요구될 페이지 번호를 높은 확률로 예측할 수 있습니다. 커널은 이 공간적 지역성(Spatial Locality) 패턴을 감지하여 애플리케이션이 요청하기도 전에 디스크에서 미리 데이터를 끌어올리는데, 이 메커니즘이 바로 Read-ahead(미리 읽기)입니다.

기계식 HDD든 현대식 NVMe SSD든, 스토리지 컨트롤러는 작은 블록을 잘게 쪼개서 요청받는 것보다 인접한 대량의 블록을 하나의 명령으로 일괄 처리할 때 훨씬 뛰어난 대역폭을 발휘합니다. 미리 읽혀진 데이터는 페이지 캐시에 대기하다가, 프로세스가 해당 오프셋을 요청하는 즉시 I/O 블로킹 딜레이 없이 0밀리초에 가깝게 반환됩니다. 하지만 무작위 접근(Random Access) 워크로드에서 과도한 Read-ahead는 아까운 I/O 대역폭을 낭비하고 유용한 기존 캐시 페이지를 축출(Eviction)시키는 부작용(Cache Thrashing)을 초래합니다.

file_ra_state — Read-ahead 상태 머신 구조체
start / size
현재 프로세스가 읽고 있는 범위인 'Current Window'의 시작 페이지 인덱스와 길이. 이미 페이지 캐시에 적재가 확정된 구간입니다.
prev_page
바로 직전 read() 콜에서 요청했던 페이지 번호. 순차성(Sequentiality) 여부를 판별하는 핵심 기준값이 됩니다.
ahead_start / ahead_size
Current Window를 뒤따르는 'Ahead Window'의 좌표. 아직 유저 레벨의 요청은 없었지만, 커널이 선제적으로 비동기 I/O를 발생시킨 예비 버퍼 영역입니다.
ra_pages
해당 파일이나 디바이스에서 허용 가능한 Read-ahead 윈도우의 최대 상한값입니다.

연속된 레일을 달리는 기차의 앞 유리에 두 개의 슬라이딩 윈도우를 설치했다고 상상해 보십시오. 첫 번째 창(Current window)은 현재 가시권에 들어온 풍경이고, 두 번째 창(Ahead window)은 기차가 도달하기 전에 미리 망원경으로 스캔을 예약해 둔 다가올 풍경입니다. 기차가 전진하여 Ahead window 영역에 진입하는 순간, 그 창은 즉각 새로운 Current window로 전환되며 시스템은 지체 없이 그 너머의 새로운 Ahead window를 설정합니다.

순차 접근 패턴의 판별 기준은 매우 명확합니다. 이번에 요청된 파일 오프셋의 첫 번째 페이지가 prev_page에 기록된 직전 접근 페이지 번호와 인접해 있다면 커널은 순차 스캔 상태로 확정합니다. 순차 히트가 연속해서 발생하면 시스템은 확신을 가지고 Ahead window의 프리페치(Prefetch) 사이즈를 기하급수적으로 확장합니다. 반대로 오프셋이 점프하는 랜덤 패턴이 감지되면, 두 윈도우의 상태를 리셋하고 무의미한 Read-ahead 작동을 즉각 정지시킵니다.

미리 읽기(Read-ahead)는 맹목적인 예측이 아니라, 프로그램의 I/O 패턴에 맞춘 적응형(Adaptive) 최적화 알고리즘입니다. 소프트웨어 엔지니어는 posix_fadvise(fd, offset, len, POSIX_FADV_SEQUENTIAL) 또는 POSIX_FADV_RANDOM 시스템 콜을 통해 커널의 휴리스틱 엔진에 명시적인 힌트를 주입하여 시스템 성능을 극대화할 수 있습니다.

핵심 O/X 퀴즈

1. read-ahead는 순차 파일 접근에서 성능을 높이기 위해 아직 요청되지 않은 페이지를 미리 읽는다.
O공간적 지역성을 활용해 향후 발생할 디스크 I/O 대기 시간을 제로(0)로 수렴시킵니다.
2. 랜덤 접근에서도 read-ahead는 항상 성능을 높인다.
X무작위 접근 시에는 불필요한 I/O 대역폭 낭비와 캐시 폴루션(Cache Pollution)을 유발하므로 오히려 시스템 전반의 성능을 저하시킬 수 있습니다.
3. ahead window의 페이지가 실제로 요청되면 그 ahead window가 새 current window가 될 수 있다.
O파이프라인이 전진함에 따라 예비 구역이 활성 구역으로 매끄럽게 교대되며 연속적인 프리페치 루프를 형성합니다.
06

일반적인 쓰기 흐름: 페이지 캐시 내 '더티 페이지(Dirty Page)' 생성WRITE ENTRY POINT

읽기 오퍼레이션에서의 성능 핵심이 "목표 페이지가 RAM 캐시에 적재되어 있는가?"였다면, 쓰기 연산에서의 핵심 화두는 "사용자가 전달한 데이터 페이로드가 커널 메모리에 안정적으로 안착되어 더티(Dirty) 상태로 전이되었는가?"로 귀결됩니다.

호출자가 write(fd, buf, count)를 실행할 때, 커널은 사용자 공간의 버퍼 내용을 해당 파일의 논리적 오프셋에 매핑되는 페이지 캐시 영역으로 복사합니다. 이 복사가 완료된 직후 해당 메모리 페이지에는 '더티(Dirty)' 플래그가 세팅됩니다. 더티 상태란 "RAM 상의 캐시 데이터가 수정되어 스토리지에 저장된 원본 데이터와 불일치하며, 향후 반드시 디스크로 동기화(Flush)되어야 하는 휘발성 상태"임을 의미합니다.

비유하자면, 수첩에 급하게 중요한 아이디어를 메모해 두었지만 아직 팀의 공식 공유 문서(디스크)에 정서하지 않은 상태와 같습니다. 데이터 자체는 존재하지만 시스템 장애 시 영구적인 보존이 보장되지는 않습니다. 우체국에 택배를 맡겼다고 해서 수취인에게 배달이 완료된 것이 아니듯, 표준 write() 콜은 커널 페이지 캐시라는 '접수처'에 데이터를 넘겨주는 순간 블로킹을 해제하고 즉시 리턴할 수 있습니다.

Ext4와 같은 주류 디스크 기반 파일시스템은 파일 객체의 쓰기 인터페이스를 generic_file_write() 계열 함수로 추상화합니다. 이 함수의 초기 진입점에서는 대상 파일의 inode 동기화 메커니즘인 i_sem (또는 i_mutex) 뮤텍스를 획득합니다. 만약 다중 프로세스가 동일한 파일에 동시다발적으로 쓰기를 시도할 경우, 논리적 파일 크기 증분 연산, 캐시 트리 구조 변경, 물리 스토리지 블록 할당 로직 등에서 치명적인 Race Condition이 발생할 수 있으므로, 쓰기 경로의 코어 섹션은 철저하게 직렬화(Serialization)되어 보호됩니다.

강제 동기화(Synchronous Write) 트리거 조건
O_SYNC 파일 오픈 플래그
파일을 열 때 O_SYNC가 지정되었다면, write() 시스템 콜은 더티 마킹에서 멈추지 않고 물리적 디스크 섹터에 데이터 및 메타데이터가 완벽히 기록될 때까지 스레드를 블로킹시킵니다.
S_SYNC (inode 플래그)
파일시스템 레벨에서 특정 inodeS_SYNC 속성이 부여된 경우, 접근하는 모든 프로세스의 쓰기 연산이 동기화 모드로 격상됩니다.
MS_SYNCHRONOUS
마운트 옵션 자체(예: mount -o sync)로 인해 해당 파티션 전체의 쓰기 정책이 엄격한 동기화 모드로 고정됩니다.

위와 같은 명시적 동기화 제약이 없는 한, 리눅스의 기본 쓰기 동작인 지연 할당(Delayed Allocation) 및 비동기적 쓰기(Asynchronous Write-behind)가 작동합니다. 이 비동기적 아키텍처 덕분에 I/O 병목이 메모리 대역폭 수준으로 은닉되지만, DB 트랜잭션 로그와 같이 데이터의 원자성(Atomicity)과 내구성(Durability)이 생명인 워크로드에서는 개발자가 fsync(), fdatasync()를 명시적으로 호출하여 메모리 장벽(Memory Barrier)을 세워야만 데이터 유실을 방지할 수 있습니다.

핵심 O/X 퀴즈

1. 일반적인 write()는 사용자 데이터를 페이지 캐시에 복사하고 더티로 표시한 뒤 반환할 수 있다.
O이러한 지연 쓰기(Delayed Write) 철학이 운영체제 성능 최적화의 근간입니다. 실제 디스크 기록은 백그라운드 스레드가 담당합니다.
2. inode->i_sem은 같은 파일에 대한 쓰기 작업의 동시 실행을 제어하는 데 사용된다.
O동시 쓰기 시 파일 메타데이터(크기, 블록 매핑 등)가 오염되는 것을 막기 위한 동기화 락입니다.
3. O_SYNC가 설정되어 있어도 write()는 절대 디스크 I/O 완료를 기다리지 않는다.
XO_SYNC는 성능을 희생하더라도 데이터 보존을 절대적으로 보장하기 위해 하드웨어 디스크 트랜잭션 완료 시점까지 호출 프로세스를 대기 상태(D-state)로 만듭니다.
07

__generic_file_aio_write_nolock(): 사용자 데이터가 커널 영역으로 편입되는 순간WRITE INNER LOOP

사용자가 제공한 원시 버퍼의 바이트 스트림이 구체적으로 어떤 단계를 거쳐 커널의 캐시 트리 내 '더티 페이지' 구조로 스며드는 것일까요? 코어 루프 함수인 __generic_file_aio_write_nolock()의 치밀한 동작 파이프라인에서 그 해답을 찾을 수 있습니다.

1
포인터 유효성 검사 (access_ok). 유저스페이스가 전달한 버퍼 포인터가 커널 영역을 침범하지 않고 정상적으로 접근 가능한 메모리 범위인지 엄격히 검사합니다. 위반 시 즉각 -EFAULT를 발생시킵니다.
2
O_APPEND 시맨틱 처리. 파일 디스크립터가 O_APPEND(이어쓰기) 모드로 열려 있다면, 사용자가 명시한 오프셋을 완전히 무시하고 커널이 락을 보유한 상태에서 파일의 i_size(최종 끝점)를 쓰기 시작 지점으로 강제 재설정합니다.
3
하드 리미트 및 쿼터 검증. 프로세스의 리소스 제한(RLIMIT_FSIZE), 파일시스템 최대 지원 크기(s_maxbytes)를 초과하는지 평가합니다. 한계에 도달하면 SIGXFSZ 시그널을 보내거나 쓸 수 있는 바이트 수를 강제로 절삭(Truncate)합니다.
4
Setuid 보안 권한 박탈. 타겟 파일에 SUID(Set-User-ID) 비트가 설정되어 있다면, 파일 내용의 변조가 발생한 즉시 커널은 권한 상승 보안 취약점을 차단하기 위해 이 특수 권한 비트를 자동으로 소멸시킵니다.
5
페이지 청크 단위 순회 연산. 전체 쓰기 범위를 4KB 단위로 분할하여 반복합니다. find_lock_page()로 캐시 존재 여부를 확인하고, prepare_write로 블록 디바이스 할당을 준비한 뒤, __copy_from_user()를 통해 컨텍스트 복사를 수행합니다. 최종적으로 commit_write 호출로 더티 상태를 확정 짓습니다.
6
백프레셔(Back-pressure)와 더티 임계치 검사. 시스템 전체 RAM 대비 더티 페이지의 점유율이 dirty_ratio 임계값을 돌파하면, 더 이상의 메모리 고갈을 막기 위해 writeback_inodes()를 동기적으로 호출하여 디스크로 강제 플러시(Flush)합니다.

데이터 복사의 방향성에 주목해야 합니다. 읽기 연산에서는 __copy_to_user() 매크로를 이용해 커널 버퍼 → 사용자 공간으로 데이터가 흘러가지만, 쓰기 연산에서는 철저히 __copy_from_user()를 통해 사용자 메모리 → 커널 페이지 공간으로 주입됩니다. 성공적으로 루프를 통과하면 파일의 데이터 수정 시각(mtime)과 메타데이터 변경 시각(ctime) 속성이 모두 최신화됩니다.

핵심 O/X 퀴즈

1. O_APPEND가 설정된 일반 파일에서는 쓰기 시작 위치가 파일 끝으로 조정된다.
O여러 프로세스가 동시에 로그 파일을 작성하더라도 락 메커니즘 하에서 항상 파일 끝부분에 원자적으로 추가됨을 보장합니다.
2. 쓰기 경로에서는 사용자 버퍼에서 커널 페이지로 복사하기 위해 __copy_from_user()가 사용된다.
ORing 3의 유저 메모리 영역을 Ring 0의 안전한 커널 캐시 페이지로 복제해오는 역방향 메모리 연산입니다.
3. 더티 페이지가 무한정 많아져도 커널은 애플리케이션의 write() 속도를 제한하지 않는다.
X시스템 메모리 고갈을 방지하기 위해, 더티 페이지 비율이 일정 임계치를 넘으면 애플리케이션의 write() 콜 자체가 디스크 플러시가 완료될 때까지 강제로 지연(Throttling)됩니다.
08

prepare_write와 commit_write: 스토리지 블록 매핑 및 더티 상태 확정PREPARE & COMMIT

페이지 복사 루프 내에 존재하는 prepare_write 단계는 "스토리지의 물리적 공간을 조달하는 준비 과정"이며, 이어서 실행되는 commit_write는 "메모리 상의 수정 내역이 유효한 더티 상태임을 시스템에 공표하는 확정 과정"입니다.

일반 파일 시스템(예: Ext2/3)에서 prepare_write는 스토리지 아키텍처에 종속적인 래퍼 함수로 라우팅됩니다. 내부의 block_prepare_write(page, from, to, ext2_get_block) 계열 함수는 매우 무거운 임무를 수행합니다. 새로 기록될 데이터가 파일의 기존 크기를 초과하거나 중간의 희소 영역(File Hole)을 덮어쓰게 될 경우, 커널의 블록 할당자(Block Allocator)를 호출하여 가용 디스크 섹터에서 새로운 물리적 블록을 배정받아야 하기 때문입니다.

block_prepare_write() — 세부 동작 메커니즘
버퍼 헤드(Buffer Head) 인스턴스화
페이지에 매핑된 버퍼 헤드가 존재하지 않는다면 create_empty_buffers()로 파일시스템 블록 사이즈(예: 1KB) 단위로 버퍼 디스크립터를 생성합니다. 4KB 페이지라면 4개의 버퍼 헤드가 체인 형태로 엮이게 됩니다.
물리 블록 주소 해석 (get_block)
버퍼가 아직 물리적 디스크 LBA(Logical Block Address)와 연결되지 않았다면, 파일시스템 전용 get_block 콜백을 실행하여 아이노드 맵을 순회한 뒤 BH_Mapped 플래그를 세팅하여 연결을 확립합니다.
부분 쓰기 시 기존 블록 인출
쓰기 연산이 블록 전체 영역을 덮어쓰지 않고 부분 수정만 발생시키는 경우, 수정되지 않는 나머지 바이트들을 보존하기 위해 ll_rw_block()을 호출하여 디스크로부터 기존 원본 데이터를 선행하여 읽어옵니다.

부분 쓰기(Partial Write) 상황은 디스크의 블록 단위(예: 4KB) 중 일부(예: 1KB)만 수정해야 할 때 발생합니다. 스토리지 장비는 물리적으로 블록 전체를 한 번에 기록해야 하므로, 메모리에서 해당 블록 전체를 재구성하기 위해 기존 블록의 데이터를 먼저 읽어온 뒤 필요한 1KB 부분만 덮어쓰는 Read-Modify-Write 동작이 필수적으로 유발됩니다.

데이터 복사가 성공적으로 마무리되면 트랜잭션의 마무리를 알리는 commit_write가 호출됩니다. generic_commit_write() 코어 함수는 타겟 범위의 버퍼 헤드들에 BH_Uptodate(최신 데이터 보유)와 BH_Dirty(디스크 동기화 필요) 상태 플래그를 각인합니다. 페이지를 구성하는 모든 하위 버퍼들이 최신 상태가 되면 상위 컨테이너인 페이지 자체에도 PG_uptodatePG_dirty 속성이 상속되며, 더 나아가 페이지 캐시의 척추인 Radix Tree(또는 XArray) 노드에 명시적인 더티 태그가 부착됩니다. 이 효율적인 트리 태깅 시스템 덕분에 향후 백그라운드 스레드가 전체 메모리를 뒤지지 않고도 플러시할 더티 페이지들을 O(1)에 가깝게 색인할 수 있습니다.

핵심 O/X 퀴즈

1. 부분 쓰기에서는 덮어쓰지 않는 부분을 보존하기 위해 기존 블록 내용을 먼저 읽어야 할 수 있다.
O블록 디바이스의 최소 단위 기록 제약 때문에, 변경되지 않는 기존 바이트를 훼손하지 않기 위한 Read-Modify-Write 현상이 prepare_write 단계에서 발생합니다.
2. commit_write는 버퍼를 더티로 표시하지 않고 항상 즉시 디스크에 쓴다.
Xcommit_write의 핵심 임무는 메타데이터와 상태 플래그를 '더티'로 갱신하여 커널에 변경 사실을 알리는 데 그치며, 물리적인 I/O 트랜잭션은 이후의 writeback 서브시스템에 위임됩니다.
3. 블록 디바이스 파일의 쓰기에서는 일반 파일처럼 파일 크기를 append로 확장하는 개념이 없다.
O블록 디바이스는 물리적 스토리지 매체의 파티션 크기가 고정되어 있으므로 논리적인 파일 크기 연장(Extending) 처리 로직이 존재할 수 없습니다.
09

Writeback: 메모리의 더티 페이지가 디스크 트랜잭션으로 진화하는 과정WRITEBACK PATH

결과적으로 비동기적 write() 콜이 생성해 낸 무수히 많은 더티 페이지들은 시스템의 어디선가 모여 물리적 스토리지로 기록되어야만 비로소 생명주기를 마감합니다. 이 중대한 책무를 수행하는 백그라운드 엔진이 바로 커널의 Writeback(플러셔 스레드) 경로입니다.

운영체제가 주기적으로, 혹은 메모리 압박에 의해 더티 페이지 축출을 결심하면 특정 파일 구조체의 address_space 객체에 등록된 writepages 메서드를 연쇄적으로 호출합니다. Ext 계열 파일시스템의 경우 내부 구현체인 mpage_writepages(mapping, wbc, ext2_get_block)로 진입합니다. 여기서 전달되는 파라미터 wbc(writeback_control) 구조체는 운영체제의 핵심 I/O 스케줄링 정책을 담고 있으며, 스캔할 페이지 인덱스 범위, 처리할 최대 페이지 할당량, 블로킹(Wait) 모드 여부 등의 전략적 지시를 포함합니다.

1
I/O 큐 혼잡도(Congestion) 평가. 타겟 블록 디바이스의 요청 큐가 이미 한계치에 달해 'write-congested' 상태이고, 프로세스가 비동기 논블로킹 모드를 요청했다면, 커널은 무리하게 스케줄링을 진행하지 않고 즉시 스킵합니다.
2
O(1) 기반 더티 트리 순회. 캐시 공간 전체를 선형 탐색하는 낭비를 피하기 위해, Radix Tree 각 계층에 기록된 더티 태그 비트맵을 따라가며 find_get_pages_tag()를 이용해 실제 더티 페이지들만 족집게처럼 신속하게 추출해냅니다.
3
진행 중(In-flight) 상태 필터링. 페이지 플래그 중 PG_writeback이 켜져 있다면 이미 디스크 컨트롤러에 전송되어 DMA 처리가 진행 중임을 의미합니다. 동기 모드인 경우 wait_on_page_bit()로 락이 풀리길 기다리지만, 일반적인 비동기 스캔 모드에서는 충돌을 피해 부드럽게 다음 페이지로 넘어갑니다.
4
mpage_writepage()를 통한 블록 병합. 디스크 오버헤드를 최소화하기 위해 논리적으로 인접하고 디스크 물리 블록 관점에서도 연속성이 증명된 여러 캐시 페이지들을 묶어 거대한 하나의 bio 페이로드로 조립합니다. 단편화가 심하면 개별 writepage 폴백 경로를 탑니다.
5
스토리지 컨트롤러 큐잉. 최적화된 bio 구조체가 완성되면 I/O 종료 시그널을 수신할 mpage_end_io_write() 콜백 포인터를 등록한 후, 엘리베이터 스케줄러 계층으로 submit_bio()를 호출하여 블록 계층으로 하달합니다. 기록 성공 시 인터럽트 핸들러가 해당 캐시의 더티 상태를 해제하고 대기하던 프로세스들을 일깨웁니다.

비유하자면 write() 콜은 사용자가 빨래통에 입었던 더러운 옷(더티 페이지)을 가볍게 던져 넣는 행위입니다. 반면 Writeback 프로세스는 커널의 백그라운드 세탁기가 스케줄에 맞춰 작동하여 쌓인 빨래통을 비우고 실질적인 세탁(디스크 기록)을 수행하는 과정입니다. 여기서 fsync()O_SYNC 플래그를 사용하는 것은, 사용자가 세탁기가 다 돌아가고 빨래가 건조되어 나올 때까지 세탁기 앞에서 한 발짝도 움직이지 않고 대기하는 엄격한 동기화 방식과 같습니다.

핵심 O/X 퀴즈

1. writeback은 페이지 캐시의 radix tree 태그를 이용해 더티 페이지를 찾을 수 있다.
O수 기가바이트의 메모리 풀 속에서도 더티 마킹된 트리의 경로만을 추적함으로써 검색 오버헤드를 극적으로 줄입니다.
2. mpage_writepage()는 어떤 경우에도 반드시 여러 페이지를 하나의 bio로 묶는다.
X물리적인 스토리지 블록이 파편화되어 연속성을 상실했거나 파일 중간에 빈 공간이 있다면 연속된 bio 병합에 실패하고 개별 처리 로직으로 우회하게 됩니다.
3. 이미 PG_writeback인 페이지는 디스크로 쓰는 작업이 진행 중인 페이지다.
O해당 플래그는 스토리지 하드웨어 인터페이스 단으로 명령이 하달되어 I/O 응답(Interrupt)을 기다리고 있는 In-flight 상태임을 명확히 지시합니다.
10

Memory Mapping: 파일 I/O를 메모리 포인터 접근으로 치환하다MMAP FUNDAMENTALS

전통적인 read(), write() 시스템 콜 인터페이스를 빈번하게 호출하는 대신, 디스크 상의 파일을 운영체제의 페이징 기법을 이용해 프로세스의 가상 주소 공간에 직접 매핑하는 우아한 해법이 존재합니다. 이렇게 되면 개발자는 마치 일반적인 C언어 배열이나 힙(Heap) 메모리를 다루듯 포인터 연산만으로 파일 내용을 수정하고 접근할 수 있으며, 커널과의 컨텍스트 스위칭 비용을 혁신적으로 절감할 수 있습니다. 이 매커니즘이 바로 Memory Mapping (mmap)입니다.

매핑 아키텍처의 두 가지 패러다임
MAP_SHARED (공유 매핑)
프로세스가 매핑된 메모리 주소에 기록한 수정 사항이 페이징 시스템을 거쳐 원본 디스크 파일에 직접적으로 전이됩니다. 또한, 동일한 파일을 공유 매핑한 다른 프로세스들과 변경된 페이지 뷰를 실시간으로 공유하게 되어 고성능 IPC(Inter-Process Communication)의 수단이 됩니다.
MAP_PRIVATE (전용 매핑)
원본 파일을 읽어들이는 목적으로 주로 사용됩니다. 만약 프로세스가 이 영역에 쓰기를 시도하면, 커널은 하드웨어 MMU의 예외를 캐치하여 해당 페이지의 독립적인 복제본을 생성하는 COW(Copy-On-Write) 기법을 발동시킵니다. 즉, 프로세스만의 격리된 사본이 생성되며 디스크의 원본 파일이나 다른 프로세스에게는 일체의 영향을 주지 않습니다.

커널에서 특정 프로세스의 메모리 매핑 구역을 표현하는 핵심 자료구조는 vm_area_struct (VMA)입니다. 이 구조체 내의 vm_file 포인터는 매핑의 소스가 되는 파일 객체를 가리키며, vm_pgoff는 원본 파일 내에서 매핑이 시작되는 페이지 오프셋 단위를 명시합니다. vm_startvm_end 쌍은 프로세스 가상 공간에 할당된 시작과 끝 주소 경계를 정의합니다.

중요한 오해 중 하나는 mmap() 시스템 콜이 리턴되는 순간 수 기가바이트의 파일 내용이 일시에 RAM으로 적재된다는 착각입니다. 커널은 mmap() 호출 시점에는 가상 주소 공간의 메타데이터(VMA)만 할당해 둘 뿐입니다. 실제 물리적 디스크 I/O와 메모리 할당은 프로세스의 CPU가 해당 가상 주소에 최초로 접근을 시도하여 하드웨어 페이지 폴트(Page Fault) 예외가 트리거되는 그 찰나의 순간에 지연 로딩(Lazy-loading) 방식으로 처리됩니다. 이 과정을 제어하는 VMA 오퍼레이션 콜백이 바로 nopage (또는 최신 커널의 fault) 함수군입니다.

공유 매핑(Shared)으로 불러들인 페이지 영역은 언제나 파일시스템의 페이지 캐시 생태계 내에 존재합니다. 전용 매핑(Private) 페이지들 또한 최초에는 원본 파일의 페이지 캐시를 가리키며 낭비를 줄이다가, 최초의 데이터 수정 명령이 하달되는 순간 Copy-On-Write 폴트 핸들러에 의해 새로운 익명(Anonymous) 페이지 프레임으로 분리 독립됩니다. 이 시점부터 분리된 복제 페이지는 파일시스템 캐시와 연을 끊고 프로세스의 독립적인 익명 메모리로 관리됩니다.

핵심 O/X 퀴즈

1. shared memory mapping에서 쓴 변경은 같은 파일을 공유 매핑한 다른 프로세스에 보일 수 있다.
O백업 파일의 페이지 캐시 프레임을 완전히 공유하므로 실시간 변경 사항을 투명하게 관찰할 수 있습니다.
2. private mapping에서 페이지를 수정하면 그 변경은 곧바로 디스크 파일에 기록된다.
X메모리 보호 예외를 통해 작동하는 Copy-On-Write 메커니즘에 의해 프로세스 소유의 독립적인 프레임으로 복제되며, 원본 파일의 불변성이 보장됩니다.
3. mmap() 직후 모든 파일 페이지가 즉시 메모리에 로드되는 것은 아니다.
ODemad Paging 철학에 입각하여 오로지 CPU가 명령어나 데이터를 인출(Fetch)하기 위해 접근하여 페이지 폴트가 발생할 때만 물리 메모리로 스왑-인 됩니다.
11

페이지 폴트 처리, msync(), 그리고 Non-linear 매핑의 기교FAULT & SYNC & REMAP

CPU가 mmap으로 획득한 가상 주소를 역참조하려 할 때, 해당 주소의 페이지 테이블 엔트리(PTE)가 물리 메모리에 매핑되어 있지 않으므로 MMU는 운영체제 커널로 제어권을 넘기는 하드웨어 트랩(Page Fault)을 발생시킵니다. 커널의 범용 폴트 핸들러는 예외 주소가 속한 VMA 영역을 찾아내고, 파일 매핑 구역임을 인지한 뒤 do_no_page() 서브루틴을 거쳐 파일시스템에 특화된 nopage (또는 fault) 콜백 메서드를 기동시킵니다.

가장 범용적인 파일 매핑 폴트 핸들러는 filemap_nopage()입니다. 이 루틴은 먼저 VMA의 vm_file 디스크립터에서 타겟 파일 객체를 식별합니다. 이후 폴트가 발생한 가상 주소와 VMA의 시작 주소(vm_start), 그리고 파일 시작 오프셋(vm_pgoff) 간의 차이를 연산하여 정확히 파일 안의 몇 번째 논리 페이지 블록을 가져와야 하는지 산출해냅니다. 계산된 인덱스를 바탕으로 find_get_page()를 통해 캐시를 조회하고, 부재중이라면 page_cache_read()를 촉발시켜 스토리지 컨트롤러로부터 데이터를 끌어올립니다.

msync(addr, length, flags) — 매핑 동기화 제어
MS_SYNC (동기식 보장)
가상 메모리 상에 변경된 부분(Dirty)을 찾아 스토리지에 기록하도록 지시하며, 물리적 디스크 I/O 트랜잭션이 완전히 끝날 때까지 시스템 콜이 반환되지 않고 블로킹됩니다.
MS_ASYNC (비동기 스케줄링)
변경된 페이지들에 더티 속성만을 각인하여 커널의 플러셔 스레드가 나중에 처리하도록 스케줄링 큐에 던져놓고 즉시 제어권을 반환합니다.
MS_INVALIDATE
해당 파일 영역을 캐싱하고 있는 페이지 뷰를 무효화(Invalidation)하여, 다음 접근 시 디스크로부터 최신 상태를 동기화받도록 강제합니다.

공유 매핑 영역의 데이터를 확실하게 디스크로 영속화하기 위해 프로세스는 msync() 시스템 콜을 호출합니다. 커널 내부의 filemap_sync() 루틴이 가상 주소 공간의 페이지 테이블 엔트리(PTE)들을 순회하면서, 하드웨어 MMU가 세팅한 하드웨어 레벨의 Dirty 비트를 추출해냅니다. 이 비트 정보를 초기화함과 동시에 하드웨어 TLB 캐시를 플러시하고, 커널 페이지 캐시 구조체의 PG_dirty 속성으로 논리적 오염 상태를 격상시킵니다. CPU 아키텍처 수준의 변경 이력을 VFS 파일시스템 수준의 더티 상태로 끌어올리는 이 핵심 과정을 거친 후, MS_SYNC 옵션의 경우 filemap_fdatawrite()filemap_fdatawait()를 차례로 호출하여 스토리지 동기화의 종지부를 찍습니다.

비선형 매핑(Non-linear memory mapping)은 기존의 파일 순서를 무시하고, "책의 제1장, 제9장, 제3장의 내용을 순서대로 이어 붙여 나만의 가상 메모리 스크랩북을 만들겠다"는 고도의 테크닉입니다. 전통적 매핑이 연속된 덩어리 위주라면, 비선형 매핑은 remap_file_pages() 시스템 콜을 통해 프로세스 메모리 공간 내부의 파편화된 페이지들을 파일 내 임의의 난해한 오프셋 지점들과 조각조각 대응시킬 수 있게 해줍니다. 흥미로운 점은 가상 주소 공간이 아무리 뒤섞여 있더라도, 기반이 되는 페이지 캐시 뷰는 여전히 철저하게 파일의 시작점을 기준으로 한 절대 오프셋 인덱스를 유지하며 질서를 지킨다는 것입니다.

핵심 O/X 퀴즈

1. filemap_nopage()는 mmap된 주소가 파일 안의 어느 오프셋에 해당하는지 계산한다.
Ovm_start (가상 메모리 기준점)와 vm_pgoff (파일 내 논리 기준점)를 산술 결합하여 정확한 I/O 타겟 오프셋을 역산해냅니다.
2. MS_SYNC는 더티 페이지를 디스크로 내보내고 완료를 기다리는 의미를 가진다.
O캐시 전송(fdatawrite)과 하드웨어 응답 대기(fdatawait) 루틴이 순차적으로 결합되어 데이터 원자성을 철저히 보장합니다.
3. non-linear mapping에서는 페이지 캐시가 메모리 영역 시작 기준 인덱스로만 페이지를 관리한다.
X페이지 캐시 트리는 철저하게 물리적 파일 구조(inode)의 오프셋 순서를 기준으로만 인덱싱을 유지하며, 가상 메모리 레이어의 비선형적 뷰 변형과는 철저히 분리되어 관리됩니다.
12

Direct I/O와 비동기 I/O: 페이지 캐시 우회(Bypass)와 비블로킹 데이터 파이프라인O_DIRECT & AIO

PostgreSQL이나 Oracle과 같은 고도화된 데이터베이스 엔진들은 자체적으로 커널보다 훨씬 복잡하고 최적화된 내부 버퍼 풀 메커니즘을 운영합니다. 이러한 애플리케이션에게 범용 운영체제의 페이지 캐시 레이어는 동일한 데이터 블록을 메모리에 두 번 중복 적재하게 만드는 비효율의 원흉이자, CPU가 개입하는 메모리 간 복사(User ↔ Kernel) 낭비를 초래하는 병목 지점입니다. 이러한 성능 저하를 우회하여 하드웨어 스토리지와 유저 메모리 사이에 직접 고속도로를 뚫어주는 아키텍처가 바로 Direct I/O (O_DIRECT)입니다.

1
O_DIRECT 플래그 식별. __generic_file_aio_read() 경로 진입 시 파일 구조체에 O_DIRECT 속성이 감지되면, 커널은 일반적인 페이지 캐시 검색 트리를 전면 우회하고 특화된 generic_file_direct_IO() 인터페이스로 실행 흐름을 전환합니다.
2
기존 잔류 캐시 정합성 보장. 캐시를 무시하기 전, 기존 mmap이나 표준 I/O로 생성된 해당 파일 영역의 더티 페이지가 메모리에 잔류해 있다면 filemap_fdatawrite()로 디스크에 강제 플러시하여 데이터 오염 및 버전 불일치 현상을 차단합니다.
3
Direct DMA 제출 (Submit). __blockdev_direct_IO()가 커널 버퍼를 배제한 채, 유저 공간의 버퍼 주소를 물리적 RAM 핀(Pinning) 상태로 묶어 직접 디바이스 컨트롤러의 DMA 타겟으로 지정하는 bio 구조체를 생성하고 하달합니다. 전송이 완료된 후 잔존할 수 있는 노후화된 캐시는 invalidate_inode_pages2()를 통해 완전 무효화시킵니다.
커널 레벨 Native 비동기 I/O (AIO) 인터페이스
io_setup()
커널 내부에 AIO 작업 처리를 위한 전용 컨텍스트를 프로비저닝하고, I/O 완료 상태 이벤트들이 쌓일 비동기 링 버퍼(AIO Ring) 구조를 유저 공간에 직접 매핑하여 생성합니다.
io_submit()
개발자가 구성한 I/O 컨트롤 블록(iocb)들의 배열을 한 번에 커널 큐로 밀어 넣습니다. 큐 삽입만 완료되면 디스크 응답을 기다리지 않고 즉각 프로세스 제어권을 반환합니다.
io_getevents()
AIO 링 버퍼에서 작업 완료 상태의 io_event 구조체들을 인출해 옵니다. 링 버퍼가 유저 메모리에 직접 매핑되어 있으므로 굳이 컨텍스트 스위칭 없이도 폴링(Polling) 방식으로 완료 이벤트를 검사할 수 있습니다.
io_cancel() / io_destroy()
펜딩(Pending)된 큐의 스케줄링을 강제 철회하거나 런타임 AIO 컨텍스트 자원을 클린업합니다.

비동기 I/O(AIO) 모델은 바쁜 카페에서 커피를 주문하고 진동벨을 건네받는 시스템과 완벽히 일치합니다. 주문(io_submit) 직후 계산대 앞에서 하염없이 대기(Blocking)할 필요 없이 사용자는 즉시 자신의 자리로 돌아가 다른 비즈니스 로직 연산을 처리할 수 있습니다. 스토리지 컨트롤러의 작업이 끝나면 진동벨(AIO 이벤트)이 울리고, 사용자는 유저 공간에 매핑된 공유 링 버퍼 창구를 통해 블로킹 오버헤드 없이 완성된 데이터(커피)를 손쉽게 수령하게 됩니다.

단 한 문장으로 압축하는 커널 파일 시스템의 정수: 표준 읽기는 캐시 조회 → 미스 시 디스크 → 유저 복사. 표준 쓰기는 유저 → 캐시(더티 마킹) → 백그라운드 디스크 플러시. mmap은 메모리 폴트를 통한 지연 로딩, Direct I/O는 캐시 시스템의 완벽한 우회, 그리고 AIO는 블로킹되는 대기 딜레이의 비동기적 분리입니다. 대상 데이터의 생애주기가 커널 아키텍처의 어느 계층에 위치하는지를 입체적으로 투시하는 시각이야말로 고성능 시스템 튜닝과 디버깅의 궁극적인 출발점입니다.

핵심 O/X 퀴즈

1. O_DIRECT는 self-caching 애플리케이션이 페이지 캐시 중복을 피하려 할 때 유용할 수 있다.
ORDBMS처럼 LRU 등 독자적인 캐시 최적화 알고리즘을 지닌 엔진에서 CPU 오버헤드를 줄이기 위한 필수 선택지입니다.
2. direct I/O에서는 페이지 캐시와 디스크 상태의 충돌을 막기 위해 기존 더티 페이지를 flush해야 할 수 있다.
O버퍼드 I/O와 다이렉트 I/O가 혼용될 때 발생하는 치명적인 캐시 코히어런시(Cache Coherency) 붕괴를 막기 위한 필수적인 동기화 장치입니다.
3. Linux AIO에서 AIO ring은 완료 이벤트를 저장하는 커널 내부 전용 버퍼이며 사용자 공간에서는 볼 수 없다.
XZero-copy 철학을 극대화하기 위해 AIO ring 자체가 유저스페이스 가상 주소로 안전하게 매핑되어 제공되므로, 고비용의 시스템 콜 없이 완료 폴링이 가능합니다.

리눅스의 파일 I/O 서브시스템을 마스터한다는 것은 단순히 read()write() 시스템 콜의 시그니처를 암기하는 차원이 아닙니다. 데이터 버퍼가 하드웨어와 커널 스택의 어느 레이어에 머물러 있는지, 캐시 뷰와 물리적 디스크 섹터 간의 정합성은 어느 시점에 보장되는지, 그리고 동기적 블로킹 병목을 어떻게 우회할 수 있는지에 대한 시스템 아키텍트적인 통찰력을 기르는 과정입니다.

강의노트 — Linux Kernel, Chapter 16 "Accessing Files" 기반 심화 재구성