수정 이력(4건, 최종 수정 2026년 09월 03일)
이 글에 적용한 변경 사항의 기록입니다. 보관해 둔 수정 전 버전은 DOI가 부여된 고정 URL에서 읽을 수 있습니다.
- Codex 리뷰에 따라 상담·문의 링크에 /ko/ 로케일 접두를 붙였습니다. 본문의 기술적인 주장은 바꾸지 않았습니다.
- 기사 맨 앞에 「이 기사의 knowledge map」을 추가했습니다. 본문에서 다루는 개념 사이의 관계를 한 장의 그림으로 볼 수 있습니다. 관계 전체 목록(근거 URL·확신도·확인 일자 포함)과 주요 개념 정의는 knowledge map 상세 페이지에 모았고, 기계 가독 데이터는 JSON-LD와 Turtle로 공개합니다.
- 대역표에서 `ReadFile` 호출 1회를 「IRP 1개」라고 단정하던 부분을 고쳤습니다. 캐시에 올라 있는 파일에 대한 동기 읽기·쓰기는 Fast I/O로 끝나고, IRP는 만들어지지 않습니다(같은 기사의 5.2에서 쓴 그대로입니다). 이 표를 보고 Procmon 트레이스를 읽으면, 존재하지 않는 IRP를 찾게 됩니다.
- Win32나 .NET에서 보이는 것과 커널 쪽 대응물을 나란히 둔 대역표를 추가했습니다(HANDLE과 file object에 대한 참조, `ReadFile` 1회와 IRP 1개, `GetLastError`와 NTSTATUS 등). 관찰 장에 필요한 것과 구하는 곳을 명시하고, 목적별 건너뛰기 가이드와 선수 지식을 맨 앞에 두었습니다.
- 최초 공개
이 글을 인용하기(DOI(등록된 아카이브): 10.5281/zenodo.22175238)
아래 DOI는 이전에 등록된 아카이브를 가리키며 현재 본문과 다를 수 있습니다. 현재 본문을 참조할 때는 이 페이지의 URL을 사용하세요.
Go Komura (2026). 「Windows I/O 내부 구조(제1회) ── 모든 읽기·쓰기는 IRP가 된다: I/O 시스템의 전체 그림」. 합동회사 코무라소프트. https://comcomponent.com/ko/blog/windows-io-internals-architecture-irp/
- DOI(등록된 아카이브)
- 10.5281/zenodo.22175238
- DOI(마지막 등록 버전)
- 10.5281/zenodo.22175239
File.ReadAllText 한 줄, 또는 ReadFile 한 번의 호출. 이 함수가 돌아올 때까지 Windows 안에서는 무엇이 일어날까요.
장애 조사에서 Process Monitor를 열면 IRP_MJ_READ나 FASTIO_READ처럼 낯선 말이 줄지어 나옵니다. handle leak을 쫓다 보면, CloseHandle을 호출했을 파일이 아직 살아 있습니다. 「network drive에서는 동작이 다르다」, 「백신 소프트웨어를 넣은 환경에서만 느리다」──업무 앱 현장에서 반복해서 만나는 이런 현상은, 모두 Windows I/O 시스템이라는 같은 바닥 위에서 일어납니다.
이 기사부터 그 바닥을 밑에서부터 파는 연재 「Windows I/O 내부 구조」를 시작합니다. 명저 『Windows Internals』가 커널 설계까지 내려가 설명하듯, 이 연재도 「API 사용법」이 아니라 「왜 그렇게 동작하는가」를 다룹니다. 1 예정 구성은 다음과 같습니다.
- I/O 시스템의 전체 그림 ── 모든 읽기·쓰기는 IRP가 된다(이 기사)
- 동기 I/O와 비동기 I/O ── OVERLAPPED의 진짜 의미
- I/O completion port(IOCP)와 .NET thread pool ── async/await의 지하실
- Cache Manager ── 당신의 WriteFile은 언제 디스크에 도착하는가
- NTFS 내부 구조 ── MFT부터 이해하는 file system
- filter driver와 minifilter ── Procmon과 바이러스 검사가 I/O에 끼어들 수 있는 이유
제1회인 이 기사는, 이후 모든 회의 토대가 되는 「등장 인물과 요청의 흐름」을 그림으로 잡습니다. driver를 작성하기 위한 기사가 아닙니다. 애플리케이션 개발자가, 자신이 호출한 API의 행선지를 끝까지 따라갈 수 있게 되기 위한 기사입니다.
선수 지식: C#에서 FileStream을 써 본 적이 있거나, C/C++에서 CreateFile/ReadFile을 호출해 본 적이 있으면 읽을 수 있습니다. driver 개발 경험은 필요 없고, 커널 쪽 코드는 개념도로만 나옵니다. 읽는 데 걸리는 시간은 그림을 보면서 통으로 약 25분, 1장의 결론만이면 2~3분입니다.
건너뛰기 가이드: 길기 때문에, 목적별 입구를 둡니다.
- 전체 그림만 잡고 싶다 → 1장(결론)→ 2장(이름 해석)→ 5장(
ReadFile한 왕복) - 용어를 정리하고 싶다(driver/device/file object, IRP) → 3장·4장
- 현장의 「깨지는 방식」을 알고 싶다(handle leak, 「파일이 사용 중」) → 6장
- 직접 확인하고 싶다 → 7장
1. 먼저 결론
- Windows I/O는 packet-driven입니다. device driver로 가는 요청의 대부분은 IRP(I/O Request Packet)라는 패킷에 담겨, driver 더미(device stack)를 위에서 아래로 흐릅니다. 23
- CreateFile이 여는 대상이 「파일」이라고 단정할 수는 없습니다. 이름 해석은 Object Manager namespace에서 이루어지고,
C:의 실체는\\Device\\HarddiskVolume3같은 NT device name으로의 symbolic link입니다(2장). 45 - 등장 인물은 세 가지 object입니다. 처리 함수 표를 가진 「driver object」, 요청의 목적지가 되는 「device object」, 연 한 번의 상태를 가진 「file object」. 당신이 가진 HANDLE은 file object에 대한 참조입니다(3장). 6789
- 각 driver의 선택지는 세 가지뿐입니다. IRP를 「직접 완료한다」, 「아래 driver로 넘긴다」, 「pending으로 두었다가 나중에 완료한다」. 이 세 가지 조합이 filter driver도 cache도 비동기 I/O도 전부 설명합니다(4장). 310
- 커널 바닥에 「동기 I/O」라는 별도의 구조는 없습니다. 동기 I/O의 정체는 「완료되기 전에는 반환되지 않는다」는 보장이고, 요청이 pending이 되었을 때만 대기가 발생합니다. 여기가 제2회·제3회의 입구입니다(5장). 11
- CloseHandle은 「닫기」가 아니라 「handle을 하나 반환하기」입니다. 마지막 handle이 닫히면 cleanup, 커널 안의 참조까지 모두 사라지면 close라는 2단계이고, 「닫았는데 사용 중」의 정체는 이 차이에 있습니다(6장). 1213
- 이 계층은 직접 관찰할 수 있습니다. WinObj로 namespace를, Process Monitor로 IRP의 흐름을 볼 수 있습니다. Procmon의 표시 어휘는 이 기사의 용어 그대로입니다(7장). 14
그림의 실선은 항상 성립하는 관계, 점선은 조건이 붙는 관계입니다(성립 조건은 상세 페이지의 관계별 설명에 적혀 있습니다). 관계 전체 목록(총 40건, 근거와 확신도 포함)과 주요 개념의 정의는 지식 맵 상세 페이지에 정리되어 있습니다(일본어). 데이터: JSON-LD / Turtle
2. 「모든 것이 파일처럼 보인다」의 정체
2.1. CreateFile에 넘긴 이름은 어디로 가는가
Win32 파일 API는 모두 「이름」에서 시작합니다. C:\project\report.csv 같은 path, \\server\share\data.csv 같은 UNC path, \\.\COM3 같은 device 지정──이들은 모두 같은 CreateFile에 넘길 수 있습니다. 15
이 일관성 뒤에는, 커널의 Object Manager가 관리하는 단일 namespace가 있습니다. 커널 안에서는 device도 event도 공유 메모리 section도, 모두 「object」로서 이 트리 모양 namespace에 등록되어 있습니다. Sysinternals의 WinObj를 쓰면, 이 namespace를 그대로 들여다볼 수 있습니다. 14
flowchart TB
ROOT["\\ (namespace의 루트)"]
DEV["\\Device<br/>(driver가 만드는 device object)"]
GLB["\\GLOBAL??<br/>(Win32에서 보이는 전역 이름의 보관 장소)"]
BNO["\\BaseNamedObjects<br/>(이름이 있는 mutex 등)"]
ROOT --> DEV
ROOT --> GLB
ROOT --> BNO
DEV --> D1["HarddiskVolume3"]
DEV --> D2["Serial0"]
DEV --> D3["Mup (network redirector)"]
GLB --> L1["C: → \\Device\\HarddiskVolume3"]
GLB --> L2["COM1 → \\Device\\Serial0"]
GLB --> L3["PhysicalDrive0 → \\Device\\Harddisk0\\DR0"]
그림 1: Object Manager namespace(발췌). \\GLOBAL?? 아래에 있는 것은 symbolic link이고, 실체는 \\Device 아래에 있습니다
핵심은, Win32 앱이 쓰는 이름(drive letter나 COM port 이름)과, 커널이 쓰는 이름(NT device name)이 다른 계층에 있다는 점입니다. 둘을 잇는 것이 symbolic link입니다.
2.2. drive letter는 symbolic link이다
driver는 자신의 device를 Win32 앱에서 보이게 하고 싶을 때, IoCreateSymbolicLink로 \\DosDevices\\COM1 같은 MS-DOS device name에서 NT device name으로의 symbolic link를 만듭니다. 4 C:도 같은 구조이고, 실체는 \\Device\\HarddiskVolume3 같은 volume device로의 링크입니다.
그래서 CreateFile("C:\\project\\report.csv")의 이름 해석은 이렇게 진행됩니다.
flowchart TB
A["앱이 넘긴 이름<br/>C:\\project\\report.csv"]
B["Win32 계층이 NT 형식으로 변환<br/>\\??\\C:\\project\\report.csv"]
C["Object Manager가 namespace를 검색<br/>\\??\\C: 가 symbolic link임을 알아낸다"]
D["링크를 따라가 치환<br/>\\Device\\HarddiskVolume3\\project\\report.csv"]
E["\\Device\\HarddiskVolume3 에서<br/>volume의 device object에 도달"]
F["남은 \\project\\report.csv 의 해석은<br/>I/O Manager가 IRP_MJ_CREATE를 발행해<br/>file system driver(NTFS)에 맡긴다"]
A --> B
B --> C
C --> D
D --> E
E --> F
그림 2: CreateFile의 이름 해석. 전반은 Object Manager, device에 도달한 후반은 file system의 일
이 그림에서 평소의 의문이 몇 가지 풀립니다.
\\\\.\\prefix의 의미.\\\\.\\PhysicalDrive0이나\\\\.\\COM10의\\\\.\\는 Win32 device namespace(≒\\??directory)를 직접 가리키기 위한 표기입니다. drive letter를 거치지 않고, 링크가 놓인 장소를 직접 지정합니다. 515CON이나NUL을 파일 이름으로 쓸 수 없는 이유. 이들은 MS-DOS device name으로 예약되어 있어, path 어디에 적어도 device 쪽으로 해석될 수 있기 때문입니다. 5 이 부근의 실무적 함정은 「MAX_PATH와 Windows path·파일 이름의 함정」에 정리해 두었습니다.- UNC path도 특별 취급이 아닙니다.
\\\\server\\share는 network redirector의 device(\\Device\\Mup)로 해석되고, 그 이후는 SMB client가 네트워크 너머로 요청을 나릅니다. 로컬과 UNC에서 동작이 달라지는 문제의 뿌리는 해석 대상 device가 다르다는 점에 있습니다(「network drive와 UNC path의 함정」 참조).
정확한 표현을 보태면, \\??는 실제로 존재하는 directory의 별명이 아니라, 「먼저 로그온 세션마다의 로컬 DOS device map을 보고, 없으면 \\GLOBAL??로 떨어진다」는 검색 순서를 나타내는 가상의 입구입니다. net use나 subst로 만든 drive letter는 이 로컬 쪽에 들어가므로, 같은 PC라도 사용자(로그온 세션)마다 보이는 drive가 다르고, 서비스에서는 사용자의 network drive가 보이지 않는 현상이 일어납니다. 그림 1에 그린 것은 전역 쪽(\\GLOBAL??)뿐입니다.
즉 「Windows에서는 모든 것이 파일처럼 보인다」의 정확한 말은, 「모든 이름이 최종적으로 device object로 해석되고, 그 이후 요청이 같은 형식(IRP)으로 통일되어 있다」입니다. 그렇다면 그 device object는 무엇인가. 등장 인물을 정리합니다.
3. 등장 인물은 세 가지 object
Windows I/O 시스템의 구조는, 세 종류의 커널 object의 관계로 그릴 수 있습니다.
여기서부터 전문 용어가 이어집니다. 길을 잃지 않도록, 먼저 「평소 작성하는 코드에서 보이는 것」과 「커널 쪽 이름」의 대역표를 둡니다. 3장과 4장은 오른쪽 열의 말로 쓰여 있습니다.
| Win32 / .NET 쪽에서 보이는 것 | 커널 쪽 대응물 | 한 줄로 말하면 |
|---|---|---|
HANDLE / SafeFileHandle |
handle table의 항목(file object에 대한 참조) | 「연 한 번」을 가리키는 번호표(3.3) |
FileStream이 쥐고 있는 연 상태 |
file object | 공유 모드·flag·현재 위치의 그릇(3.3) |
drive letter C:나 \\\\.\\COM3 |
device object(로의 이름 해석 결과) | 요청의 목적지(2장·3.2) |
| 「NTFS driver」「disk driver」 | driver object | 요청 종류마다의 처리 함수 표(3.1) |
ReadFile / stream.Read 호출 1회 |
보통은 IRP 1개 | 목적지로 운반되는 요청 패킷(4장). 캐시에 올라 있는 파일에 대한 동기 읽기·쓰기는 Fast I/O라는 지름길로 IRP를 만들지 않고 끝냅니다(5.2). Procmon에서 FASTIO_로 시작하는 행이 그것입니다 |
FileOptions / CreateFile의 flag |
file object에 기록되는 속성 | 미리 읽기나 비동기 동작을 정합니다(7장의 대응표) |
GetLastError의 값 / .NET 예외 |
NTSTATUS(STATUS_PENDING 등) |
완료 상태. Win32 오류 코드로 번역되어 올라옵니다 |
3.1. driver object ── 처리 함수의 표
driver가 로드되면, I/O Manager는 그 driver를 나타내는 driver object(DRIVER_OBJECT)를 만듭니다. 6 앱 개발자 관점에서 가장 중요한 멤버는 MajorFunction 배열입니다. 이것은 「요청 종류 → 처리 함수」의 대응표이고, 요청 종류는 IRP_MJ_CREATE(열기), IRP_MJ_READ(읽기), IRP_MJ_WRITE(쓰기), IRP_MJ_CLEANUP, IRP_MJ_CLOSE 같은 major function code로 나타냅니다. 16
C#으로 억지로 쓰면, 이런 이미지입니다.
// 개념도. 실제는 커널 안의 C 구조체
class DriverObject
{
// IRP_MJ_XXX 가 인덱스. 전부 28종류
public DispatchRoutine[] MajorFunction = new DispatchRoutine[28];
}
// ntfs.sys 라면 MajorFunction[IRP_MJ_READ] 에 「NTFS의 읽기 처리」가 들어 있다
3.2. device object ── 요청의 목적지
driver는 자신이 맡은 device마다 device object(DEVICE_OBJECT)를 만듭니다. 7 이것이 I/O 요청의 목적지입니다. 물리 device와 1대1이라고 단정할 수는 없고, volume(HarddiskVolume3) 같은 논리적인 존재나, filter driver가 만드는 「끼어들기만을 위한 device」도 있습니다. device object는 자신을 만든 driver object를 가리키므로, 목적지가 정해지면 처리 함수의 표도 정해집니다.
3.3. file object ── 「연 한 번」의 상태
CreateFile이 성공할 때마다, 커널은 file object를 하나 만듭니다. 이것은 「디스크 위의 파일」 그 자체가 아니라, 그 파일(또는 device)을 연 한 번의 세션을 나타내는 object입니다. 8 같은 파일을 두 번 열면, file object는 두 개 생깁니다. 현재 file pointer(동기 handle인 경우), 열 때의 공유 모드나 flag는 여기에 들어 있습니다.
그리고 앱이 받는 HANDLE은, 프로세스마다의 handle table을 거친, file object에 대한 참조입니다. 9
flowchart LR
subgraph P["프로세스(user mode)"]
H1["HANDLE 0x1A4"]
H2["HANDLE 0x1B8"]
end
subgraph K["커널 공간"]
FO1["file object 1<br/>report.csv를 읽기로 연 상태<br/>현재 offset: 4096"]
FO2["file object 2<br/>report.csv를 이어 쓰기로 연 상태<br/>현재 offset: 65536"]
DO["device object<br/>HarddiskVolume3 상당"]
DR["driver object NTFS<br/>MajorFunction = 처리 함수의 표"]
end
H1 --> FO1
H2 --> FO2
FO1 --> DO
FO2 --> DO
DO --> DR
그림 3: 세 가지 object의 관계. handle은 handle table을 거쳐 file object를 가리키고, file object는 device로, device는 driver로 이어집니다
이 그림을 머리에 넣으면, 몇 가지 실무 지식이 「암기」에서 「당연」으로 바뀝니다.
- handle leak의 정체는, 계속 참조되어 해제되지 못하는 file object(와 그 앞의 리소스)의 더미입니다. 세어야 할 것은 handle table의 항목이고, Process Explorer나
handle.exe가 보여주는 것이 바로 이것입니다(「Process Explorer / Handle / VMMap 실전」, 조사기로서는 「산업용 카메라 장기 가동 크래시 조사 - handle leak편」). - 같은 파일을 연 두 handle에서 file pointer가 독립되어 있는 것은, pointer가 file object 쪽에 있기 때문입니다. 반대로
DuplicateHandle로 복제한 handle은 같은 file object를 가리키므로, pointer를 공유합니다. - 공유 위반(sharing violation)은, 기존 file object들의 공유 모드와 새로운
CreateFile요청을 커널이 맞춰 보고 판정합니다. 배타 제어의 실무는 「파일 연동의 배타 제어 기초 지식」에서 다루었습니다.
4. IRP ── I/O 요청은 소포가 된다
4.1. 왜 패킷으로 만드는가
I/O Manager는 앱으로부터의 요청(열기·읽기·쓰기…)을 받으면, IRP(I/O Request Packet)라는 패킷에 담아 driver에 넘깁니다. device driver로의 요청의 대부분은 IRP로 도착합니다. 3 device는 OS와 속도가 맞지 않으므로(디스크는 CPU보다 몇 자릿수나 느림), 요청을 「호출」이 아니라 「짐」으로 만들어, 발행과 완료를 떼어낼 수 있는 형태로 한 것입니다. 2
IRP에는 요청의 전체 정보를 가진 헤더에 더해, I/O stack location이라는 영역이 경유 예정 driver 수만큼 늘어서 있습니다. 각 driver는 자기용 stack location에서 「자신에게 향한 지시」(major function code, parameter)를 읽습니다. 17
4.2. device stack을 내려간다
device object는 쌓여 device stack을 만듭니다. 18 예를 들어 로컬 디스크 위 파일에 대한 읽기는, 대략 다음 길을 지납니다.
flowchart TB
IOM["I/O Manager가 IRP를 조립한다<br/>(IRP_MJ_READ+stack location)"]
subgraph FSSTACK["file system 쪽 stack"]
FLT["file system filter<br/>(백신·암호화·Procmon 등)"]
NTFS["NTFS<br/>파일 안 offset을 volume 위 위치로 변환"]
end
subgraph STSTACK["storage 쪽 stack"]
VOL["volume/partition 관리<br/>(volmgr 등)"]
DISK["disk class driver<br/>(disk.sys)"]
PORT["storage port/miniport<br/>(storport 등)"]
end
HW[("디스크 장치")]
IOM --> FLT
FLT --> NTFS
NTFS --> VOL
VOL --> DISK
DISK --> PORT
PORT --> HW
그림 4: 읽기 요청이 지나는 길. file system 쪽과 storage 쪽은 별도의 device stack이고, NTFS는 storage 쪽을 향한 하위 IRP를 새로 발행해 일을 맡깁니다
이 그림에서 기억할 것은 두 가지입니다.
- filter는 정규 구성원입니다. 백신 소프트웨어가 모든 파일 I/O를 검사할 수 있는 것은 편법이 아니라, 이 「사이에 끼는」 구조가 OS의 공식 확장 지점이기 때문입니다. 19 Process Monitor도 같은 자리에 서서 모든 I/O를 기록합니다. 「그 환경에서만 파일 접근이 느리다」는 조사에서 먼저 의심해야 할 장소이기도 합니다(자세한 내용은 제6회).
- 계층마다 요청의 의미가 번역됩니다. 앱은 「이 파일의 offset 4096부터 8KB」라고 말하고, NTFS가 그것을 「이 volume의 이 cluster」로, storage stack이 「이 디스크의 이 sector」로 번역합니다. 위 계층은 아래 계층의 사정을 모릅니다. 여기서 중요한 보충이 하나 있습니다. 하나의 IRP가 앱에서 디스크까지 그대로 통과해 도착하는 것은 아니다는 점입니다. file system 쪽과 storage 쪽은 별도의 stack이고, NTFS는 파일 대상 IRP를 처리한 결과로서, volume(storage stack) 대상의 하위 IRP를 새로 만들어 발행합니다. 단편화된 파일이라면 하나의 읽기가 여러 하위 IRP로 나뉘기도 하고, 요청의 입도와 수명도 계층마다 달라집니다.
4.3. 각 driver의 세 가지 선택지
IRP를 받은 driver가 할 수 있는 일은, 본질적으로 세 가지뿐입니다. 310
flowchart TB
RECV["driver가 IRP를 받는다"]
Q{"이 요청을 어떻게 다룰까"}
DONE["(1) 직접 완료한다<br/>IoCompleteRequest 를 호출<br/>예: cache에 있는 데이터로 즉시 응답"]
PASS["(2) 아래 device로 넘긴다<br/>IoCallDriver 를 호출<br/>예: filter가 검사하고 그대로 통과"]
PEND["(3) pending으로 둔다<br/>STATUS_PENDING 을 반환하고 IRP를 queue에<br/>예: 하드웨어 응답 대기"]
LATER["interrupt 등을 계기로<br/>나중에 IoCompleteRequest"]
UP["완료 처리가 stack을 역순으로 올라간다<br/>(각 계층의 completion routine이 호출된다)"]
RECV --> Q
Q --> DONE
Q --> PASS
Q --> PEND
PASS -->|"아래 계층이 그 자리에서 완료했다"| UP
PASS -->|"아래 계층이 pending으로 두었다<br/>(pending은 호출자까지 전달된다)"| LATER
PEND --> LATER
DONE --> UP
LATER --> UP
그림 5: IRP를 받은 driver의 세 갈래. 셋은 배타가 아니라 「아래로 넘긴 곳에서 pending이 된다」가 가장 흔한 길이고, 모두 마지막은 IoCompleteRequest에 의한 완료로 끝납니다
이 세 가지는 배타적인 선택지가 아닙니다. 가장 흔한 길은 「(2)로 아래로 넘긴 곳에서, 어느 계층이 (3)의 pending을 고르는」 것이고, 그 경우 STATUS_PENDING은 사이의 driver에도 호출자에게도 그대로 전달됩니다. 나중에 아래 계층이 완료시키면, 넘길 때 completion routine을 등록해 둔 각 계층이 역순으로 다시 호출됩니다──즉 「넘기기」와 「pending」은 같은 하나의 IRP 위에서 겹쳐 일어납니다.
이 세 갈래가 Windows I/O 유연성의 원천입니다.
- cache hit로 즉시 완료하면 빠릅니다(제4회의 Cache Manager).
- 그대로 통과시키는 filter를 몇 장이든 끼울 수 있습니다(제6회의 minifilter).
- pending으로 둘 수 있으므로, 느린 device를 기다리는 동안 thread를 붙잡아 두지 않아도 됩니다(제2회·제3회의 비동기 I/O).
연재의 나머지 전부가, 실은 이 그림의 각주입니다.
5. ReadFile 한 왕복을 따라간다
등장 인물과 도구가 갖춰졌으므로, ReadFile 1회의 왕복을 통으로 따라갑니다. cache를 벗어나 디스크까지 가는 경우입니다(cache에 오르는 이야기는 제4회에서).
sequenceDiagram
participant App as 앱의 thread
participant IOM as I/O Manager
participant FS as filter+NTFS
participant ST as storage stack
participant HW as 디스크 장치
App->>IOM: ReadFile → NtReadFile(시스템 호출)
Note over IOM: handle에서 file object를 해석하고<br/>IRP(IRP_MJ_READ)를 조립한다
IOM->>FS: IoCallDriver(stack의 선두로)
FS->>ST: 위치를 volume 위로 번역하고<br/>storage stack 대상의 하위 IRP를 발행
ST->>HW: 읽기 명령을 발행
ST-->>FS: STATUS_PENDING(하위 IRP는 pending)
FS-->>IOM: 원래 IRP도 pending인 채로 돌아온다<br/>(가는 길은 여기서 끝)
Note over App: 동기 I/O: 여기서 완료를 기다리며 잠든다<br/>비동기 I/O: 제어가 돌아와 다른 일을 할 수 있다
HW-->>ST: interrupt 「데이터를 다 읽었다」
Note over ST: interrupt 처리(ISR)에서<br/>DPC로 완료 처리를 계속
ST->>FS: 하위 IRP를 완료(IoCompleteRequest)<br/>NTFS 쪽 completion routine이 받는다
Note over FS: 필요한 하위 IRP가<br/>모두 완료되면
FS->>IOM: 원래 IRP(IRP_MJ_READ)를 완료
Note over IOM: 각 계층의 completion routine을 역순으로 실행하고<br/>요청한 thread로의 APC로 결과를 확정
IOM->>App: 상태와 바이트 수가 확정(event 통지 등)
그림 6: cache를 벗어난 ReadFile의 한 왕복. 하위 IRP의 완료와 원래 IRP의 완료는 별도 단계이고, 「가는 길」과 「오는 길」도 별도의 이벤트로 진행됩니다
5.1. 가는 길과 오는 길은 별개의 사건
이 그림의 급소는 STATUS_PENDING 행입니다. storage driver는 하드웨어에 명령을 발행한 시점에서 일단 손을 놓고, 「가는 길」의 처리는 거기서 끝납니다. 데이터가 읽혀 올라왔다는 사실은, 나중에 interrupt라는 별도 이벤트로 통지되고, 거기서부터 「오는 길」의 완료 처리가 시작됩니다. 10
즉 커널 안에서는, I/O의 발행과 완료를 분리할 수 있게 만들어져 있습니다. 「동기 I/O」라는 별도의 배관이 있는 것이 아니라, 동기 I/O의 정확한 의미는 「완료되기 전에는 호출이 반환되지 않는다」는 보장입니다. 대기가 발생하는 것은 요청이 pending이 되었을 때뿐이고, driver가 그 자리에서 완료할 수 있는 요청(그림 5의 「완료한다」 길)이라면, 동기 I/O라도 thread는 한 번도 잠들지 않고 결과를 들고 돌아옵니다. Win32가 동기·비동기를 handle을 여는 방식(FILE_FLAG_OVERLAPPED)으로 바꾸는 것도, 비동기가 「특별한 추가 기능」이 아니라 기다리는 방식의 차이이기 때문입니다. 11
이 관점을 가지면, 제2회에서 다룰 의문──왜 비동기 여부는 호출마다가 아니라 handle을 열 때 정해지는가(OVERLAPPED 구조체 자체는 발행 중인 조작 하나마다 필요합니다), 「비동기여야 하는데 동기적으로 완료된다」는 무엇인가──가, 구조의 필연으로 보입니다. .NET의 async/await에서 I/O 대기가 thread를 소비하지 않는 이유(「C# async/await 실무 판단표」에서 실무 쪽에서 쓴 이야기)도, 근거는 이 그림입니다.
5.2. 예외도 있다 ── IRP를 만들지 않는 지름길
솔직히 덧붙이면, 모든 I/O가 IRP가 되는 것은 아닙니다. 캐시에 올라 있는 파일에 대한 동기 읽기·쓰기를 위해, file system은 Fast I/O라는 「IRP를 조립하지 않고 cache에서 직접 복사하는」 지름길을 제공합니다. Procmon의 Operation 열에서 FASTIO_로 시작하는 행이 그것입니다. 지름길을 쓸 수 없는 조건이나 Cache Manager와의 관계는 제4회에서 다룹니다.
6. CloseHandle의 이면 ── cleanup과 close는 별개
마지막으로, 연 것을 닫는 방식입니다. 여기는 handle leak 조사나 「파일이 사용 중」 문제에 직결됩니다.
CloseHandle이 하는 일은, 프로세스의 handle table에서 항목을 하나 지우는 것입니다. file object에는 handle count(handle의 수)와 reference count(커널 안으로부터의 참조 수)가 있고, 둘은 따로 줄어듭니다.
sequenceDiagram
participant App as 앱
participant OB as Object Manager
participant IOM as I/O Manager
participant FS as file system
App->>OB: CloseHandle(h)
Note over OB: handle table에서 항목을 삭제<br/>handle count를 줄인다
alt 그것이 마지막 handle이었다
IOM->>FS: IRP_MJ_CLEANUP
Note over FS: 그 file object의<br/>미완료 I/O 취소나 lock 해제
end
Note over OB: 다만 미완료 I/O나 section(memory map) 등<br/>커널 안의 참조가 남아 있으면<br/>file object는 아직 살아 있다
alt reference count도 0이 되었다
IOM->>FS: IRP_MJ_CLOSE
Note over FS: file object의 뒷정리가 완료<br/>여기서 비로소 「닫기가 끝났다」
end
그림 7: cleanup(마지막 handle이 닫힘)과 close(참조도 모두 사라짐)의 2단계
- IRP_MJ_CLEANUP은 「마지막 handle이 닫혔다」는 통지입니다. 다만 미완료 I/O가 남아 있으면, file object의 해제는 아직일 수 있다고 문서 자신이 명시합니다. 12
- IRP_MJ_CLOSE는 「reference count가 0이 되었다」는 통지입니다. cleanup과 close 사이에는 틈이 있고, 즉시 이어진다고 단정할 수는 없습니다. 13
이 2단계를 알면, 현장의 이상한 현상을 설명할 수 있습니다.
- memory-mapped file을 닫아도 파일이 해제되지 않는다. map된 section이 file object에 대한 참조를 계속 쥐고 있기 때문이고, view의 unmap과 section의 close가 끝날 때까지 close는 오지 않습니다. 공유 메모리 주변의 실무는 「공유 메모리의 함정과 실무 베스트 프랙티스」에서 다루었습니다.
- 「사용 중인 파일」의 범인 찾기는 handle만으로 끝나지 않습니다. handle을 전부 닫아도, 커널 안의 참조(map된 이미지 등)가 파일을 붙잡고 있는 경우가 있습니다. Process Explorer의 검색이 handle과 DLL(map된 파일) 양쪽을 대상으로 하는 것은, 바로 이 이유입니다.
- .NET의
SafeFileHandle이나 finalizer가 「언젠가 닫아 준다」에 기대면 안 되는 것도, 닫기를 잊은 handle이 cleanup을 늦추고, 공유 위반이나 lock 유지를 길게 끌기 때문입니다.
7. 직접 눈으로 확인한다
여기까지의 내용은, 관리자 권한이 있는 Windows PC가 한 대 있으면 전부 관찰할 수 있습니다.
필요한 것은 세 가지뿐입니다.
- 관리자 권한. Process Monitor는 커널 driver를 로드하므로, 관리자로 실행해야 합니다. WinObj도, 관리자가 아니면 보이지 않는 object가 있습니다.
- Sysinternals 도구. WinObj와 Process Monitor는 Microsoft가 무상으로 배포합니다. 개별 다운로드 페이지(WinObj, Process Monitor)에서 구하거나, 한꺼번에 넣으려면 Sysinternals Suite입니다. 설치 프로그램은 없고, 펼쳐서 exe를 실행하면 됩니다.
- 관찰에 쓸 적당한 조작. 메모장으로 파일을 하나 저장하거나, 작은 파일을 하나 복사하는 정도면 충분합니다. 업무용 공유 폴더나 운영 장비가 아니라, 손안의 로컬 디스크에서 시험해 주세요.
WinObj로 namespace를 본다. Sysinternals의 WinObj를 실행하고, GLOBAL?? directory를 열면, C:가 \\Device\\HarddiskVolumeN으로의 symbolic link라는 것(그림 1)이 그대로 표시됩니다. \\Device 아래에서는, driver들이 만든 device object의 실명을 볼 수 있습니다. 14
Procmon으로 IRP의 말을 읽는다. Process Monitor 메뉴에서 Filter > Enable Advanced Output을 켜면, Operation 열이 ReadFile이 아니라 IRP_MJ_READ나 FASTIO_READ라는 커널 쪽 어휘로 바뀝니다. 파일 복사 한 번만 따라가도, IRP_MJ_CREATE → FASTIO_READ/IRP_MJ_READ → IRP_MJ_WRITE → IRP_MJ_CLEANUP → IRP_MJ_CLOSE라는 이 기사의 흐름이 실제로 줄지어 나옵니다. Procmon의 실무적 사용법은 「Process Monitor(ProcMon) 실전 가이드」에 정리해 두었습니다.
.NET에서 바닥을 의식한다. C#의 FileStream은 내부에서 CreateFileW를 호출하고, SafeFileHandle로 handle을 유지합니다. 생성자의 FileOptions는, 거의 그대로 Win32 flag로의 직통입니다.
.NET (FileOptions) |
Win32 (CreateFile flag) |
의미(관련 회) |
|---|---|---|
Asynchronous |
FILE_FLAG_OVERLAPPED |
비동기 I/O용으로 handle을 연다(제2회·제3회) |
WriteThrough |
FILE_FLAG_WRITE_THROUGH |
쓰기를 cache에서 멈추지 않는다(제4회) |
SequentialScan |
FILE_FLAG_SEQUENTIAL_SCAN |
미리 읽기 힌트(제4회) |
RandomAccess |
FILE_FLAG_RANDOM_ACCESS |
미리 읽기 억제 힌트(제4회) |
DeleteOnClose |
FILE_FLAG_DELETE_ON_CLOSE |
마지막 handle이 닫히면 삭제(6장 구조의 응용) |
.NET 6 이후라면 File.OpenHandle과 RandomAccess 클래스로, FileStream의 추상을 끼우지 않고 「handle+offset 지정 I/O」라는 Win32의 날것에 가까운 쓰기도 가능합니다. 이 표의 오른쪽 의미를 알고 있으면, 왼쪽의 선택은 암기가 아니게 됩니다.
8. 정리
- Windows I/O는, Object Manager의 namespace에서 목적지(device object)를 정하고, 요청을 IRP에 담아 device stack으로 흘리는, 한 줄기의 설계로 관통되어 있습니다. 2318
C:는 symbolic link,\\\\.\\는 이름이 놓인 장소의 직접 지정, UNC는 redirector로의 해석. 「모든 것이 파일처럼 보인다」의 정체는 이름 해석의 일관성입니다. 45- 등장 인물은 driver object(처리 함수의 표), device object(목적지), file object(연 한 번의 상태) 세 가지. HANDLE은 file object에 대한 참조입니다. 6789
- driver의 선택지는 완료·통과·pending 세 가지. filter의 끼어들기도, cache의 즉시 응답도, 비동기 I/O도, 이 세 갈래의 응용입니다. 310
- 발행과 완료는 분리할 수 있게 만들어져 있고, 동기 I/O는 「완료되기 전에는 반환되지 않는다」는 보장입니다. pending이 된 요청에서는, 가는 길(발행)과 오는 길(interrupt→완료)이 별도 이벤트로 진행됩니다. 1110
CloseHandle은 「handle을 반환하는」 일뿐입니다. cleanup(마지막 handle)과 close(마지막 참조)의 2단계를 알고 있으면, 「닫았는데 사용 중」은 수수께끼가 아니게 됩니다. 1213
이어지는 글은 제2회 「동기 I/O와 비동기 I/O ── OVERLAPPED의 진짜 의미」입니다. 이 기사에서 본 「발행과 완료의 분리」를 앱 쪽에서 쓰는 구조──FILE_FLAG_OVERLAPPED, 완료 통지의 4가지 방식, 취소, 그리고 「비동기여야 하는데 동기로 돌아온다」는 함정──을 팝니다.
관련 기사
- Process Monitor(ProcMon) 실전 가이드 ── 「설정이 읽히지 않는다」「ACCESS DENIED」를 10분 만에 특정한다
- Process Explorer / Handle / VMMap 실전 ── hang·leak·「파일이 사용 중」을 지금 이 순간의 상태에서 쫓는다
- 산업용 카메라 장기 가동 크래시 조사 - handle leak편
- 파일 연동의 배타 제어 기초 지식 - 파일 lock과 원자적 claim의 베스트 프랙티스
- 공유 메모리의 함정과 실무 베스트 프랙티스
- C# async/await 실무 판단표 - Task.Run과 ConfigureAwait
- network drive와 UNC path의 함정 ── 업무 앱에서 파일 서버(공유 폴더)를 다루는 실무
- MAX_PATH와 Windows path·파일 이름의 함정 ── 260자 제한, 예약 이름, 끝의 점, 대소문자
관련 상담 영역
고무라소프트에서는 Windows 업무 앱의 파일 I/O 주변 설계·장애 조사(handle leak, 「파일이 사용 중」, 특정 환경에서의 I/O 지연 등)를 다룹니다.
참고 링크
-
Microsoft Learn, Windows Internals - Sysinternals. 책 『Windows Internals』의 소개 페이지. Windows 커널 아키텍처와 I/O 시스템을 포함한 내부 구조를 다루는 정평 있는 책이며, 이 연재가 다루는 내용을 더 깊게 배우기 위한 출발점으로서. ↩
-
Microsoft Learn, I/O manager. Windows 커널 모드 I/O Manager가 애플리케이션과 device driver가 제공하는 인터페이스 사이의 통신을 관리한다는 점, device는 OS와 일치하지 않는 속도로 동작하므로 OS와 driver 사이의 통신은 주로 IRP(I/O Request Packet)를 통해 이루어진다는 점, IRP가 네트워크 패킷이나 Windows 메시지와 비슷한 존재로서 OS에서 driver로, 또 driver에서 driver로 전달된다는 점에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, I/O request packets. device driver로 가는 요청의 대부분이 IRP에 담긴다는 점, OS 구성 요소나 driver가 IoCallDriver(device object에 대한 포인터와 IRP에 대한 포인터를 받음)로 driver에 IRP를 보낸다는 점, IRP가 보통 device stack으로 쌓인 여러 driver에 의해 처리되고 먼저 stack 최상위 device object로 보내진다는 점, 각 driver가 IRP를 처리해 완료하거나 아래 driver로 전달할지를 고를 수 있다는 점에 대해. ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
Microsoft Learn, Introduction to MS-DOS device names. MS-DOS device name이 NT 스타일 device name으로의 symbolic link라는 점, user mode Windows 앱은 MS-DOS device name(drive letter나 COM port 이름)으로 device에 접근하는 반면 driver나 커널은 NT 스타일 이름을 쓴다는 점, driver가 IoCreateSymbolicLink로 \DosDevices\이름 에서 device로의 symbolic link를 만든다는 점에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, Naming files, paths, and namespaces. CON, PRN, AUX, NUL, COM1~COM9, LPT1~LPT9가 파일 이름으로 예약되어 있다는 점, Win32 namespace에 「file namespace」와 「device namespace」가 있고, “\\.\” prefix가 Win32 device namespace로의 접근을 의미한다는 점(예: \\.\PhysicalDrive0), 이들 이름 해석 규칙에 대해. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Introduction to driver objects. driver 로드 시 I/O Manager가 DRIVER_OBJECT 구조체를 만든다는 점, driver object가 driver의 표준 routine 묶음으로의 입구(dispatch table인 MajorFunction 배열을 포함)를 유지한다는 점, I/O Manager가 이 표를 써서 요청에 대응하는 처리 함수를 호출한다는 점에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, Introduction to device objects. DEVICE_OBJECT 구조체가 논리·가상·물리 device를 나타내고 I/O 요청의 목적지(target)가 된다는 점, driver가 IoCreateDevice로 device object를 만든다는 점, device object가 자신을 만든 driver(driver object)와 묶여 있다는 점에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, Using files in a driver. 커널에서 file object가 「열린 파일(또는 device)의 인스턴스」를 나타낸다는 점, 파일을 열 때마다 file object가 만들어지고, 열 때의 context(현재 바이트 offset 등)를 유지한다는 점에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, File handles. CreateFile이 반환하는 file handle이 프로세스에 고유하고 열린 file object와 묶여 있다는 점, 같은 파일을 여러 번 열면 각각 별도 handle(과 연 상태)이 된다는 점, handle이 필요 없어지면 CloseHandle로 닫아야 한다는 점에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, Completing IRPs. I/O 조작을 완료시키는 것은 IoCompleteRequest 호출이라는 점, 완료 시에는 stack 상위 driver가 등록한 IoCompletion routine이 차례로 호출된다는 점, 요청의 완료가 발행과는 다른 타이밍에 일어나고 최종적으로 요청자에게 상태가 반환되기까지의 흐름에 대해. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, Synchronous and asynchronous I/O. 동기 I/O에서는 함수가 I/O 완료까지 반환되지 않고 thread가 기다리게 되는 반면, 비동기 I/O(overlapped I/O)에서는 요청을 발행한 함수가 바로 반환되어 thread가 다른 일을 계속할 수 있다는 점, 비동기 I/O에는 FILE_FLAG_OVERLAPPED를 지정해 handle을 열 필요가 있다는 점, 완료 통지를 받는 여러 방법이 있다는 점에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, IRP_MJ_CLEANUP. 이 요청의 수신이 「대상 device object에 연관된 file object의 마지막 handle이 닫혔다」는 것을 나타낸다는 점, 다만 미처리 I/O 요청 때문에 file object의 해제는 아직일 수 있다는 점, 이 IRP가 handle을 닫은 프로세스의 context에서 보내진다는 점에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, IRP_MJ_CLOSE. 이 요청의 수신이 「file object의 reference count가 0이 되어 file object가 해제되려 한다」는 것을 나타낸다는 점, cleanup 요청 뒤에 보내지지만 미처리 I/O의 완료를 기다리므로 즉시 이어진다고 단정할 수는 없다는 점에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, WinObj - Sysinternals. WinObj가 NT Object Manager namespace를 표시하는 도구이며, device object나 symbolic link를 포함한 namespace 안의 object를 열람할 수 있다는 점에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, CreateFileW function. CreateFile이 파일뿐 아니라 물리 디스크·volume·콘솔·통신 포트(COM port)·pipe 등의 device를 열어 handle을 반환할 수 있다는 점, device를 열 때 “\\.\” 형식의 이름을 쓴다는 점, FILE_FLAG_OVERLAPPED를 비롯한 각종 flag의 의미에 대해. ↩ ↩2
-
Microsoft Learn, IRP major function codes. IRP의 major function code(IRP_MJ_CREATE, IRP_MJ_READ, IRP_MJ_WRITE, IRP_MJ_CLEANUP, IRP_MJ_CLOSE, IRP_MJ_DEVICE_CONTROL, IRP_MJ_PNP 등) 목록과, 각 요청이 무엇을 의미하고 어느 driver가 대응해야 하는지에 대해. ↩
-
Microsoft Learn, I/O stack locations. I/O Manager가 layered driver 체인의 driver마다 IRP 안에 I/O stack location을 마련한다는 점, 각 stack location에 major/minor function code와 그 요청의 parameter가 들어간다는 점, 각 driver가 IoGetCurrentIrpStackLocation으로 자기용 stack location을 얻어 요청 내용을 안다는 점에 대해. ↩
-
Microsoft Learn, Device nodes and device stacks. device object가 쌓여 device stack을 구성한다는 점, IRP가 먼저 stack 최상위 device object로 보내지고 각 계층에서 처리 또는 하위로의 전달이 이루어진다는 점, filter driver의 device object가 stack에 끼는 형태로 존재한다는 점에 대해. ↩ ↩2
-
Microsoft Learn, Filter Manager Concepts. Filter Manager가 Windows에 포함된 커널 모드 driver이고, minifilter driver가 file system으로의 I/O 요청에 대해 사전(pre)·사후(post) callback으로 끼어들 수 있다는 점, 각 minifilter의 끼어들 위치(altitude)에 따라 I/O stack 안의 순서가 정해진다는 점에 대해. ↩
관련 기사
같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.
Windows I/O의 심층(제4회) ── 캐시 관리자: WriteFile은 언제 디스크에 도달하는가
Windows의 캐시 관리자를 그림으로 설명하는 연재 제4회입니다. 파일 매핑으로 구현된 캐시, read-ahead와 lazy write, FlushFileBuffers와 FILE_FLAG_NO_BUFFERING의 용도 구분, 전원 차단으로 데이...
Windows I/O의 심층(제2회) ── 동기 I/O와 비동기 I/O: OVERLAPPED의 진짜 의미
Windows의 동기 I/O와 비동기 I/O(overlapped I/O)를 그림으로 설명하는 연재의 제2회입니다. FILE_FLAG_OVERLAPPED의 의미, 완료 통지의 네 가지 방식, 비동기인데도 동기 완료되는 조건, 취소 절차, .NET과...
Windows I/O의 심층(제5회) ── NTFS의 내부 구조: MFT로 이해하는 파일 시스템
NTFS의 내부 구조를 그림으로 설명하는 연재 제5회입니다. MFT와 파일 레코드, 다중 데이터 스트림(Zone.Identifier), 하드 링크와 8.3 이름, reparse point, 두 종류의 저널, 스파스와 압축까지를 개발자 관점에서 정...
Windows I/O의 심층(제3회) ── I/O 완료 포트(IOCP)와 .NET 스레드 풀: async/await의 지하실
I/O 완료 포트(IOCP)를 그림으로 설명하는 연재의 제3회입니다. 완료 큐와 스레드 수 제어를 하나로 묶은 설계, 동시성 값과 LIFO 해제, .NET 스레드 풀과 async/await 계속(continuation)의 실행 스레드까지 정리합니다.
Windows impersonation token을 올바르게 다루기 ── 스레드 단위 권한 차용과 안전한 되돌리기
Windows impersonation token에 대해 access token, primary token, thread token, impersonation level, RevertToSelf, .NET의 WindowsIdentity.RunIm...
관련 토픽
이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.
Windows 기술 토픽
Windows 개발, 장애 조사, 기존 자산 활용에 관한 KomuraSoft LLC 기사를 모은 토픽 허브입니다.
장애 조사 & 장기 가동 장애
간헐적 장애, 통신 진단, 장기 가동 크래시, 실패 경로 테스트 기반을 정리한 토픽 페이지입니다.
이 주제와 연결되는 서비스
이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.
Windows 앱 개발
상주 처리, 장비 연동, 운영 로그, 유지 보수 가능한 구조가 필요한 Windows 데스크톱 애플리케이션을 지원합니다.
장애 조사 & 원인 분석
간헐적 장애, 장기 가동 중 크래시, 누수, 통신 중단 등 까다로운 프로덕션 이슈를 조사합니다.
자주 묻는 질문
이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.
- IRP란 무엇입니까?
- IRP(I/O Request Packet)는 Windows 커널의 I/O Manager가 애플리케이션의 읽기·쓰기 요청 등을 담아 device driver에 넘기기 위한 패킷입니다. Microsoft의 driver 개발 문서는, device driver로 가는 요청의 대부분이 IRP에 담긴다고 설명합니다. IRP는 요청 종류(생성·읽기·쓰기·cleanup 등)를 나타내는 major function code와, 거치는 driver마다의 stack location을 가지며, device stack을 위에서 아래로 전달되면서 처리됩니다. 각 driver는 IRP를 직접 완료하거나, 아래 driver로 넘기거나, pending으로 두었다가 나중에 완료합니다. 앱 개발자가 IRP를 직접 다루지는 않지만, Process Monitor의 Operation 열에 나오는 IRP_MJ_READ 같은 표기는 바로 이 구조입니다.
- Windows에서는 파일도 시리얼 포트도 프린터도 같은 CreateFile로 여는 이유는 무엇입니까?
- CreateFile에 넘긴 이름이 모두 Object Manager namespace에서 최종적으로 device object로 해석되고, I/O Manager가 그 device에 묶인 file object를 만들어 handle을 반환하는, 같은 경로를 따르기 때문입니다. C: 같은 drive letter의 실체는 \\Device\\HarddiskVolume3 같은 NT device name으로의 symbolic link이고, \\\\.\\COM1 같은 지정도 마찬가지로 시리얼 포트의 device object로 해석됩니다. 해석 대상이 어떤 device이든, 그 이후 요청은 모두 IRP라는 같은 형식에 담겨 driver에 도착하므로, 파일도 device도 같은 API로 열어 읽고 쓸 수 있습니다. UNC path도 같은 구조이고, network redirector의 device로 해석될 뿐입니다. 이 「namespace + packet」 설계가 Windows I/O 일관성의 정체입니다.
- CloseHandle을 호출했는데도 파일이 바로 해제되지 않는 경우가 있는 이유는 무엇입니까?
- CloseHandle이 하는 일은 「handle을 하나 반환하는 것」이지, 「파일을 닫는 것」이 아니기 때문입니다. 커널 안의 file object에는 handle 수(handle count)와, 커널 구성 요소로부터의 참조 수(reference count)라는 두 카운트가 있습니다. 마지막 handle이 닫히면 file system에는 IRP_MJ_CLEANUP이 전달되지만, 미완료 I/O나 memory-mapped file의 section처럼 커널 안의 참조가 남아 있는 동안은 file object 자체가 살아 있고, reference count가 0이 되어야 비로소 IRP_MJ_CLOSE가 전달됩니다. memory-mapped file을 사용한 뒤에 파일을 삭제하지 못하거나, 앱을 닫았는데도 「파일이 사용 중」이라고 나오는 현상은 상당수가 이 2단계 구조로 설명됩니다.
- IRP나 device stack 지식은 앱 개발자에게 어떤 도움이 됩니까?
- IRP를 직접 작성하지는 않더라도, 조사와 설계 양쪽에서 효과가 있습니다. 먼저 Process Monitor의 Operation 열(상세 출력을 켠 경우)은 IRP_MJ_CREATE나 IRP_MJ_READ 같은 IRP 용어 그대로 표시되므로, 이 계층의 말을 알면 로그를 읽을 수 있습니다. 다음으로, 백신 소프트웨어 같은 filter driver가 모든 파일 I/O 경로에 끼어 있다는 점을 알면, 「특정 환경에서만 파일 접근이 느리다」는 문제를 조사할 때 실마리를 잡을 수 있습니다. 더 나아가 Windows I/O가 발행과 완료를 분리할 수 있는 구조이고, 동기 I/O는 「완료되기 전에는 반환되지 않는다」는 보장에 지나지 않는다고 이해하면, 비동기 I/O나 I/O completion port, .NET의 async/await 동작을 구조에서부터 납득하고 쓸 수 있습니다.