수정 이력(1건, 최종 수정 2026년 09월 03일)
이 글에 적용한 변경 사항의 기록입니다. 보관해 둔 수정 전 버전은 DOI가 부여된 고정 URL에서 읽을 수 있습니다.
- 한국어 전면 재작성에 맞춰 본문 표현을 바로잡았습니다. 기술적인 주장은 일본어판과 같습니다.
- 최초 공개
이 글을 인용하기(DOI(등록된 아카이브): 10.5281/zenodo.22176084)
아래 DOI는 이전에 등록된 아카이브를 가리키며 현재 본문과 다를 수 있습니다. 현재 본문을 참조할 때는 이 페이지의 URL을 사용하세요.
Go Komura (2026). 「Windows 메모리의 심층(제1회) ── 가상 주소가 물리 RAM으로 바뀌는 순간: 페이지 폴트의 전말」. 합동회사 코무라소프트. https://comcomponent.com/ko/blog/windows-memory-internals-page-fault/
- DOI(등록된 아카이브)
- 10.5281/zenodo.22176084
- DOI(마지막 등록 버전)
- 10.5281/zenodo.22176085
VirtualAlloc에 MEM_COMMIT을 넘기면 그 순간에 Commit은 늘어납니다. 그러나 Working Set이 같은 만큼 늘어난다고는 할 수 없습니다. 그렇다면 확보했다고 생각한 메모리는 어디에 있을까요?
답은 「대부분의 페이지에는 아직 대응하는 물리 RAM이 없다」입니다. Windows는 애플리케이션이 실제로 페이지에 접근할 때까지 물리 페이지 할당을 미룹니다. 첫 접근에서 CPU가 페이지 폴트를 일으키면, 메모리 관리자가 VAD, PTE, 보호 속성, backing store를 조사하고, 필요하면 RAM을 한 페이지씩 연결합니다.1
이 글에서는 이 「처음 건드린 1바이트」가 물리 RAM에 도달하기까지의 경로를 따라갑니다. Working Set이나 Commit 같은 숫자의 의미를 먼저 정리하고 싶다면 도입편 「Windows의 『메모리 사용량』은 무엇을 나타내는가 ── Working Set·Private Bytes·Commit·페이지 파일을 올바르게 읽는 법」을 먼저 읽으면 됩니다. 이 연재는 거기서 사용한 용어를 다시 정의하지 않고, 「왜 그 숫자가 나오는지」를 메커니즘 쪽에서 파고듭니다.
「Windows 메모리의 심층」 전 3회
- 제1회(이 글): 가상 주소와 페이지 폴트
VirtualAlloc한 영역이 언제 물리 RAM을 얻는지 따라갑니다. - 제2회: 물리 페이지의 일생
Working Set에서 벗어난 페이지가 Modified, Standby, Free, Zeroed를 어떻게 이동하는지 따라갑니다. - 제3회: 섹션 객체와 Copy-on-Write
DLL, 파일 매핑, 공유 메모리가 물리 페이지를 공유할 수 있는 이유를 따라갑니다.
제1회가 답하는 질문은 단 하나입니다.
Commit된 가상 주소는 어느 순간에 물리 RAM으로 바뀌는가.
대상 독자는 Windows 앱의 메모리 사용량, 시작 직후의 페이지 폴트, 0xC0000005, VMMap과 PerfMon의 숫자를 메커니즘부터 이해하고 싶은 개발자와 운영 담당자입니다. 전제 환경은 Windows 10/11 또는 현행 Windows Server이고, 전제 지식은 포인터와 VirtualAlloc의 기초이며, 페이지 테이블의 비트 배치나 커널 디버거 경험은 필요하지 않습니다. 난이도는 중급입니다. 내부 구조의 이름은 다루지만, 특정 Windows 빌드에 의존하는 비공개 레이아웃은 전제로 하지 않습니다.
그림의 실선은 항상 성립하는 관계, 점선은 조건이 붙는 관계입니다(성립 조건은 상세 페이지의 관계별 설명에 적혀 있습니다). 관계 전체 목록(총 14건, 근거와 확신도 포함)과 주요 개념의 정의는 지식 맵 상세 페이지에 정리되어 있습니다(일본어). 데이터: JSON-LD / Turtle
1. 먼저 결론
일반적인 프라이빗 메모리의 흐름을 한 줄로 정리하면 다음과 같습니다.
Reserve는 가상 주소 범위를 확보하고, Commit은 앞으로 내용을 유지할 수 있게 commit charge를 부과하며, 첫 접근에서 일어나는 페이지 폴트가 물리 페이지를 할당합니다.
즉 MEM_COMMIT은 「지금 당장 RAM을 확보하라」는 명령이 아닙니다. Microsoft의 VirtualAlloc 문서도 커밋된 페이지의 초기 내용이 0임을 보장하는 한편, 실제 물리 페이지는 가상 주소에 접근될 때까지 할당되지 않는다고 설명합니다.1
그렇다고 「Reserve/Commit은 VAD에 기록할 뿐」이라고 단정하는 것도 정확하지 않습니다. 실제로는 Reserve에서 주로 가상 주소 범위와 속성을 나타내는 VAD가 만들어지고, Commit에서 시스템의 Commit Total이 늘며 범위의 커밋 상태가 기록됩니다. 페이지 테이블의 중간 계층과 개별 PTE는 필요해진 시점에 지연 구축되고, 물리 RAM과의 최종 연결은 보통 첫 접근 때 일어납니다.
Commit은 빈 약속이 아닙니다. 앞으로 그 내용을 RAM 또는 적절한 backing store에 유지할 수 있다는, 시스템 전체의 약속입니다. 그 약속을 한 페이지씩 실체화하는 입구가 페이지 폴트입니다.
flowchart TB
accTitle: Reserve, Commit, 첫 접근에서 일어나는 일
accDescr: MEM_RESERVE는 범위와 속성을 VAD에 기록하고, MEM_COMMIT은 Commit Total을 소비해 저장을 약속하며, 첫 접근의 페이지 폴트가 물리 페이지를 할당해 Working Set에 넣는다
reserve["1. MEM_RESERVE"] --> commit["2. MEM_COMMIT"]
commit --> touch["3. 첫 접근(Touch)"]
reserve -.-> vad["범위와 속성을 VAD에 기록"]
commit -.-> charge["Commit Total을 소비(아직 물리 페이지 없음)"]
touch --> fault["페이지 폴트"]
fault --> zero["0으로 채운 물리 페이지를 PTE에 연결"]
zero --> ws["Working Set에 추가하고 명령을 다시 실행"]
그림 1: Reserve, Commit, Touch는 서로 다른 사건입니다. 물리 RAM이 연결되는 것은 마지막인 첫 접근 때입니다.
2. 가상 페이지를 추적하는 세 가지 원장
가상 주소에서 물리 RAM까지의 흐름을 이해하려면, Windows가 가진 세 종류의 원장을 구분해 둘 필요가 있습니다.
| 원장 | 단위 | 역할 |
|---|---|---|
| VAD | 가상 주소 범위 | 영역의 종류, Reserve/Commit, 보호, 섹션 대응을 관리 |
| 페이지 테이블/PTE | 가상 페이지 | 현재의 물리 페이지 변환, 또는 아직 실체화되지 않은 상태를 표현 |
| PFN 데이터베이스 | 물리 페이지 | 각 RAM 페이지의 소유, 참조, 상태를 추적 |
VAD는 범위, PTE는 가상 페이지, PFN 데이터베이스는 물리 페이지의 정보를 가집니다. 페이지 폴트 핸들러는 이들을 대조해 접근을 계속할 수 있는지를 판단합니다.
flowchart TB
accTitle: 가상 주소에서 물리 RAM까지의 세 가지 원장
accDescr: 가상 주소는 VAD가 범위 단위로, PTE가 가상 페이지 단위로 관리하고, PTE의 변환 대상인 물리 페이지를 PFN 데이터베이스가 물리 페이지 단위로 추적한다
va["가상 주소"] --> vad["VAD(범위 원장)"]
va --> pte["PTE(가상 페이지 원장)"]
vad -.->|Reserve/Commit·보호를 판정| pte
pte -->|유효한 변환| pfn["PFN 데이터베이스(물리 페이지 원장)"]
pfn --> ram["물리 RAM 페이지"]
그림 2: 입도가 다른 세 가지 원장. 폴트 처리는 VAD와 PTE를 대조하고, 결과를 PFN 쪽에 반영합니다.
이 글의 주역은 VAD와 PTE입니다. PFN 데이터베이스는 제2회에서 물리 페이지 쪽에서 다룹니다.
3. Reserve, Commit, Touch는 서로 다른 사건
3.1. Reserve ── 주소를 잡아 두기
먼저 256MiB의 연속된 가상 주소 범위를 예약해 봅니다.
void* base = VirtualAlloc(
nullptr,
256ull * 1024 * 1024,
MEM_RESERVE,
PAGE_NOACCESS);
이 시점에 일어난 일은, 이 범위를 다른 할당에 쓰지 못하도록 프로세스의 가상 공간에 주소를 확보한 것뿐입니다. MEM_RESERVE는 RAM에도 페이지 파일에도 물리 저장 영역을 할당하지 않습니다.1
64비트 프로세스에서는 가상 공간이 넓으므로, 큰 범위를 먼저 Reserve해 두고 필요한 부분만 나중에 Commit하는 설계가 현실적입니다.
3.2. Commit ── 저장할 수 있음을 약속하기
다음으로 예약해 둔 범위를 Commit합니다.
void* committed = VirtualAlloc(
base,
256ull * 1024 * 1024,
MEM_COMMIT,
PAGE_READWRITE);
성공하면 시스템의 Commit Total과, 보통은 프로세스의 Private Bytes에 반영되는 약속량이 늘어납니다. 그래도 256MiB분의 물리 페이지가 한꺼번에 늘어서는 것은 아닙니다. 일반적인 페이지는 첫 접근까지 물리적으로 할당되지 않은 채로 남습니다.12
그렇다면 Commit에는 무슨 의미가 있을까요. 시스템이 약속을 받아들일 수 없을 때, 메모리를 사용하는 도중에가 아니라 Commit 시점에 실패를 반환할 수 있다는 점입니다.
3.3. Touch ── 물리 페이지가 필요해지는 때
마지막으로 다음 대입으로 선두 페이지에 처음 씁니다.
static_cast<unsigned char*>(base)[0] = 1;
CPU는 가상 주소를 물리 주소로 변환하려 하지만, PTE에는 아직 유효한 물리 페이지 변환이 없습니다. 여기서 페이지 폴트가 발생합니다.
제어를 받은 메모리 관리자는 「Commit되어 있고 쓰기가 가능한 프라이빗 페이지에 대한 첫 접근」으로 판정하고, 0으로 채운 물리 페이지를 확보해 PTE에 연결한 뒤 Working Set에 넣습니다. 그다음 실패한 쓰기 명령을 다시 실행하게 합니다.
앱에서 보이는 것은 단순한 대입이지만, 내부에서는 대입 도중에 커널로 들어가 물리 페이지를 할당하고 같은 명령으로 돌아오는 처리가 진행됩니다.
4. VAD ── 가상 공간의 범위 원장
VAD는 Virtual Address Descriptor의 약자로, Windows는 프로세스가 사용 중인 주소 범위를 VAD 트리로 관리합니다. WinDbg의 !vad 명령을 쓰면 시작·종료 VPN, Commit, 보호 속성, Private/Mapped, Control Area 등을 확인할 수 있습니다.3
VAD가 나타내는 대표적인 정보는 다음과 같습니다.
- 주소 범위의 시작과 끝
- Private, Mapped, Image 등의 종류
- Reserve/Commit 상태
- 읽기, 쓰기, 실행, Copy-on-Write 등의 보호
- 파일이나 섹션과의 대응
- 가드 페이지 등의 특수 속성
범위 단위로 관리하는 이유는 효율입니다. 256MiB는 4KiB 페이지로 환산하면 65,536페이지입니다. 처음부터 모든 페이지에 완전한 관리 구조를 만들기보다, 「이 연속 범위는 하나의 예약」이라는 정보를 VAD에 두고 필요해진 페이지부터 구체화하는 편이 낭비가 없습니다.
4.1. VAD를 찾았다고 반드시 복구되는 것은 아니다
「VAD에 있으면 폴트를 해결하고, 없으면 접근 위반」이라는 설명은 입구로는 편리하지만 단순화가 지나칩니다. VAD를 찾았더라도, 예를 들어 다음 경우에는 일반적인 접근을 이어갈 수 없습니다.
- Reserve만 되어 있고 대상 페이지가 Commit되지 않음
PAGE_NOACCESS임- 읽기 전용 페이지에 씀
- 실행 불가 페이지에서 명령을 실행함
- 가드 페이지에 처음 접근함
- 섹션의 유효 범위 밖에 접근함
반대로 PTE가 무효여도, VAD와 PTE의 소프트웨어 상태에서 정당한 접근임이 드러나면 demand-zero, Transition 복귀, 페이지 인, CoW로 해결할 수 있습니다. 정확히 말하면 VAD, PTE, 보호 속성, 접근 종류를 함께 판정한다가 답입니다.
5. 페이지 테이블과 TLB
앱이 가진 포인터는 가상 주소입니다. CPU가 RAM에 접근하려면 가상 페이지 번호를 물리 페이지 번호로 변환해야 합니다. 그 계층적 변환 표가 페이지 테이블이고, 말단 항목이 PTE(Page Table Entry)입니다.
유효한 PTE는 개념적으로 PFN, 읽기·쓰기·실행 보호, 사용자 모드 접근 가능 여부, Accessed/Dirty 등의 정보를 가집니다. 실제 비트 배치는 CPU와 Windows 버전에 따라 달라집니다.
다만 매번 페이지 테이블을 따라가는 것은 너무 느리므로, CPU는 최근 변환 결과를 TLB(Translation Lookaside Buffer)에 캐시합니다. 주소 변환은 다음 순서로 진행됩니다.
- TLB에 변환이 있고 접근이 그 보호에 맞으면 그 결과를 사용합니다.
- TLB에 변환이 없으면 CPU가 페이지 테이블을 따라갑니다.
- 유효한 PTE가 있고 보호에도 맞으면 TLB에 등록하고 계속합니다.
- 유효한 변환이 없거나 보호 위반이면 페이지 폴트 입구로 들어갑니다. 보호 검사는 변환이 TLB에서 온 경우에도 수행됩니다.
이 흐름에서 알 수 있듯이 TLB miss와 페이지 폴트는 다른 현상입니다. TLB에 변환이 없을 뿐이고 PTE가 유효하면, 페이지 테이블 워크만으로 처리가 끝납니다. 반대로 TLB에 변환이 있어도, 읽기 전용 페이지에 쓰기나 실행 불가 페이지에서 명령 실행 같은 보호 위반은 페이지 폴트 입구로 들어갑니다. CoW 페이지에 대한 쓰기가 변환이 캐시된 상태에서도 폴트할 수 있는 것은 이 때문입니다.
flowchart TB
accTitle: 주소 변환의 흐름과 페이지 폴트 입구
accDescr: TLB에 변환이 있어도 보호에 맞지 않으면 페이지 폴트 입구로 들어간다. TLB에 변환이 없으면 페이지 테이블을 따라가고, 유효한 PTE이며 보호에도 맞으면 TLB에 등록하고 계속하며, 무효이거나 보호 위반이면 페이지 폴트 입구로 들어간다
access["메모리 접근"] --> tlb{"TLB에 변환이 있는가?"}
tlb -->|있음| perm{"보호에 맞는가?"}
perm -->|맞음| go["그 변환으로 계속"]
perm -->|보호 위반| entry["페이지 폴트 입구로"]
tlb -->|없음| walk["페이지 테이블 워크"]
walk --> valid{"유효한 PTE이며 보호에도 맞는가?"}
valid -->|예| register["TLB에 등록하고 계속(폴트 없음)"]
valid -->|무효 또는 보호 위반| entry
그림 3: TLB miss는 페이지 테이블 워크로 해결할 수 있습니다. 페이지 폴트로 들어가는 것은 변환이 무효이거나 보호 위반일 때이며, 보호 위반은 TLB hit에서도 일어납니다.
5.1. 무효 PTE는 빈칸이 아니다
무효 PTE라고 해서 내용이 비어 있는 것은 아닙니다. Windows는 무효 PTE의 소프트웨어 상태에서 예를 들어 다음 경우를 구분합니다.
- 한 번도 실체화하지 않은 demand-zero 페이지
- RAM에 남아 있는 Transition 페이지
- Prototype PTE를 참조하는 공유 페이지
- 페이지 파일에 저장된 프라이빗 페이지
- 보호 위반이나 무효 영역
CPU의 일은 「일반적인 유효 변환이 아니다」라고 판단해 커널에 넘기는 것까지이고, 그다음 의미 부여는 메모리 관리자가 합니다.
6. 페이지 폴트의 전말
Commit된 프라이빗 페이지에 대한 첫 쓰기를 6단계로 따라갑니다.
- CPU가 쓰기를 시도한다.
TLB와 페이지 테이블을 조사하지만, 대상 PTE에 유효한 PFN이 없습니다. - CPU가 페이지 폴트를 일으킨다.
폴트가 난 가상 주소, 읽기·쓰기·실행의 종류, 사용자/커널, 변환 부재인지 보호 위반인지를 커널에 넘깁니다. - 메모리 관리자가 VAD와 PTE를 조사한다.
Commit되었는지, 보호에 맞는지, demand-zero·Transition·공유·페이지 인·CoW·예외 중 무엇인지를 정합니다. - demand-zero이면 0으로 채운 물리 페이지를 얻는다.
다른 프로세스의 데이터가 새어 나오지 않도록, 새로 넘기는 페이지는 0이어야 합니다. - PTE와 PFN 관리 정보를 갱신한다.
PTE에 PFN과 보호를 설정하고, 물리 페이지를 Active로 만든 뒤, 프로세스의 Working Set에 넣습니다. - 실패한 명령을 다시 실행한다.
정상적으로 해결되었으므로 사용자 모드 예외는 전달되지 않고, 앱은 평범한 대입으로 처리를 이어갑니다.
ETW의 페이지 폴트 이벤트도 Transition, Demand Zero, Copy-on-Write, Guard Page, Hard Page Fault, Access Violation을 서로 다른 종류로 기록합니다.4
즉 페이지 폴트는 처음부터 「이상」을 뜻하는 말이 아닙니다. CPU가 일반 경로로 변환하지 못했을 때 OS에 판단을 맡기기 위한 공통 입구입니다.
flowchart LR
accTitle: 페이지 폴트 해결 경로의 분기
accDescr: 메모리 관리자는 VAD, PTE, 보호 속성, 접근 종류를 판정해 demand-zero, RAM 안 페이지의 재연결, backing store를 읽는 하드 폴트, Copy-on-Write, 가드 페이지 통지, 예외 중 하나로 나눈다
faultIn["페이지 폴트 발생"] --> judge["VAD·PTE·보호·종류를 판정"]
judge -->|첫 접근| dz["demand-zero(소프트)"]
judge -->|RAM에 잔존| soft["Standby 등에서 재연결(소프트)"]
judge -->|디스크 읽기 필요| hard["하드 폴트(디스크 I/O)"]
judge -->|CoW 쓰기| cow["복사하고 PTE를 교체"]
judge -->|가드 페이지| guard["가드를 해제하고 통지"]
judge -->|해결 불가| av["예외(0xC0000005 등)"]
그림 4: 같은 입구로 들어온 폴트가 판정 결과에 따라 6종류의 결말로 갈립니다. 가드 페이지의 자세한 내용은 제9절에서 다룹니다.
7. demand-zero ── 디스크를 읽지 않는 소프트 폴트
demand-zero는 Commit된 프라이빗 페이지에 처음 접근할 때 일어나는 대표적인 소프트 폴트입니다. Microsoft의 Working Set 해설도 「할당된 가상 페이지를 프로세스가 처음 참조하는」 경우를 소프트 폴트의 예로 듭니다.5
demand-zero에는 다음 특징이 있습니다.
- 원본 데이터를 디스크에서 읽을 필요가 없다
- 초기 내용은 0이다
- 사용 가능한 물리 페이지를 연결한다
- Working Set과 누적 Page Fault Count는 늘어난다
- 이 처리만으로는
Memory\\Pages Input/sec는 늘어나지 않는다
이 때문에 시작 직후 Page Faults/sec가 뛰더라도, 그것만으로 스토리지가 막혀 있다고 말할 수는 없습니다.
지연 할당의 장단도 정리해 둡니다. 256MiB를 Commit하고 실제로 쓰는 양이 8MiB뿐이라면, 나머지 248MiB를 RAM에 두지 않는 지연 할당은 합리적입니다. 그 대신 첫 접근에는 폴트 처리 비용이 붙습니다. 지연 시간에 민감한 처리에서는 시작 전에 각 페이지에 접근해 미리 폴트를 일으켜 두는 설계도 있지만, 이는 RAM 상주량을 먼저 늘리는 트레이드오프입니다.
8. 소프트 폴트와 하드 폴트
8.1. 소프트 폴트
소프트 폴트는 backing store에 대한 읽기 I/O 없이 해결할 수 있는 폴트입니다. 대표 예는 다음과 같습니다.
- demand-zero
- Standby/Transition에 남은 페이지의 재연결
- 다른 프로세스의 Working Set에 있는 공유 페이지의 연결
- 미리 읽어 둔 페이지의 연결
- 원본 페이지가 상주하는 Copy-on-Write
커널 전환, 잠금, PTE/PFN 갱신, TLB 일관성 등의 CPU 비용은 들지만, 스토리지 대기는 없습니다.5
8.2. 하드 폴트
반면 필요한 페이지가 RAM 어디에도 없어 backing store에서 읽어야 하는 경우가 하드 폴트입니다. 이때 읽기 원본은 페이지 파일만이 아닙니다.
- 페이지 파일로 내보낸 프라이빗 페이지
- 메모리 매핑 파일
- EXE나 DLL의 이미지
- 파일 캐시가 참조하는 데이터 파일
ETW의 HardFault 이벤트에는 FileObject, ReadOffset, ByteCount가 포함되어 있어, 실제 읽기 원본을 추적할 수 있습니다.6
따라서 Hard Fault = pagefile.sys를 읽었다가 아닙니다.
backing store 읽기가 필요해지면 요청은 Windows의 I/O 스택으로 들어갑니다. IRP와 발행·완료의 흐름은 「Windows I/O의 심층(제1회)」, 파일 캐시와의 합류는 「Windows I/O의 심층(제4회)」에서 다룹니다. 페이지가 RAM에 있으면 메모리 관리자만으로 돌아올 수 있지만, 없으면 I/O를 발행하고 완료될 때까지 폴트가 난 스레드를 기다리게 됩니다.
9. 해결할 수 없는 폴트는 예외가 된다
VAD와 PTE를 조사해도 정당한 할당·페이지 인·CoW로 해결할 수 없는 폴트는 사용자 모드로 예외가 전달됩니다.
그 대표가 STATUS_ACCESS_VIOLATION, 예외 코드 0xC0000005입니다. 무효한 주소의 읽기, 쓰기, 실행에서 발생하며, 제1 예외 매개변수가 접근 종류, 제2 매개변수가 위반 주소를 나타냅니다.7
전형적인 발생 패턴은 다음과 같습니다.
- NULL, 해제 후, 배열 밖 주소를 읽는다
- 읽기 전용 페이지에 쓴다
- DEP/NX로 실행 불가인 페이지에서 명령을 실행한다
- Commit되지 않은 Reserve 범위에 접근한다
한편 PAGE_GUARD는 의미가 조금 다릅니다. 접근을 한 번만 알리는 장치로, STATUS_GUARD_PAGE_VIOLATION을 일으키며 스택 확장 등에 쓰입니다.8
정상적인 지연 할당도, 페이지 인도, CoW도, 가드 통지도, 최종 접근 위반도, CPU에서 보면 같은 페이지 폴트 입구로 모입니다. 결말을 정하는 것은 VAD, PTE, 보호 속성, 접근 종류의 조합입니다.
10. 직접 확인해 보기
지금까지의 흐름은 자기 환경에서 관측할 수 있습니다. 다음 C++ 프로그램은 256MiB를 Reserve하고, Commit하고, 각 페이지에 1바이트를 쓴 뒤, 마지막으로 Release합니다. 각 단계에서 Enter 키를 기다리므로, VMMap과 PerfMon으로 변화를 관측할 수 있습니다.
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <psapi.h>
#include <cstdio>
#include <cstdlib>
#pragma comment(lib, "Psapi.lib")
constexpr SIZE_T kSize = 256ull * 1024 * 1024;
void PrintMemory(const char* stage)
{
PROCESS_MEMORY_COUNTERS_EX c{};
c.cb = sizeof(c);
if (!GetProcessMemoryInfo(
GetCurrentProcess(),
reinterpret_cast<PROCESS_MEMORY_COUNTERS*>(&c),
sizeof(c))) {
std::printf("GetProcessMemoryInfo failed: %lu\n", GetLastError());
return;
}
std::printf(
"%-10s WS=%zu MiB Private=%zu MiB Faults=%lu\n",
stage,
c.WorkingSetSize / 1024 / 1024,
c.PrivateUsage / 1024 / 1024,
c.PageFaultCount);
}
void Pause(const char* message)
{
PrintMemory(message);
std::puts("Press Enter...");
(void)std::getchar();
}
int main()
{
SYSTEM_INFO si{};
GetSystemInfo(&si);
std::printf("PID=%lu, page=%lu bytes\n",
GetCurrentProcessId(), si.dwPageSize);
void* base = VirtualAlloc(nullptr, kSize, MEM_RESERVE, PAGE_NOACCESS);
if (!base) {
std::fprintf(stderr, "Reserve failed: %lu\n", GetLastError());
return EXIT_FAILURE;
}
Pause("reserved");
if (!VirtualAlloc(base, kSize, MEM_COMMIT, PAGE_READWRITE)) {
std::fprintf(stderr, "Commit failed: %lu\n", GetLastError());
VirtualFree(base, 0, MEM_RELEASE);
return EXIT_FAILURE;
}
Pause("committed");
auto* bytes = static_cast<volatile unsigned char*>(base);
for (SIZE_T offset = 0; offset < kSize; offset += si.dwPageSize) {
bytes[offset] = 1;
}
Pause("touched");
if (!VirtualFree(base, 0, MEM_RELEASE)) {
std::fprintf(stderr, "Release failed: %lu\n", GetLastError());
return EXIT_FAILURE;
}
Pause("released");
}
Visual Studio의 x64 Native Tools Command Prompt라면 다음 명령으로 빌드할 수 있습니다.
cl /std:c++20 /EHsc /W4 memory_fault_demo.cpp
10.1. VMMap에서 볼 것
VMMap은 예약된 가상 메모리, Commit, Working Set, Private, Shareable을 종류별로 표시하는 도구입니다.9 각 단계에서 기대하는 변화는 다음과 같습니다.
| 단계 | 기대하는 변화 |
|---|---|
| Reserve | Address Space의 Size는 늘지만 Commit/WS는 같은 양만큼 늘지 않음 |
| Commit | Private의 Commit이 약 256MiB 늘어남 |
| Touch | Working Set과 Private WS가 크게 늘고 Fault Count도 늘어남 |
| Release | 대상 범위가 사라지고 Commit과 WS가 내려감 |
실제 수치는 런타임, 보안 제품, 메모리 압력, 관측 시점에 따라 달라집니다. 정확히 256MiB가 되는지가 아니라 단계 사이에서 어느 쪽으로 움직였는지를 보면 됩니다.
10.2. PerfMon에서 소프트/하드를 나누기
PerfMon에서는 다음 카운터를 같은 시간축에 나란히 둡니다.
Process(<대상>)\\Page Faults/secMemory\\Pages Input/secMemory\\Page Reads/secMemory\\Available MBytesProcess(<대상>)\\Working Set - PrivateProcess(<대상>)\\Private Bytes
Process\\Page Faults/sec에는 소프트와 하드가 모두 포함됩니다. 반면 Memory\\Pages Input/sec는 하드 폴트 해결을 위해 디스크에서 읽어 들인 페이지 수입니다.10
이 프로그램의 Touch 단계에서는 Page Faults/sec가 뛰더라도 Pages Input/sec는 크게 늘지 않아야 합니다. 새로 Commit한 페이지는 demand-zero로 실체화되므로, 원본 데이터를 디스크에서 읽을 필요가 없기 때문입니다.
같은 이름 프로세스가 여러 개 있으면 PerfMon의 process#1 같은 번호는 재시작으로 바뀔 수 있습니다. PID를 표시하는 카운터와 대조하거나, Process V2나 ETW/WPA에서 PID로 식별하면 됩니다.
11. 실무에서 피하고 싶은 세 가지 오독
11.1. 「Commit이 늘었으니 RAM 누수」
Commit은 내용을 유지하는 약속량이며, 아직 Touch하지 않은 페이지는 RAM에 상주하지 않는 경우가 있습니다. 누수 판정에서는 Private Bytes의 시간 추이, 할당의 내역, 처리 후에 기준값으로 돌아가는지를 봅니다.
11.2. 「Page Faults/sec가 높으니 디스크가 느리다」
소프트 폴트는 디스크 I/O를 동반하지 않습니다. Page Faults/sec, Pages Input/sec, 스토리지 대기를 나눠 보고, 필요하면 ETW의 HardFault 이벤트로 읽기 원본 파일과 스택까지 추적합니다.
11.3. 「Working Set을 비우면 누수가 고쳐진다」
Working Set에서 빼더라도 Commit이나 소유권은 해제되지 않습니다. 페이지는 Standby나 Modified로 옮겨 갔다가, 나중에 폴트해서 돌아올 뿐입니다. 누수를 고치려면 할당을 한 쪽이 VirtualFree, 힙 해제, 객체 소멸 등을 수행해야 합니다.
빠진 그 물리 페이지가 어디로 가는지는 제2회에서 따라갑니다.
12. 정리
MEM_RESERVE는 가상 주소 범위를 잡아 두지만, RAM이나 페이지 파일의 물리 영역은 할당하지 않습니다.1MEM_COMMIT은 Commit을 소비하고 앞으로 내용을 유지할 수 있음을 보장하지만, 일반적인 물리 페이지는 첫 접근까지 할당되지 않습니다.12- VAD는 범위, PTE는 가상 페이지, PFN 데이터베이스는 물리 페이지의 원장입니다.
- TLB miss는 페이지 폴트가 아닙니다. 유효한 PTE가 있으면 페이지 테이블 워크만으로 해결됩니다.
- demand-zero, Transition 복귀, 공유 페이지 연결은 디스크 I/O 없이 해결할 수 있는 소프트 폴트입니다.5
- 페이지 파일, DLL, EXE, 매핑된 파일에서 읽어야 하면 하드 폴트입니다.6
- VAD, PTE, 보호 속성을 검사해 해결할 수 없으면
0xC0000005등의 예외가 됩니다.7 - 성능 판단에서는
Page Faults/sec단독이 아니라Pages Input/sec, Available, Working Set, Private Bytes, 스토리지 대기를 같은 시간축에서 봅니다.
이어지는 글은 제2회 「물리 페이지의 일생: 다섯 목록과 페이지 파일의 진실」입니다.
Commit이라는 약속을 물리 페이지로 바꾼 뒤, 그 페이지가 Working Set에서 빠지면 어디로 가는지를 PFN 데이터베이스와 페이지 목록에서 따라갑니다.
관련 기사
- Windows의 「메모리 사용량」은 무엇을 나타내는가 ── Working Set·Private Bytes·Commit·페이지 파일을 올바르게 읽는 법
- Windows I/O의 심층(제1회) ── Windows의 I/O 아키텍처와 IRP
- Windows I/O의 심층(제4회) ── 캐시 관리자와 WriteFile
- WinDbg + SOS로 크래시 덤프 분석
- Windows 앱의 크래시 덤프 수집 입문
관련 상담 영역
합동회사 코무라소프트는 Windows 애플리케이션의 메모리 사용량 조사, 접근 위반, 시작 지연, 페이징, 네이티브 코드 결함 분석을 다룹니다.
참고 링크
-
Microsoft Learn, VirtualAlloc function.
MEM_RESERVE가 물리 스토리지를 할당하지 않고 가상 주소 범위를 예약하는 점,MEM_COMMIT이 시스템 전체 메모리와 페이지 파일에 대해 commit charge를 부과하는 점, 커밋된 페이지의 초기 내용이 0인 점, 실제 물리 페이지는 접근될 때까지 할당되지 않는 점에 대해. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 -
Microsoft Learn, PERFORMANCE_INFORMATION structure.
CommitTotal이 현재 시스템 Commit 페이지 수이고,CommitLimit이 페이지 파일을 확장하지 않고 Commit할 수 있는 상한인 점에 대해. ↩ ↩2 -
Microsoft Learn, !vad (WinDbg).
!vad가 VAD 트리를 표시하고 시작·종료 VPN, Commit, Mapped/Private, 보호 속성, Control Area 등을 확인할 수 있는 점에 대해. ↩ -
Microsoft Learn, PageFault_TypeGroup1 class. ETW가 Transition Fault, Demand Zero Fault, Copy-on-Write, Guard Page Fault, Hard Page Fault, Access Violation을 구분해 기록하는 점에 대해. ↩
-
Microsoft Learn, Working Set. 소프트 폴트가 backing store 접근 없이 해결될 수 있으며, 다른 프로세스의 Working Set, Transition, 첫 참조의 demand-zero 등에서 일어나는 점에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, PageFault_HardFault class. HardFault 이벤트가 FileObject, ReadOffset, ByteCount, VirtualAddress, Thread ID를 포함해 읽기 원본을 추적할 수 있는 점에 대해. ↩ ↩2
-
Microsoft Learn, Access Violation C0000005.
0xC0000005가 무효 메모리 주소의 읽기·쓰기·실행에서 발생하고, 예외 매개변수가 접근 종류와 위반 주소를 나타내는 점에 대해. ↩ ↩2 -
Microsoft Learn, Creating Guard Pages.
PAGE_GUARD가 페이지 접근의 일회성 알림을 제공하고STATUS_GUARD_PAGE_VIOLATION을 일으키는 점에 대해. ↩ -
Microsoft Learn, VMMap - Sysinternals. VMMap이 커밋된 가상 메모리를 종류별로 분해하고, 각 종류의 Working Set과 상세 주소 맵을 표시할 수 있는 점에 대해. ↩
-
Microsoft Learn, Performance Analysis of Logs (PAL) Tool.
Memory\\Pages Input/sec가 하드 페이지 폴트 해결을 위해 디스크에서 읽어 들인 페이지 수인 점에 대해. ↩
관련 기사
같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.
Windows 메모리의 심층(제2회) ── 물리 페이지의 일생: 다섯 목록과 페이지 파일의 진실
PFN 데이터베이스, Standby, Modified, 메모리 압축, 페이지 파일을 이어서 Working Set에서 벗어난 물리 페이지의 행선을 설명합니다.
Windows의 「메모리 사용량」은 무엇을 나타내는가 ── Working Set·Private Bytes·Commit·페이지 파일을 올바르게 읽는 법
작업 관리자의 메모리, Working Set, Private Bytes, Commit은 같은 값이 아닙니다. Windows의 가상 메모리와 물리 메모리의 관계, 페이지 파일의 역할, 메모리 부족이나 누수 조사에서 봐야 할 지표를 설명합니다.
Windows 메모리의 심층(제3회) ── 섹션 객체와 Copy-on-Write: DLL과 파일 매핑의 정체
섹션 객체, 이미지·데이터 매핑, 공유 캐시, Copy-on-Write를 이어서 DLL과 공유 메모리가 물리 페이지를 공유하는 구조를 설명합니다.
같은 1GB인데, 동영상 한 편보다 사진 폴더 복사가 느린 이유는?
Windows에서 용량이 같은데 복사 시간이 다른 이유를 그림으로 설명합니다. 파일 수, SSD와 NAS의 대기 시간, ZIP으로 묶는 효과, 생성·전송·압축 해제를 포함한 비교 절차, robocopy의 쓰임새를 정리합니다.
Windows의 「하드웨어 가속 GPU 일정 예약」이란? 켜면 빨라질까요?
Windows의 하드웨어 가속 GPU 일정 예약(HAGS)을 일반 사용자를 위해 그림으로 설명합니다. 무엇이 바뀌는지, 켜고 끄는 판단, 설정이 보이지 않는 이유, 프레임 생성과의 관계, 안전한 비교 절차를 정리합니다.
관련 토픽
이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.
Windows 기술 토픽
Windows 개발, 장애 조사, 기존 자산 활용에 관한 KomuraSoft LLC 기사를 모은 토픽 허브입니다.
이 주제와 연결되는 서비스
이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.
Windows 앱 개발
상주 처리, 장비 연동, 운영 로그, 유지 보수 가능한 구조가 필요한 Windows 데스크톱 애플리케이션을 지원합니다.
자주 묻는 질문
이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.
- VirtualAlloc으로 MEM_COMMIT한 시점에 RAM이 확보되나요?
- 일반적인 프라이빗 메모리에서는 Commit 시점에 시스템의 commit 여력이 소비되지만, 대응하는 물리 페이지는 처음 접근될 때까지 할당되지 않습니다. 쓰기로 처음 건드린 페이지는 demand-zero 폴트 처리 중에 물리 페이지를 얻습니다.
- 페이지 폴트는 이상이나 성능 문제를 뜻하나요?
- 아닙니다. demand-zero나 Standby에서의 복귀처럼 디스크 I/O가 없는 소프트 폴트는 정상 동작입니다. 성능을 판단할 때는 Page Faults/sec만이 아니라 Pages Input/sec, 스토리지 대기, Available MBytes도 함께 봅니다.
- TLB miss와 페이지 폴트는 같은가요?
- 다른 현상입니다. TLB에 변환 결과가 없어도 페이지 테이블의 PTE가 유효하면 CPU가 표를 따라가 변환을 다시 등록할 뿐입니다. PTE가 무효이거나 보호 위반이 있을 때 페이지 폴트 입구로 들어갑니다.
- VAD에 주소 범위가 있으면 접근 위반은 나지 않나요?
- 반드시 그렇지는 않습니다. VAD 유무에 더해, Reserve인지 Commit인지, 읽기·쓰기·실행 보호, 가드 페이지, PTE 상태 등을 메모리 관리자가 평가합니다. 해결할 수 없으면 0xC0000005 같은 예외가 됩니다.
- Page Faults/sec가 많으면 RAM이 부족한가요?
- 그것만으로는 판단할 수 없습니다. Page Faults/sec에는 소프트 폴트도 대량으로 포함됩니다. Memory\Pages Input/sec, Memory\Page Reads/sec, Available MBytes, 디스크 대기 시간을 같은 시간축에서 함께 봐야 합니다.