Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (seongtaejeong at gmail.com)
홈페이지
첨부 파일

(시리즈 글이 8개 있습니다.)
디버깅 기술: 194. Windbg - x64 가상 주소를 물리 주소로 변환
; https://www.sysnet.pe.kr/2/0/13500

디버깅 기술: 203. Windbg - x64 가상 주소를 물리 주소로 변환 (페이지 크기가 2MB인 경우)
; https://www.sysnet.pe.kr/2/0/13836

디버깅 기술: 206. Windbg로 알아보는 PFN (_MMPFN)
; https://www.sysnet.pe.kr/2/0/13844

디버깅 기술: 207. Windbg로 알아보는 PTE (_MMPTE)
; https://www.sysnet.pe.kr/2/0/13845

디버깅 기술: 208. Windbg로 알아보는 Trans/Soft PTE와 2가지 Page Fault 유형
; https://www.sysnet.pe.kr/2/0/13846

디버깅 기술: 209. Windbg로 알아보는 Prototype PTE
; https://www.sysnet.pe.kr/2/0/13848

디버깅 기술: 210. Windbg - 논리(가상) 주소를 Segmentation을 거쳐 선형 주소로 변경
; https://www.sysnet.pe.kr/2/0/13849

디버깅 기술: 212. Windbg - (Ring 3 사용자 모드의) FS, GS Segment 레지스터
; https://www.sysnet.pe.kr/2/0/13852




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/]
pfn_table_1.gif

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로 인한 성능 이슈가 발생했을 가능성이 높은 것입니다.




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]







[최초 등록일: ]
[최종 수정일: 12/17/2024]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13881정성태2/7/2025393닷넷: 2316. C# - Port I/O를 이용한 PCI Configuration Space 정보 열람파일 다운로드1
13880정성태2/5/2025439오류 유형: 947. sshd - Failed to start OpenSSH server daemon.
13879정성태2/5/2025501오류 유형: 946. Ubuntu - N: Updating from such a repository can't be done securely, and is therefore disabled by default.
13878정성태2/3/2025695오류 유형: 945. Windows - 최대 절전 모드 시 DRIVER_POWER_STATE_FAILURE 발생 (pacer.sys)
13877정성태1/25/20251023닷넷: 2315. C# - PCI 장치 열거 (레지스트리, SetupAPI)파일 다운로드1
13876정성태1/25/20251168닷넷: 2314. C# - ProcessStartInfo 타입의 Arguments와 ArgumentList파일 다운로드1
13875정성태1/24/20251160스크립트: 69. 파이썬 - multiprocessing 패키지의 spawn 모드로 동작하는 uvicorn의 workers
13874정성태1/24/20251132스크립트: 68. 파이썬 - multiprocessing Pool의 기본 프로세스 시작 모드(spawn, fork)
13873정성태1/23/20251058디버깅 기술: 217. WinDbg - PCI 장치 열거
13872정성태1/23/20251045오류 유형: 944. WinDbg - 원격 커널 디버깅이 연결은 되지만 Break (Ctrl + Break) 키를 눌러도 멈추지 않는 현상
13871정성태1/22/20251159Windows: 278. Windows - 윈도우를 다른 모니터 화면으로 이동시키는 단축키 (Window + Shift + 화살표)
13870정성태1/18/20251235개발 환경 구성: 741. WinDbg - 네트워크 커널 디버깅이 가능한 NIC 카드 지원 확대
13869정성태1/18/20251256개발 환경 구성: 740. WinDbg - _NT_SYMBOL_PATH 환경 변수에 설정한 경로로 심벌 파일을 다운로드하지 않는 경우
13868정성태1/17/20251195Windows: 277. Hyper-V - Windows 11 VM의 Enhanced Session 모드로 로그인을 할 수 없는 문제
13867정성태1/17/20251302오류 유형: 943. Hyper-V에 Windows 11 설치 시 "This PC doesn't currently meet Windows 11 system requirements" 오류
13866정성태1/16/20251309개발 환경 구성: 739. Windows 10부터 바뀐 device driver 서명 방법
13865정성태1/15/20251394오류 유형: 942. C# - .NET Framework 4.5.2 이하의 버전에서 HttpWebRequest로 https 호출 시 "System.Net.WebException" 예외 발생
13864정성태1/15/20251345Linux: 114. eBPF를 위해 필요한 SELinux 보안 정책
13863정성태1/14/20251285Linux: 113. Linux - 프로세스를 위한 전용 SELinux 보안 문맥 지정
13862정성태1/13/20251228Linux: 112. Linux - 데몬을 위한 SELinux 보안 정책 설정
13861정성태1/11/20251346Windows: 276. 명령행에서 원격 서비스를 동기/비동기로 시작/중지
13860정성태1/10/20251269디버깅 기술: 216. WinDbg - 2가지 유형의 식 평가 방법(MASM, C++)
13859정성태1/9/20251336디버깅 기술: 215. Windbg - syscall 이후 실행되는 KiSystemCall64 함수 및 SSDT 디버깅
13858정성태1/8/20251410개발 환경 구성: 738. PowerShell - 원격 호출 시 "powershell.exe"가 아닌 "pwsh.exe" 환경으로 명령어를 실행하는 방법
13857정성태1/7/20251820C/C++: 187. Golang - 콘솔 응용 프로그램을 Linux 데몬 서비스를 지원하도록 변경파일 다운로드1
[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...