Windbg로 알아보는 Trans/Soft PTE와 2가지 Page Fault 유형
지난 글에 PTE의 유형 중 Hard, 즉 Hardware PTE에 대해 설명했는데요,
Windbg로 알아보는 PTE (_MMPTE)
; https://www.sysnet.pe.kr/2/0/13845
간단하게 다시 정리하면, Hard PTE는 운영체제가 그에 대한 페이징 테이블을 마련해 두면 자연스럽게 CPU에서 해당 PTE를 이용해 물리 메모리 접근하게 연동이 되는 유형입니다.
반면 이번에 다룰 Trans/Soft PTE는 그 과정에서 반드시 운영체제, 즉 소프트웨어의 도움을 받아야만 물리 주소로의 접근이 허용되는 PTE 유형입니다.
// Windows 10 x64 환경
5: kd> dt _MMPTE
nt!_MMPTE
+0x000 u : <anonymous-tag>
// What are anonymous structs, and more importantly, how do I tell windows.h to stop using them?
// https://devblogs.microsoft.com/oldnewthing/20170907-00/?p=96956
5: kd> dt _MMPTE u.
nt!_MMPTE
+0x000 u :
+0x000 Long : Uint8B
+0x000 VolatileLong : Uint8B
+0x000 Hard : _MMPTE_HARDWARE
+0x000 Proto : _MMPTE_PROTOTYPE
+0x000 Soft : _MMPTE_SOFTWARE
+0x000 TimeStamp : _MMPTE_TIMESTAMP
+0x000 Trans : _MMPTE_TRANSITION
+0x000 Subsect : _MMPTE_SUBSECTION
+0x000 List : _MMPTE_LIST
실제로 Trans, Soft PTE 상태가 어떤 때 생기는지 확인해 볼까요? ^^
테스트를 위해 우선 C#으로 다음의 예제를 만들고,
internal class Program
{
[DllImport("kernel32.dll")]
static extern bool SetProcessWorkingSetSize(IntPtr hProcess, nint dwMinimumWorkingSetSize, nint dwMaximumWorkingSetSize);
static unsafe void Main(string[] args)
{
int pid = Environment.ProcessId;
Console.WriteLine($"Process ID: {pid} (0x{pid:x})");
long* ptr = (long*)Marshal.AllocHGlobal(sizeof(long) * 1024 * 1024 * 6); // 8 * 6MB === 48MB 크기
for (int i = 0; i < 1024 * 1024 * 6; i++)
{
ptr[i] = i;
}
Console.WriteLine($"variable address in gc heap: 0x{(nint)ptr:x}");
while (true)
{
string? text = Console.ReadLine();
if (text == "q")
{
break;
}
if (text == "m")
{
SetProcessWorkingSetSize(System.Diagnostics.Process.GetCurrentProcess().Handle, -1, -1);
}
}
}
}
실행하면 이런 식의 출력이 나옵니다.
Process ID: 2100 (0x834)
variable address in native heap: 0x1ec8062f040
자, 그럼 저 가상 주소의 PTE를 확인해 보면,
// 프로세스 문맥으로 전환
0: kd> !process 0 0 mem_map.exe
PROCESS ffff86821dc9a0c0
SessionId: 2 Cid: 27e8 Peb: 5effe10000 ParentCid: 0854
DirBase: 4c253000 ObjectTable: ffff978afc4c8600 HandleCount: 175.
Image: mem_map.exe
0: kd> .process /i ffff86821dc9a0c0
You need to continue execution (press 'g' <enter>) for the context
to be switched. When the debugger breaks in again, you will be in
the new process context.
0: kd> g
Break instruction exception - code 80000003 (first chance)
nt!DbgBreakPointWithStatus:
fffff806`8381f130 cc int 3
// PTE 조회
7: kd> !pte 0x1ec8062f040
VA 000001ec8062f040
PXE at FFFFFA7D3E9F4018 PPE at FFFFFA7D3E803D90 PDE at FFFFFA7D007B2018 PTE at FFFFFA00F6403178
contains 0A000001DE873867 contains 0A00000050D13867 contains 0A00000075FE0867 contains 8100000075FE2847
pfn 1de873 ---DA--UWEV pfn 50d13 ---DA--UWEV pfn 75fe0 ---DA--UWEV pfn 75fe2 ---D---UW-V
// 마지막 페이징의 PFN 상태 확인
7: kd> !pfn 75fe2
PFN 00075FE2 at address FFFFC4000161FA60
flink 00000001 blink / share count 00000001 pteaddress FFFFFA00F6403178
reference count 0001 used entry count 0000 Cached color 0 Priority 5
restore pte 00000080 containing page 075FE0 Active M
Modified
일단 여기까지는 지난번의 Hard PTE를 확인하는 것과 동일합니다. 이 상태에서, SetProcessWorkingSetSize를 호출해 해당 프로세스가 유지하고 있는 working set 메모리를 최소화시킬 텐데요, 위의 예제에서는 'm' 키를 입력해 엔터를 누르면 됩니다. (혹은 "
working set trimmer"에 의해 제거된 경우.)
이후 다시 프로세스의 가상 주소에 대해 PTE를 확인해 보면,
// ...[생략: 문맥 전환]...
1: kd> !pte 0x1ec8062f040
VA 000001ec8062f040
PXE at FFFFFA7D3E9F4018 PPE at FFFFFA7D3E803D90 PDE at FFFFFA7D007B2018 PTE at FFFFFA00F6403178
contains 0A000001DE873867 contains 0A00000050D13867 contains 0A00000075FE0867 contains 0000000075FE2880
pfn 1de873 ---DA--UWEV pfn 50d13 ---DA--UWEV pfn 75fe0 ---DA--UWEV not valid
Transition: 75fe2
Protect: 4 - ReadWrite
보는 바와 같이 "Not valid" 상태가 되었고, PTE는 _MMPTE 구조체에 있던 Trans에 해당하는 Transition 상태로 변경됐습니다.
1: kd> dt _MMPTE FFFFFA00F6403178 u.Trans.
nt!_MMPTE
+0x000 u :
+0x000 Trans :
+0x000 Valid : 0y0
+0x000 Write : 0y0
+0x000 OnStandbyLookaside : 0y0
+0x000 IoTracker : 0y0
+0x000 SwizzleBit : 0y0
+0x000 Protection : 0y00100 (0x4)
+0x000 Prototype : 0y0
+0x000 Transition : 0y1
+0x000 PageFrameNumber : 0y0000000000000000000001110101111111100010 (0x75fe2)
+0x000 Unused : 0y000000000000 (0)
// Active 상태였을 때 Page File로 기록된 적이 없다면, Standby 상태로 변경
// 반면 기록된 적이 있다면, "Modified" 상태로 변경 (참고: https://codemachine.com/articles/prototype_ptes.html)
1: kd> !pfn 75fe2
PFN 00075FE2 at address FFFFC4000161FA60
flink 00075FE3 blink / share count 00075E40 pteaddress FFFFFA00F6403178
reference count 0000 used entry count 0000 Cached color 0 Priority 0
restore pte 4212E00002084 containing page 075FE0 Standby
그런데, 비록 PTE의 상태는 Transition이지만, Valid가 0이라는 것을 제외하고는 PTE가 가리키는 PFN도 여전히 유효하고, 게다가 그 PFN도 여전히 PteAddress를 Trans 상태로 바뀐 그 PTE를 가리키고 있습니다.
다시 말해, CPU는 이런 상태의 메모리를 접근하면 PTE의 Valid가 0이므로 무조건 Page Fault를 발생하게 됩니다. 이후 제어는 Fault 인터럽트 핸들러로 이동하고 운영체제는 PTE가 가리키는 PFN이 여전히 유효한 메모리를 보유하고 있으므로 Trans 상태의 PTE를 다시 Hard PTE 상태로 복원한 후 Page Fault가 발생한 그 코드로 다시 제어를 돌려줍니다.
지난 글에서 PFN 데이터베이스를 가리키는 연결 리스트가 있다고 설명했는데요,
[출처:
https://rayanfam.com/topics/inside-windows-page-frame-number-part1/]
Trans 상태가 된 PTE가 가리키는 PFN은 위의 연결 리스트에서 "Standby" 또는 "Modified"로 표시된 리스트로 이동하게 됩니다. 그러다, 만약 다시 응용 프로그램이 다시 그 메모리에 접근하게 되면 (위의 실습에서는) 아직 "Standby" 상태의 PFN이므로 금방 Active 상태로 바꿔 메모리 접근을 빠르게 허용해 줍니다.
그런데, 저 상태에서 다른 프로그램이 메모리를 소비하면 어떻게 될까요? 역시 간단하게, 약간의 메모리 부하를 주는 프로그램을 실행해 주면,
internal class Program
{
static void Main(string[] args)
{
// 현재 테스트 머신의 남은 물리 메모리 공간이 4GB 정도여서, 모두 점유할 수 있도록 4GB 할당
for (int i = 0; i < 1024 * 1024; i ++)
{
Marshal.AllocHGlobal(4096);
}
}
}
높은 확률로 저 "Standby" 상태의 PFN은 위의 프로그램이 요청한 메모리로 대체될 것입니다. 확인을 위해 다시 PTE 상태를 조회하면,
// ...[생략: 문맥 전환]...
2: kd> !pte 0x1ec8062f040
VA 000001ec8062f040
PXE at FFFFFA7D3E9F4018 PPE at FFFFFA7D3E803D90 PDE at FFFFFA7D007B2018 PTE at FFFFFA00F6403178
contains 0A000001DE873867 contains 0A00000050D13867 contains 0A00000075FE0867 contains 0004212E00002084
pfn 1de873 ---DA--UWEV pfn 50d13 ---DA--UWEV pfn 75fe0 ---DA--UWEV not valid
PageFile: 2
Offset: 4212e
Protect: 4 - ReadWrite
보는 바와 같이 이번에는 (Trans PTE가 아닌) 아예 페이징 파일로 내려간 위치의 Offset을 가리키는 PTE로 변경됐습니다. 물론, 기존에 저 PTE가 가리키던 PFN은 Standby 상태에서 Active로 바뀌긴 했지만,
2: kd> !pfn 75fe2
PFN 00075FE2 at address FFFFC4000161FA60
flink 00000001 blink / share count 00000001 pteaddress FFFFFA3FFCFEFC68
reference count 0001 used entry count 0000 Cached color 0 Priority 5
restore pte 00000080 containing page 0A8345 Active M
Modified
이번에는 다른 프로그램이 요청한 메모리로 대체되었기 때문에 가리키고 있는 Hard PTE의 주소가 달라졌습니다. 저렇게 page-out된 상태를 나타내는 PTE가 바로 "Soft PTE"에 해당합니다.
2: kd> dt _MMPTE FFFFFA00F6403178 u.Soft.
nt!_MMPTE
+0x000 u :
+0x000 Soft :
+0x000 Valid : 0y0
+0x000 PageFileReserved : 0y0
+0x000 PageFileAllocated : 0y1
+0x000 ColdPage : 0y0
+0x000 SwizzleBit : 0y0
+0x000 Protection : 0y00100 (0x4)
+0x000 Prototype : 0y0
+0x000 Transition : 0y0
+0x000 PageFileLow : 0y0010
+0x000 UsedPageTableEntries : 0y0000000000 (0)
+0x000 ShadowStack : 0y0
+0x000 OnStandbyLookaside : 0y0
+0x000 Unused : 0y0000
+0x000 PageFileHigh : 0y00000000000001000010000100101110 (0x4212e)
이것 역시 Valid 상태가 0이므로 CPU는 이런 PTE를 접근하면 Page Fault를 발생시키게 되는데, 마찬가지로 운영체제는 이에 대한 인터럽트 핸들러에서 PTE의 유형이 Soft인 것을 확인하고 페이지 파일로 내려간 메모리를 다시 물리 메모리로 복원한 후 제어를 프로그램으로 돌려줍니다.
이러한 Trans PTE, Soft PTE는 동일하게 Page Fault를 발생시키지만 그 유형이 Major/Minor로 나뉩니다.
Windows Page Faults
; https://medium.com/@idobhh/windows-page-faults-d45797f9404
위의 글에서 Page Fault에 대해 "Major/Hard Page Fault"와 "Minor/Soft Page Fault"로 구분하고 있는데요, 그 기준은 I/O 발생 유무라고 합니다. 즉, Trans PTE의 경우 I/O가 발생하지 않으므로 그런 경우를 Soft Page Fault라고 하고, Soft PTE의 경우 I/O가 발생하므로 Hard Page Fault라고 합니다.
한 가지 혼동하지 말아야 할 것은 그것이 1:1 관계는 아닙니다. 즉, Soft Page Fault가 발생했다고 해서 그것이 반드시 Trans PTE와 연관돼 있는 경우는 아닙니다.
(위의 문서에서 이미 설명하듯이) 메모리를 할당하는 과정에서도 "Minor Page Fault"가 발생할 수 있다고 합니다. 즉, 메모리를 프로그램이 요청한다고 해서 곧바로 물리 메모리까지 할당하기보다는 주소 공간만 예약해 두는 정도로 처리했다가 이후 실제로 메모리를 요구했을 때 Page Fault 동작을 이용해 그제야 물리 메모리를 확보하는 식으로도 동작하는 것입니다. (이와 유사한 사례로
stack의 guard page를 들 수 있습니다.)
또한, 프로세스 간 공유 메모리를 사용할 때도 Soft Page Fault가 발생할 수 있는데요, 2개의 프로세스(A, B)가 있다고 가정할 때 A 프로세스의 PTE는 Invalid 상태로 바뀌고 B 프로세스는 여전히 Hard PTE로 유지되는 상황에서 A 프로세스가 실제 메모리를 요구하면 Page Fault가 발생하게 되고 이미 B 프로세스가 사용하고 있는 그 PFN으로 연결만 시키면 되므로 I/O 발생 없이 끝납니다.
위의 2가지 경우 모두 Soft Page Fault가 발생하지만, Trans PTE와는 별 상관이 없는 경우입니다.
관련해서 좀 더 찾아보니까,
The Basics of Page Faults
; https://techcommunity.microsoft.com/blog/askperf/the-basics-of-page-faults/373120
PTE의 transitional 상태가 운영체제의 prefetch 캐시로도 발생한다고 합니다.
Soft page faults may also occur when the page is in a transitional state because it has been removed from the working sets of the processes that were using it, or it is resident as the result of a prefetch operation.
위의 문서에 재미있는 내용이 또 있는데요,
The page fault counters in Performance Monitor do not distinguish between hard and soft faults, so you have to do a little bit of work to determine the number of hard faults.
To track paging, you should use the following counters: Memory\ Page Faults /sec, Memory\ Cache Faults /sec and Memory\ Page Reads /sec. The first two counters track the working sets and the file system cache. The Page Reads counter allows you to track hard page faults. If you have a high rate of page faults combined with a high rate of page reads (which also show up in the Disk counters) then you may have an issue where you have insufficient RAM given the high rate of hard faults.
윈도우의 성능 모니터 도구에서 보여주는 Page Fault 카운터는 Hard/Soft를 구분하지 않는다고 합니다. 사실 Soft Page Fault는 많이 발생한다고 해도 처리 자체가 고속으로 이뤄지므로 성능에 큰 영향을 미치지 않아 관심 대상이 아닙니다. 즉, 대개의 경우 사용자는 Hard Page Fault 발생 유무를 더 보고 싶을 텐데요, 아쉽게도 Page Fault 성능 카운터는 어떤 유형인지까지는 나누고 있지 않습니다.
대신 Hard Page Fault를 유추할 수 있도록 "Memory\ Page Reads /sec" 성능 카운터를 함께 보라고 합니다. 즉, Page Fault가 많이 발생하는 상황에서 Page Read 카운터까지 높다면 물리 메모리 자원인 RAM 부족으로 인해 발생하는 hard fault로 인한 성능 이슈가 발생했을 가능성이 높은 것입니다.
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]