VirtualAlloc에 MEM_COMMIT을 넘기면 그 순간에 Commit은 늘어납니다. 그러나 Working Set이 같은 만큼 늘어난다는 보장은 없습니다. 그렇다면 할당했다고 생각한 메모리는 어디에 있을까요?
답은 대부분의 페이지에는 아직 대응하는 물리 RAM이 없다는 것입니다. Windows는 애플리케이션이 실제로 페이지에 닿을 때까지 물리 페이지 할당을 미룹니다. 첫 접근으로 CPU가 페이지 폴트를 일으키면, 메모리 관리자가 VAD, PTE, 보호 속성, 백킹 스토어를 살펴보고 필요하면 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회: 섹션 객체와 카피 온 라이트
DLL, 파일 매핑, 공유 메모리가 물리 페이지를 공유할 수 있는 이유를 따라갑니다.
제1회가 답하는 질문은 단 하나입니다.
커밋된 가상 주소는 어느 순간에 물리 RAM이 되는가?
대상 독자는 Windows 앱의 메모리 사용량, 시작 직후의 페이지 폴트, 0xC0000005, VMMap과 PerfMon의 숫자를 메커니즘부터 이해하고 싶은 개발자와 운영 담당자입니다. 전제 환경은 Windows 10/11 또는 현행 Windows Server이고, 필요 배경은 포인터와 VirtualAlloc의 기초이며, 페이지 테이블 비트 배치나 커널 디버거 경험은 필요 없습니다. 난이도는 중급입니다. 내부 구조의 이름은 다루지만, 특정 Windows 빌드에 의존하는 비공개 레이아웃은 전제로 하지 않습니다.
1. 먼저 결론
일반적인 프라이빗 메모리의 흐름은 한 줄로 이렇습니다.
Reserve는 가상 주소 범위를 잡아 두고, Commit은 앞으로 내용을 둘 자리를 보장하려고 커밋 한도에 커밋 차지(commit charge)를 반영하며, 첫 접근의 페이지 폴트가 물리 페이지를 할당합니다.
즉 MEM_COMMIT은 「지금 당장 RAM을 할당하라」는 명령이 아닙니다. Microsoft의 VirtualAlloc 문서도 커밋된 페이지의 초기 내용이 0임을 보장하면서, 실제 물리 페이지는 가상 주소에 접근할 때까지 할당되지 않는다고 설명합니다.1
그렇다고 「Reserve/Commit은 VAD에만 쓴다」고 단정하는 것도 정확하지 않습니다. 실제로는 Reserve가 주로 가상 주소 범위와 속성을 나타내는 VAD를 만들고, Commit이 시스템의 Commit Total을 늘리며 범위의 커밋 상태를 기록합니다. 중간 페이지 테이블 계층과 개별 PTE는 필요할 때 지연 구축되고, 물리 RAM과의 최종 결합은 보통 첫 접근에서 일어납니다.
Commit은 빈 약속이 아닙니다. 내용을 앞으로 RAM이나 적절한 백킹 스토어에 둘 수 있다는 시스템 전체의 약속입니다. 그 약속을 한 페이지씩 현실로 만드는 입구가 페이지 폴트입니다.
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에는 아직 물리 페이지로의 유효한 변환이 없습니다. 여기서 페이지 폴트가 납니다.
제어를 받은 메모리 관리자는 이를 「커밋되고 쓰기 가능한 프라이빗 페이지에 대한 첫 접근」으로 판단하고, 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 상태
- 읽기, 쓰기, 실행, 카피 온 라이트 같은 보호
- 파일이나 섹션과의 대응
- 가드 페이지 같은 특수 속성
범위 단위로 관리하는 이유는 효율입니다. 256MiB는 4KiB 기준으로 65,536페이지입니다. 모든 페이지에 완전한 관리 구조를 미리 만들기보다, 「이 연속 범위는 하나의 예약」을 VAD에 두고 필요할 때 페이지를 실현하는 편이 낭비가 적습니다.
4.1. VAD를 찾았다고 복구가 보장되지는 않는다
「VAD에 있으면 폴트가 해결되고, 없으면 접근 위반」은 입문용 설명으로는 편하지만 단순화입니다. VAD를 찾았어도 다음처럼 일반 접근을 이어갈 수 없는 경우가 있습니다.
- Reserve만 되어 있고 대상 페이지가 커밋되지 않음
PAGE_NOACCESS- 읽기 전용 페이지에 쓰기
- 실행 불가 페이지에서 명령 실행
- 가드 페이지의 첫 터치
- 섹션의 유효 범위 밖을 터치
반대로 PTE가 무효여도 VAD와 PTE의 소프트웨어 상태가 정당한 접근을 보이면, 디맨드 제로, 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 미스와 페이지 폴트는 다른 것입니다. 문제는 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 미스는 페이지 테이블 워크로 해결될 수 있습니다. 변환이 무효이거나 보호 위반이 있을 때 페이지 폴트로 진행하며, 보호 위반은 TLB 히트에서도 납니다.
5.1. 무효 PTE는 빈칸이 아니다
무효 PTE라도 비어 있지는 않습니다. Windows는 무효 PTE의 소프트웨어 상태에서 다음 같은 경우를 가립니다.
- 한 번도 실현되지 않은 디맨드 제로 페이지
- RAM에 남아 있는 Transition 페이지
- Prototype PTE를 가리키는 공유 페이지
- 페이지 파일에 저장된 프라이빗 페이지
- 보호 위반 또는 무효 영역
CPU의 일은 「이것은 보통의 유효 변환이 아니다」라고 판단해 커널에 넘기는 것뿐이고, 그다음 의미는 메모리 관리자가 붙입니다.
6. 페이지 폴트의 처음부터 끝까지
커밋된 프라이빗 페이지에 대한 첫 쓰기를 여섯 단계로 따라가 봅시다.
- CPU가 쓰기를 시도한다.
TLB와 페이지 테이블을 확인하지만, 대상 PTE에는 유효한 PFN이 없습니다. - CPU가 페이지 폴트를 일으킨다.
폴트가 난 가상 주소, 읽기/쓰기/실행 유형, 사용자/커널, 빠진 변환인지 보호 위반인지를 커널에 넘깁니다. - 메모리 관리자가 VAD와 PTE를 살핀다.
페이지가 커밋되었는지, 보호가 맞는지, 디맨드 제로·Transition·공유·페이지 인·CoW·예외 중 어느 것인지 결정합니다. - 디맨드 제로이면 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, 보호 속성, 접근 유형을 판단해 디맨드 제로, RAM에 남은 페이지 재연결, 백킹 스토어에서의 하드 폴트, 카피 온 라이트, 가드 페이지 알림, 예외로 나눈다
faultIn["페이지 폴트 발생"] --> judge["VAD, PTE, 보호, 유형 판단"]
judge -->|첫 접근| dz["디맨드 제로(소프트)"]
judge -->|아직 RAM에 있음| soft["Standby에서 재연결(소프트)"]
judge -->|디스크 읽기 필요| hard["하드 폴트(디스크 I/O)"]
judge -->|CoW 쓰기| cow["복사하고 PTE를 교체"]
judge -->|가드 페이지| guard["가드를 지우고 알림"]
judge -->|해결 불가| av["예외(0xC0000005 등)"]
그림 4: 같은 입구로 들어온 폴트가 판단에 따라 여섯 가지 결과로 갈립니다. 가드 페이지의 자세한 내용은 9절에서 다룹니다.
7. 디맨드 제로 ── 디스크를 읽지 않는 소프트 폴트
디맨드 제로는 커밋된 프라이빗 페이지에 처음 닿을 때 나는 대표적인 소프트 폴트입니다. Microsoft의 Working Set 문서도 「프로세스가 할당된 가상 페이지를 처음 참조한다」를 소프트 폴트의 예로 듭니다.5
디맨드 제로의 특징은 다음과 같습니다.
- 원본 데이터를 디스크에서 읽을 필요가 없음
- 초기 내용은 0
- 사용 가능한 물리 페이지를 묶음
- Working Set과 누적 Page Fault Count가 늘어남
- 이 처리만으로는
Memory\\Pages Input/sec가 늘지 않음
그래서 시작 직후 Page Faults/sec가 튀어도 그것만으로 스토리지가 병목이라고 말할 수 없습니다.
지연 할당의 트레이드오프도 정리해 둘 만합니다. 256MiB를 Commit하고 실제로 8MiB만 쓰면, 남은 248MiB를 RAM 밖에 두는 것이 합리적입니다. 대신 첫 접근에는 폴트 처리 비용이 따릅니다. 지연에 민감한 작업에는 시작 전에 각 페이지를 미리 만져 프리폴트하는 설계가 있지만, 그것은 RAM 상주를 앞에서 늘리는 트레이드오프입니다.
8. 소프트 폴트와 하드 폴트
8.1. 소프트 폴트
소프트 폴트는 백킹 스토어에 대한 읽기 I/O 없이 해결할 수 있는 폴트입니다. 대표적인 예는 다음과 같습니다.
- 디맨드 제로
- Standby/Transition에 남은 페이지를 다시 연결
- 다른 프로세스의 Working Set에 있는 공유 페이지를 연결
- 프리페치된 페이지를 연결
- 원본 페이지가 상주하는 카피 온 라이트
커널 전환, 잠금, PTE/PFN 갱신, TLB 일관성 등 CPU 비용은 있지만, 스토리지 대기는 없습니다.5
8.2. 하드 폴트
반대로 필요한 페이지가 RAM 어디에도 없어 백킹 스토어에서 읽어야 하면 하드 폴트입니다. 읽기 원본은 페이지 파일만이 아닙니다.
- 페이지 파일에 기록된 프라이빗 페이지
- 메모리 매핑 파일
- EXE나 DLL 이미지
- 파일 캐시가 참조하는 데이터 파일
ETW HardFault 이벤트에는 FileObject, ReadOffset, ByteCount가 있어 실제 읽기 원본을 추적할 수 있습니다.6
따라서 Hard Fault = pagefile.sys 읽기는 사실이 아닙니다.
백킹 스토어 읽기가 필요하면 요청은 Windows I/O 스택으로 들어갑니다. IRP의 발행·완료 흐름은 「Windows I/O의 심층(제1회)」에서, 파일 캐시와의 접점은 「Windows I/O의 심층(제4회)」에서 다룹니다. 페이지가 RAM에 있으면 메모리 관리자가 스스로 돌아올 수 있고, 없으면 I/O를 내고 완료까지 폴트가 난 스레드를 기다립니다.
9. 해결할 수 없는 폴트는 예외가 된다
VAD와 PTE를 살펴본 뒤 정당한 할당, 페이지 인, CoW로 해결할 수 없는 폴트는 사용자 모드에 예외로 전달됩니다.
대표적인 경우는 STATUS_ACCESS_VIOLATION, 예외 코드 0xC0000005입니다. 무효 주소에 대한 읽기, 쓰기, 실행에서 나며, 첫 예외 매개변수는 접근 유형을, 두 번째는 위반 주소를 가리킵니다.7
전형적인 패턴은 다음과 같습니다.
- NULL, 해제한 주소, 배열 밖 주소를 읽기
- 읽기 전용 페이지에 쓰기
- DEP/NX가 실행 불가로 만든 페이지에서 명령을 실행
- 커밋되지 않은 예약 범위를 터치
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(<target>)\\Page Faults/secMemory\\Pages Input/secMemory\\Page Reads/secMemory\\Available MBytesProcess(<target>)\\Working Set - PrivateProcess(<target>)\\Private Bytes
Process\\Page Faults/sec에는 소프트 폴트와 하드 폴트가 모두 들어갑니다. 반면 Memory\\Pages Input/sec는 하드 폴트를 해결하려고 디스크에서 읽은 페이지 수입니다.10
이 프로그램의 Touch 단계에서는 Page Faults/sec가 뛰고 Pages Input/sec는 거의 오르지 않아야 합니다. 새로 커밋한 페이지는 디맨드 제로로 실현되므로 원본 데이터를 디스크에서 읽을 필요가 없습니다.
같은 이름을 여러 프로세스가 공유하면 PerfMon의 process#1 같은 번호가 재시작마다 바뀔 수 있습니다. PID를 보여 주는 카운터와 교차 확인하거나, Process V2 또는 ETW/WPA로 PID로 식별하세요.
11. 실무에서 피해야 할 세 가지 오독
11.1. 「Commit이 올랐으니 RAM 누수다」
Commit은 내용을 두겠다는 약속량이고, 아직 닿지 않은 페이지는 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, 힙 free, 객체 소멸 등을 해야 합니다.
빠진 물리 페이지가 어디로 가는지는 제2회에서 따라갑니다.
12. 정리
MEM_RESERVE는 가상 주소 범위를 잡아 두지만 RAM이나 페이지 파일에 물리 영역을 할당하지 않습니다.1MEM_COMMIT은 Commit을 소비해 앞으로 내용을 둘 수 있음을 보장하지만, 일반 물리 페이지는 첫 접근까지 할당되지 않습니다.12- VAD는 범위 장부, PTE는 가상 페이지 장부, PFN 데이터베이스는 물리 페이지 장부입니다.
- TLB 미스는 페이지 폴트가 아닙니다. PTE가 유효하면 페이지 테이블 워크만으로 해결됩니다.
- 디맨드 제로, 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회) ── 모든 읽기·쓰기는 IRP가 된다: I/O 시스템의 전체상
- Windows I/O의 심층(제4회) ── 캐시 관리자: 당신의 WriteFile은 언제 디스크에 도달하는가
- WinDbg + SOS로 크래시 덤프 읽기 ── 수집 이후 실무 분석 입문
- Windows 앱의 크래시 덤프 수집 입문 - 우선 WER / ProcDump / WinDbg를 어떻게 구분해서 쓸까
관련 상담 영역
합동회사 코무라소프트는 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이 페이지 파일을 확장하지 않고 커밋할 수 있는 상한이라는 점에 대해. ↩ ↩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. 소프트 폴트가 백킹 스토어에 접근하지 않고 해결될 수 있으며, 다른 프로세스의 Working Set, Transition, 첫 참조 디맨드 제로 등에서 난다는 점에 대해. ↩ ↩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회) ── 섹션 객체와 복사 시 쓰기: DLL과 파일 매핑의 정체
섹션 객체, 이미지·데이터 매핑, 공유 캐시, 복사 시 쓰기를 연결해 DLL과 공유 메모리가 물리 페이지를 어떻게 공유하는지 설명합니다.
Windows 가상화의 심층(제3회) ── 수초 만에 기동하는 가상 머신: WSL2, Windows Sandbox, 컨테이너가 가벼운 이유
WSL2와 Windows Sandbox가 수초 만에 기동하고 가벼운 이유는 무엇일까요. 동적 베이스 이미지와 다이렉트 맵, 동적 메모리 할당부터 Hyper-V 격리 컨테이너까지 구조를 해설합니다.
Windows 가상화의 심층(제2회) ── 커널에서도 보이지 않는 메모리: VBS, HVCI, Credential Guard의 구조
호환 하드웨어에 대한 클린 설치에서 기본으로 켜지는 VBS는, 하이퍼바이저와 SLAT로 커널보다 강한 격리를 만듭니다. VTL, Secure Kernel, HVCI, Credential Guard의 구조를 해설합니다.
관련 토픽
이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.
Windows 기술 토픽
Windows 개발, 장애 조사, 기존 자산 활용에 관한 KomuraSoft LLC 기사를 모은 토픽 허브입니다.
이 주제와 연결되는 서비스
이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.
Windows 앱 개발
상주 처리, 장비 연동, 운영 로그, 유지 보수 가능한 구조가 필요한 Windows 데스크톱 애플리케이션을 지원합니다.
자주 묻는 질문
이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.
- VirtualAlloc에 MEM_COMMIT을 넘기면 그 순간에 RAM이 할당되나요?
- 일반적인 프라이빗 메모리에서는 Commit이 시스템의 커밋 여력을 소비하지만, 대응하는 물리 페이지는 첫 접근까지 할당되지 않습니다. 쓰기로 처음 닿은 페이지는 디맨드 제로 폴트 처리 중에 물리 페이지를 얻습니다.
- 페이지 폴트는 이상이나 성능 문제를 뜻하나요?
- 아닙니다. 디맨드 제로나 Standby에서 되돌리기처럼 디스크 I/O가 없는 소프트 폴트는 정상 동작입니다. 성능 판단에서는 Page Faults/sec뿐 아니라 Pages Input/sec, 스토리지 대기, Available MBytes도 함께 봅니다.
- TLB 미스와 페이지 폴트는 같은 것인가요?
- 다릅니다. 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, 디스크 대기 시간과 같은 시간축에서 상관을 봐야 합니다.