Process Explorer / Handle / VMMap 실전 ── 행(hang)・누수・「파일 사용 중」을 지금 이 순간의 상태에서 추적한다

· · Sysinternals, Process Explorer, VMMap, 핸들 누수, 메모리 누수, 불량 조사, Windows, 장기 가동

“월요일 아침에 재부팅하면 고쳐집니다” ── 장비 연동 앱의 유지보수 상담에서 몇 번이나 들은 말입니다. 가동 직후에는 쾌적하다가도 목요일쯤부터 화면 전환이 버벅거리기 시작하고, 금요일 저녁에는 버튼을 눌러도 몇 초간 반응이 없습니다. 가끔 “핸들이 잘못되었습니다”라는 본 적도 없는 오류가 뜨기도 합니다. 그리고 재부팅하면 아무 일도 없었다는 듯이 고쳐집니다. 이렇게 되면 운영은 결국 “매주 재부팅”으로 고정되고, 근본 원인은 아무도 모른 채 몇 년이 지나갑니다.

이런 유형의 증상은 로그를 아무리 들여다봐도 원인에 다다르지 못합니다. 필요한 것은 “지금 이 순간, 그 프로세스가 무엇을 얼마나 붙들고 있는가“를 직접 보는 일입니다. 지난번 “Process Monitor(ProcMon) 실전 가이드“에서는 조작의 시계열을 기록하는 ProcMon을 다뤘습니다. 이번에는 그 속편이자 Sysinternals 실전 제2탄으로서, 상태를 보는 쪽의 도구 ── Process ExplorerHandleVMMap ── 를, 장기 가동 앱의 3대 증상인 “점점 느려진다” “파일을 지울 수 없다” “행(hang)이 걸렸다”에 따라 정리합니다.

이 글의 전제 환경은 다음과 같습니다.

  • OS ── Process Explorer의 공식 동작 요건은 클라이언트가 Windows 11 이상, 서버가 Windows Server 2016 이상입니다.1
  • 비트 수 ── 64bit 환경에서 procexp.exe를 실행하면 64bit 버전이 자동으로 전개(추출)되어 실행됩니다. 64bit 프로세스의 스택이나 핸들을 올바르게 보려면 이 64bit 버전에서 동작하고 있어야 합니다.
  • 권한 ── 조사 목적이라면 “관리자 권한으로 실행”이 사실상 필수입니다. 일반 권한 그대로 두면 다른 사용자나 서비스의 프로세스에 한해 스레드 스택도 핸들 목록도 “액세스가 거부되었습니다”가 됩니다. CLI 버전인 handle.exe는 공식적으로 관리자 권한이 필요하다고 명시되어 있습니다.2
  • 최초 실행 ── EULA(최종 사용자 사용권 계약)에 대한 동의 대화상자가 나타납니다. 스크립트나 무인 실행에서는 /accepteula 스위치로 동의를 명시합니다(8장).

참고로 이 글은 GUI 도구의 조작을 다루지만, 화면 캡처는 싣지 않았습니다. 대신 메뉴 이름과 단축키를 실제 표기 그대로 적었습니다. 직접 도구를 열어 두고 따라가며 읽을 수 있는 형태로 구성했습니다.

1. 먼저 결론

  • ProcMon이 “이력”이라면 Process Explorer는 “현재”입니다. Process Explorer는 각 프로세스가 열고 있는 핸들과 로드한 DLL을 나열・검색할 수 있는 도구로, DLL 버전 문제나 핸들 누수 조사에 적합하다고 공식 문서에서도 명시하고 있습니다.1
  • “파일을 지울 수 없다・교체할 수 없다”는 Find Handle or DLL(Ctrl+F)이 가장 빠른 길입니다. 경로의 일부로 검색하면 그 파일을 잡고 있는 프로세스를 몇 초 만에 특정할 수 있습니다(3장).
  • “행(hang)이 걸렸다”는 프로세스 속성 → Threads 탭에서 스레드의 스택을 확인합니다. 다만 심볼 설정(dbghelp.dll과 심볼 경로)을 하지 않으면 주소가 나열되기만 할 뿐입니다(4장).13
  • “점점 느려진다”는 컬럼을 추가해 기울기를 봅니다. Handle Count・USER Objects・GDI Objects・Private Bytes 네 가지를 추가하고, 시간을 두고 계속 증가하는 항목을 찾습니다. GDI/USER 객체에는 프로세스별 상한이 있어, 도달하면 그리기나 창 생성이 망가지기 시작합니다(5장).4
  • CLI 버전인 handle.exe는 스크립트를 이용한 정점 관측에 적합합니다. handle -s -p <프로세스>로 종류별 핸들 수를 정기적으로 기록할 수 있습니다. -c에 의한 강제 핸들 종료는 공식 문서가 불안정화를 경고하고 있어 기본적으로 사용하지 않습니다.2
  • “Private Bytes가 계속 증가한다”의 내역은 VMMap으로 나눕니다. Heap이면 네이티브, Managed Heap이면 .NET이라는 식으로 범인의 무대를 먼저 정한 뒤 전용 도구로 넘어갑니다(6장).5
  • 프로세스가 아니라 OS 전체의 메모리가 의심될 때는 RAMMap입니다.6 크래시라면 ProcDump로 덤프를 채취하는(「Windows 크래시 덤프 수집 입문」) 방식으로 전환합니다.
  • 어느 도구든 설치가 필요 없고, Sysinternals Live에서 바로 실행할 수도 있습니다. 오프라인 장비 PC에도 들고 들어가기 쉬운 반면, 심볼만큼은 사전 준비가 필요합니다(8장).7

2. Process Explorer 기본 ── 관리자 권한 실행・작업 관리자 대체・화면 읽는 법

Process Explorer는 설치가 필요 없으며, ZIP 압축을 풀어 procexp.exe(64bit라면 자동으로 procexp64가 동작합니다)를 실행하기만 하면 됩니다.1 다른 사용자의 프로세스나 서비스의 내부까지 들여다보는 도구이므로, 조사할 때는 반드시 “관리자 권한으로 실행”합니다. 일반 권한으로 실행하면 정작 봐야 할 프로세스에 한해 “액세스가 거부되었습니다”가 되어 스택도 핸들도 보이지 않습니다.

조사 담당자의 PC에서는 Options → Replace Task Manager를 켜 두면 Ctrl+Shift+Esc나 작업 표시줄에서 작업 관리자를 호출할 때 그대로 Process Explorer로 대체됩니다. “급할 때 익숙한 도구가 열린다”는 효과는 은근히 크며, 저희 회사도 개발용 PC는 전부 이렇게 바꿔 두었습니다(원래대로 되돌리는 것도 같은 메뉴에서 합니다).

화면은 위쪽에 프로세스 트리, 아래쪽에 선택한 프로세스의 상세 정보가 나오는 2단 구성입니다. 아래쪽 창은 View → Lower Pane View에서 Handles 뷰(열려 있는 핸들 목록)와 DLLs 뷰(로드한 DLL과 메모리 매핑 파일 목록)를 전환합니다.1 먼저 익혀야 할 것은 행 색상의 의미입니다(Options → Configure Colors에서 확인・변경할 수 있습니다).

색상(기본값) 의미
초록 새로 시작된 프로세스(기본값은 약 1초간)
빨강 종료 중인 프로세스
연한 파랑 자신과 같은 사용자 계정의 프로세스
분홍 서비스를 호스팅하는 프로세스
보라 팩(압축・난독화)된 실행 파일로 의심되는 것
짙은 회색 일시 중단(suspend)된 프로세스

“잠깐 초록색 프로세스가 생겼다가 곧바로 빨갛게 변해 사라진다”가 끝없이 반복되는 것 같은 이상 현상은 트리 표시와 색상 구분만으로도 발견할 수 있습니다. 프로세스의 부모-자식 관계가 트리로 보이기 때문에 “이 conhost.exe는 누구의 자식인가”, “앱을 실행하는 것은 서비스인가 작업 스케줄러인가”도 한눈에 알 수 있습니다.

3. “파일이 사용 중이라 지울 수 없다” ── Find Handle or DLL로 범인을 특정한다

“로그 파일을 삭제할 수 없다” “업데이트하려 했더니 exe가 사용 중이다” “USB 메모리를 안전하게 제거할 수 없다” ── 이런 증상의 조사는 Find → Find Handle or DLL(Ctrl+F)로 끝납니다.1 조작은 다음 5단계입니다.

  1. Process Explorer를 관리자 권한으로 실행한다(이것을 잊으면 잡고 있는 것이 서비스나 다른 사용자의 프로세스일 경우 보이지 않습니다)
  2. 메뉴의 Find > Find Handle or DLL…(단축키 Ctrl+F)를 연다
  3. 검색 문자열 입력란에 파일 이름이나 폴더 이름의 일부를 입력한다(예: report.csv, D:\Data). 완전한 경로일 필요는 없으며, 부분 일치로 검색됩니다
  4. Search를 누른다. 그 이름을 포함하는 핸들을 열고 있는 프로세스, 또는 해당 DLL을 로드한 프로세스가 목록에 나타납니다
  5. 결과 행을 클릭하면 위쪽 창에서 해당 프로세스가 선택되고, 아래쪽 창에서 해당 핸들이 강조 표시됩니다

찾아낸 뒤의 대처는 “그 프로세스를 올바르게 종료시키는” 것이 정공법입니다. Process Explorer에서 핸들을 마우스 오른쪽 버튼으로 클릭해 Close Handle로 강제 종료할 수도 있지만, 뒤에서 설명할 handle -c와 같은 이유로 운영 환경에서는 권장하지 않습니다.

이 검색의 한계도 미리 짚어 두겠습니다. 적중하는 것은 이름을 가진 객체뿐입니다. 이름 없는 이벤트나 뮤텍스, 스레드 핸들은 아무리 대량으로 열려 있어도 이름으로는 검색할 수 없습니다. “파일을 지울 수 없다”처럼 경로라는 이름이 있는 조사에서는 최강이지만, 5장의 핸들 누수 조사에서는 이 방법을 쓸 수 없으며, 종류별 집계(handle -s)와 아래쪽 창의 Handles 뷰를 Type 순으로 정렬하는 방법으로 전환해야 합니다(7장의 사례 연구가 그 실례입니다).

DLLs 뷰는 “어느 위치의 어떤 버전의 DLL을 실제로 로드했는가”를 확인하는 데도 사용합니다. “오래된 DLL을 붙잡고 있었다” “배치했어야 할 수정판이 로드되지 않았다”와 같은 DLL 버전 문제 확인은, 이 화면이 전편 ProcMon 기사의 5.2절 “이 환경에서만 실행되지 않는다 ── DLL 탐색을 추적한다”와 짝을 이룹니다. 그쪽은 “탐색 순서대로 NAME NOT FOUND가 나열되고, 어디서 SUCCESS하는가(혹은 끝까지 찾지 못하는가)”를 시계열로 추적하는 절로, 어디를 찾았는지를 알 수 있습니다. 이쪽 DLLs 뷰에서는 결국 어디의 무엇을 붙잡았는지를 알 수 있습니다. 자세한 내용은 「Process Monitor(ProcMon) 실전 가이드」를 참조하세요.

애초에 “사용 중인 exe/DLL을 어떻게 안전하게 교체할 것인가”는 조사가 아니라 설계의 문제입니다. 같은 날 공개한 자매 기사 「사용 중인 exe/DLL을 어떻게 교체할까」에서 Restart Manager를 포함한 교체 설계를 다루고 있으니, 업데이트 메커니즘을 만드는 분은 그쪽을 참고하세요.

4. “행(hang)이 걸렸다” ── Threads 탭에서 스레드 스택 읽기

창이 하얗게 변하며 응답 없음 상태가 됩니다. 강제 종료하기 전에 Threads 탭에서 스레드의 스택을 확인해 보세요. 조작은 다음 4단계입니다.

  1. 위쪽 창에서 대상 프로세스를 더블클릭한다(또는 마우스 오른쪽 버튼 클릭 → Properties…). 속성 창이 열립니다
  2. Threads 탭으로 전환한다. 스레드별 CPU 사용률・사이클 수・시작 주소가 나열됩니다
  3. 확인할 스레드를 선택한다. CPU를 계속 소모하는 스레드나, 시작 주소가 앱 본체의 EXE로 되어 있는 스레드(대부분의 경우 이것이 UI 스레드입니다)가 첫 번째 후보입니다
  4. Stack 버튼을 누른다. 그 스레드가 지금 어떤 함수 호출 도중에 멈춰 있는지가, 호출된 지 얼마 안 된 순서대로 표시됩니다

행(hang)의 대부분은 “UI 스레드가 무언가를 기다리고 있는” 상태이므로, 스택 상단에 WaitForSingleObjectEnterCriticalSection, 동기적인 네트워크・시리얼 I/O 대기가 보인다면 그것이 직접적인 원인입니다.

다만 심볼을 설정하지 않으면 스택은 주소와 모듈명+0x1234의 나열이 되어 거의 읽을 수 없습니다. Options → Configure Symbols에서 다음 두 가지를 설정합니다.

Dbghelp.dll path:
  C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\dbghelp.dll
  ※ WinDbg(Debugging Tools for Windows)에 포함된 것을 지정한다

Symbols path:
  srv*C:\Symbols*https://msdl.microsoft.com/download/symbols
  ※ C:\Symbols는 로컬 캐시. 두 번째부터는 여기서 읽힌다

요점은 두 가지입니다. 먼저, System32에 있는 기본 dbghelp.dll은 심볼 서버로부터의 다운로드를 지원하지 않기 때문에 WinDbg에 부속된 것을 지정해야 한다는 점입니다. 그리고 공식 문서에 있는 대로, 심볼 서버를 사용할 경우 지정한 dbghelp.dll과 같은 위치에 symsrv.dll이 존재해야 한다는 점입니다(WinDbg 설치 폴더라면 둘 다 갖춰져 있습니다).1 심볼 경로의 srv*캐시*서버URL이라는 서식과, Microsoft의 공용 심볼 서버 https://msdl.microsoft.com/download/symbols는 WinDbg와 공통되는 구조입니다.3 자사 앱의 스택을 읽으려면 자사 빌드의 PDB도 필요하므로, 심볼 경로에 PDB 보관 폴더를 세미콜론으로 구분해 추가해 둡니다.

그 dbghelp.dll을 어디서 가져오느냐가 여기서 처음으로 막히는 지점입니다. 위 경로에 나온 C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\Debugging Tools for Windows를 설치하면 만들어지는 폴더입니다. 입수 경로는 세 가지가 있습니다.8

입수 경로 적합한 상황
Windows SDK 설치 프로그램에서 “Debugging Tools for Windows”만 선택 이번 용도에는 이것이 가장 빠릅니다. 다른 기능의 체크를 모두 해제하고 실행하면 SDK 본체는 설치하지 않고 디버거만 들어옵니다8
Windows SDK / WDK의 일부로 설치 개발 PC에서 다른 용도로도 SDK가 필요한 경우
WinDbg 단독 설치(winget install Microsoft.WinDbg 또는 Microsoft Store) WinDbg 본체를 사용하고 싶은 경우. 다만 패키지 형식으로 설치되기 때문에 위와 같은 고정 경로가 되지는 않습니다9

Process Explorer에 설정하는 것은 dbghelp.dll의 파일 경로이므로, 설치 후 그 폴더 안에 dbghelp.dllsymsrv.dll이 나란히 있는지 확인한 다음 지정하세요.

.NET 앱의 경우, Threads 탭의 스택은 네이티브 프레임 위주라서 매니지드 메서드 이름이 정확하게 나오지 않는 경우가 있습니다. 매니지드 행(hang)・데드락을 제대로 추적하려면 행이 걸린 상태에서 덤프를 채취해 WinDbg + SOS로 보는 것이 확실합니다(「WinDbg + SOS로 크래시 덤프 읽기」). Threads 탭은 “그 자리에서 3분 만에 대강 짚어보는” 도구, 덤프는 “가지고 돌아가 확정 짓는” 도구라는 역할 분담입니다. 또한 Threads 탭의 Kill/Suspend 버튼은 운영 중인 프로세스에는 절대 누르지 마세요. 관찰하려던 것이 개입이 되어 버립니다.

5. “점점 느려진다” ── 핸들 수・USER/GDI 컬럼과 handle.exe로 누수를 감시한다

장기 가동으로 인해 열화되는 앱의 단골 원인이 핸들・GDI/USER 객체・메모리의 누수입니다. Process Explorer는 감시 도구로도 우수해서, View → Select Columns에서 다음 컬럼을 추가합니다.

  • Process Performance 탭 → Handle Count(커널 객체의 핸들 수)
  • Process Memory 탭 → Private Bytes, Virtual Size, USER Objects, GDI Objects

봐야 할 것은 절대값이 아니라 기울기입니다. 건전한 앱은 핸들 수가 조작에 따라 증감하면서도 일정 범위 안에 머무르지만, 누수가 있으면 “조작할 때마다 늘어나고 두 번 다시 줄지 않는” 우상향 그래프가 됩니다. 1시간마다, 혹은 아침저녁으로 스크린샷을 남기는 것만으로도 다음 날이면 경향을 알 수 있습니다. GDI 객체와 USER 객체에는 프로세스별 상한(기본값 1만 개, 레지스트리의 GDIProcessHandleQuota 등으로 변경 가능)과 세션 전체의 이론상 상한인 65,536개가 있어, 상한에 도달하면 펜이나 브러시 생성, 창 생성이 실패하기 시작합니다.4 “금요일이 되면 화면 그리기가 깨진다”의 정체는 대개 이것입니다.

GUI를 열 수 없는 환경이나 스크립트를 이용한 정점 관측에는 CLI 버전인 handle.exe를 사용합니다. 핸들 목록・검색을 수행하는 명령줄 도구로, 관리자 권한이 필요합니다.2

:: 이 파일/폴더를 누가 잡고 있는가(이름의 부분 일치로 검색)
handle.exe /accepteula report.csv
handle.exe D:\Data

:: 파일 이외를 포함한 전체 종류의 핸들을, 프로세스 이름으로 좁혀서 덤프
handle.exe -a -p MyEquipApp

:: 종류별 핸들 수를 집계 ── 누수의 정점 관측은 이것을 로그로 남긴다
handle.exe -s -p MyEquipApp.exe >> C:\Logs\handles_%date:~0,4%%date:~5,2%%date:~8,2%.log

세 번째 줄의 %date:~0,4%에 대해 보충 설명합니다. 이것은 환경 변수의 부분 문자열 전개로, 배치 파일 안이든 명령 프롬프트에 직접 입력하는 경우든 표기법은 동일하게 % 하나입니다(%%로 이중화해야 하는 것은 for의 변수와, 리터럴 %를 쓰고 싶을 때이며, 이 표기법은 여기에 해당하지 않습니다).

다만 %date%의 내용은 OS의 짧은 날짜 형식에 의존하기 때문에, ~0,4(앞 4글자=연도)와 같은 오프셋이 환경에 따라 어긋납니다. 일본어판 Windows의 기본값은 yyyy/MM/dd라서 위 예시로 맞지만, 장비 PC의 지역 설정이 다르면 파일 이름이 깨진 형태로 만들어집니다. 여러 환경에 배포하는 정점 관측 스크립트라면, 날짜 생성만큼은 형식이 확정되는 방법으로 맞춰 두세요.

:: 환경의 날짜 형식에 의존하지 않는 작성법(배치 파일에 작성하는 경우)
for /f %%d in ('powershell -NoProfile -Command "Get-Date -Format yyyyMMdd"') do set TODAY=%%d
handle.exe -s -p MyEquipApp.exe >> C:\Logs\handles_%TODAY%.log

for의 변수는 배치 파일 안에서는 %%d, 명령 프롬프트에 직접 입력할 때는 %d로 구분해서 써야 합니다.10 위 예시는 배치 파일용이므로 그대로 명령 프롬프트에 붙여넣으면 동작하지 않습니다. 직접 시험해 볼 때는 %%d%d로 바꾸세요. 정점 관측은 작업 스케줄러로 돌리게 되므로, 실제 운영에서는 배치 파일 쪽 표기법을 사용합니다. 이 부분이 앞서 나온 %date:~0,4%(어느 쪽이든 % 하나)와 헷갈리기 쉬운 지점입니다.

-s의 출력(Event: 1523, File: 88, … 같은 종류별 집계)을 작업 스케줄러로 1시간마다 추가 기록해 두면, 원격 유지보수밖에 할 수 없는 장비 PC에서도 “어떤 종류의 핸들이 하루에 몇 개 늘어나는가”를 숫자로 확보할 수 있습니다. 종류를 알면 용의자를 단숨에 좁힐 수 있습니다. Event라면 이벤트 해제 누락, File이라면 닫기 잊음, Thread라면 스레드 종료 후 핸들 방치, 하는 식입니다.

참고로 -c 옵션으로 지정한 핸들을 강제 종료할 수 있지만, 공식 문서 자체가 “핸들을 닫으면 애플리케이션이나 시스템이 불안정해질 수 있다”고 경고하고 있습니다.2 잠금 해제의 응급 처치로도 운영 환경에서는 기본적으로 사용하지 않는 것이 저희 회사의 운용 방침입니다. “어떤 종류가 누수되고 있는가”까지 알아낸 뒤, “어느 코드가 확보했는가”를 특정하는 절차는 「산업용 카메라 장기 가동 크래시 조사 - 핸들 누수 편」과 「Application Verifier로 이상계 테스트 기반을 만든다」에서 이어집니다.

6. VMMap ── “Private Bytes가 계속 증가한다”의 내역을 분해한다

핸들은 변동 없음인데 Private Bytes만 계속 증가한다 ── 그럴 때 여는 것이 VMMap입니다. 프로세스의 가상 메모리와 물리 메모리(워킹 셋)를 분석하는 도구로, 커밋된 가상 메모리를 종류별로 분해해서 보여줍니다.5 실행해서 대상 프로세스를 선택하면 상단에 색상으로 구분된 요약, 하단에 상세한 메모리 맵이 나타납니다. 주요 분류는 다음과 같이 읽습니다.

분류 내용 계속 증가할 때의 전형적인 범인
Image EXE/DLL 본체 플러그인 DLL을 로드한 채로 방치
Heap 네이티브 힙(new/malloc/HeapAlloc) C/C++ 코드, 장비 SDK, 상호운용의 해제 누락
Managed Heap .NET의 GC 힙 매니지드 참조 누수(이벤트 핸들러 등)
Private Data VirtualAlloc에 의한 직접 확보 등 이미지 프레임 버퍼, SDK 내부 버퍼
Stack 스레드의 스택 스레드를 만든 채로 방치
Shareable / Mapped File 공유 메모리・매핑된 파일 프로세스 간 연계의 섹션 해제 누락

조작은 다음 4단계입니다.

  1. VMMap을 관리자 권한으로 실행한다. 실행 직후 프로세스 선택 대화상자가 나타나므로, 대상 프로세스를 선택하고 OK를 누른다
  2. 상단의 요약 표를 본다. 행은 위 표의 Type(Image / Heap / Managed Heap / Private Data / Stack …), 열은 SizeCommitted 같은 크기입니다. 이 시점의 값을 메모하거나, File 메뉴에서 내보내기(export)해서 남겨 두는 것이 중요합니다.
  3. 문제가 되는 조작(장비 측정 1사이클, 화면 열고 닫기 등)을 정해진 횟수만큼 반복한다
  4. F5(Refresh)로 스냅샷을 갱신하고, 2에서 남겨 둔 값과 비교한다. 증가한 Type의 행을 클릭하면 하단에 해당 영역의 목록이 나타납니다

사용법의 기본형은 “F5로 스냅샷을 갱신하면서 문제의 조작을 반복하고, 어느 분류가 증가하는지 보는” 것입니다. VMMap은 스냅샷 비교와 타임라인 표시, 결과 내보내기를 지원하므로5, “측정 100회에 Heap이 40MB 증가, Managed Heap은 변동 없음” 같은 증거를 그 자리에서 확보할 수 있습니다. 여기서 무대가 정해지면 그다음은 전용 도구의 차례입니다. Managed Heap이라면 .NET 쪽 조사(「PerfView와 dotnet-trace로 “느림”을 특정한다」), Heap/Private Data라면 네이티브 쪽 조사(Application Verifier나 덤프 분석)로 진행합니다. 이 구분 작업을 하지 않고 다짜고짜 GC를 의심하며 며칠을 허비하는 것이, .NET+네이티브 SDK 구성의 장비 앱에서 가장 흔한 우회로입니다.

32bit 프로세스에서는 단편화(fragmentation)도 관찰 대상입니다. Fragmentation View에서 주소 공간의 빈 영역이 흩어진 모습을 볼 수 있으며, Free의 합계는 수백 MB인데 최대 연속 공간(Largest)은 수십 MB밖에 안 되는 상태를 시각화해 줍니다. “메모리는 남아 있을 텐데 OutOfMemory” “큰 이미지 버퍼 확보만 실패한다”의 정체는 대개 이것이며, 대책(64bit화, 버퍼 재사용 설계)의 근거 자료가 됩니다.

프로세스 단위가 아니라 OS 전체의 메모리가 의심스러울 때(어느 프로세스도 부풀어 있지 않은데 메모리가 부족해질 때)는, 물리 메모리 전체의 용도를 탭별로 보여주는 RAMMap으로 전환합니다. 파일 캐시・드라이버・커널의 사용량까지 볼 수 있으므로, “앱 이외의 범인”은 이쪽에서 찾습니다.6

7. 사례 연구 ── “매주 금요일마다 느려지는 장비 PC”를 진단한다

서두에 나온 장비 PC를 지금까지의 도구로 실제로 진단하면 이렇게 됩니다. 월요일 아침에 재부팅하는 운영이므로 금요일은 “가동 5일째”입니다. 즉 시간에 비례해 증가하는 무언가가 있다는 것이 초기 가설입니다.

  1. 수요일쯤 Process Explorer를 배치하고 컬럼을 설정한다. Handle Count・USER Objects・GDI Objects・Private Bytes를 추가하고, 대상 프로세스의 값을 기록합니다. 아울러 handle -s -p 대상앱.exe의 매시간 로그를 작업 스케줄러에 등록합니다(5장).
  2. 다음 날, 기울기를 본다. 이 사례에서는 Handle Count가 하루 만에 약 2만 증가했고, 종류별 로그에서는 Event 핸들이 단조 증가하고 있었습니다. Private Bytes와 GDI/USER는 거의 변동이 없었습니다. 이 시점에서 “커널 객체(이벤트)의 해제 누락”으로 좁힐 수 있습니다.
  3. Handles 뷰에서 실물을 확인한다. 아래쪽 창의 Handles 뷰를 Type 순으로 정렬하면 이름 없는 Event가 수만 개 있습니다. 이름이 없어서 Find Handle로는 추적할 수 없지만, 종류와 증가 속도를 알면 충분합니다. 측정 주기(이 사례에서는 장비 폴링 1회당 2개)와의 대응 관계로부터 통신 라이브러리의 대기 이벤트 해제 누락이라는 가설을 세웠고, 코드 리뷰로 확정했습니다. 확보 위치를 도구로 특정하려면 여기서부터 Application Verifier나 !htrace가 나설 차례입니다.
  4. 만약 Private Bytes가 증가하고 있었다면, 3 대신 VMMap으로 내역을 분해하고(6장), Heap이면 네이티브, Managed Heap이면 .NET 조사로 분기합니다.
  5. 만약 어느 수치도 변동이 없는데 행(hang)만 걸린다면, Threads 탭에서 스택을 확인하고(4장), 대기 지점을 특정합니다.

요점은 수정하기 전에 “그래프의 기울기”라는 증거를 확보해 두는 것입니다. 수정 후에 같은 측정을 해서 기울기가 0이 되었음을 보여주면, “고쳐졌습니다”를 숫자로 보고할 수 있습니다. 재현을 기다리는 데 며칠 단위가 걸리는 장기 가동 문제에서는, 이 측정 준비 작업이야말로 조사의 본체입니다.

8. 운영 PC 반입과 운용 시 주의사항 ── Sysinternals Live・EULA・오프라인 심볼

Sysinternals 도구는 모두 설치가 필요 없는 독립 실행 파일(standalone)이라서, ZIP을 USB 메모리에 담아 가져가면 그대로 동작합니다. 최초 실행 시 EULA 동의 대화상자가 나타나므로, 무인 실행이나 스크립트에서는 /accepteula 스위치로 동의를 명시합니다. 네트워크를 사용할 수 있는 환경이라면 Sysinternals Live라는 서비스를 통해, 다운로드하지 않고도 https://live.sysinternals.com/procexp.exe와 같은 URL이나 \\live.sysinternals.com\tools\<도구명>의 UNC 경로로 바로 실행할 수 있습니다.7 “지금 당장 이 한 대에서 확인하고 싶다”고 할 때 가장 빠른 경로입니다.

오프라인 장비 PC에서 걸리는 부분은 심볼입니다(4장의 스택 표시를 쓸 수 없습니다). 대처법은 두 가지로, (1) 인터넷에 연결할 수 있는 개발 PC에서 같은 OS 빌드에 대해 심볼을 해결시킨 뒤, 로컬 캐시(C:\Symbols)째로 장비 PC에 복사하고 심볼 경로를 그 로컬 폴더만으로 설정한다. (2) 스택 분석은 포기하고, 현장에서는 덤프 채취(ProcDump)와 수치 기록에 전념한 뒤 개발 PC로 가져가 분석한다. 확실한 쪽은 (2)이며, 덤프를 채취하는 방법은 「Windows 크래시 덤프 수집 입문」에 정리해 두었습니다.

마지막으로 운용 측면입니다. Process Explorer나 VMMap을 이용한 관찰은 저부하・저위험이지만, 프로세스의 Kill/Suspend, 스레드의 Kill, Close Handle, handle -c와 같은 개입 조작은 운영 환경에서는 원칙적으로 금지해야 합니다. 전편의 ProcMon과 마찬가지로, 실시할 때는 일반적인 변경 작업과 같은 승인 프로세스를 거치고 “언제・누가・무엇을 관찰하고, 무엇은 조작하지 않을 것인가”를 정한 뒤 임하는 것이 안전합니다.

9. 실무 정석(판단표)

증상 사용할 도구 확인할 위치
점점 느려진다・며칠 만에 불안정해진다 Process Explorer Handle Count / USER・GDI Objects / Private Bytes 컬럼의 기울기(5장)
파일을 삭제・교체할 수 없다 Process Explorer / handle.exe Find Handle or DLL(Ctrl+F)로 경로 검색 → 잡고 있는 프로세스(3장)
행(hang)・응답 없음 Process Explorer 속성 → Threads 탭 → 스레드 스택(심볼 필요, 4장)
Private Bytes가 계속 증가한다 VMMap Heap / Managed Heap / Private Data 중 어느 것이 증가하는가(6장)
어느 프로세스도 부풀어 있지 않은데 메모리 부족 RAMMap Use Counts・File Summary로 물리 메모리 용도6
크래시한다・예외로 죽는다 ProcDump + WinDbg 덤프 수집WinDbg + SOS
CPU를 어디서 쓰고 있는가・.NET의 GC PerfView / dotnet-trace “느림”의 정량 조사
조작의 시계열(어느 파일을・언제・어떤 순서로) Process Monitor 전편 기사

10. 정리

  • ProcMon이 “조작의 이력”을 기록하는 데 비해, Process Explorer / Handle / VMMap은 “지금 이 순간의 상태”를 보는 도구입니다. 장기 가동 앱의 조사에서는 둘 다 필요합니다.
  • “파일을 지울 수 없다”는 Find Handle or DLL(Ctrl+F)로 몇 초 만에, “행(hang)이 걸렸다”는 Threads 탭의 스택으로 대강 짚어봅니다. 스택을 읽으려면 dbghelp.dll(symsrv.dll이 함께 있는 위치)과 심볼 경로 설정이 전제이며, 그 dbghelp.dll은 Debugging Tools for Windows를 설치해서 얻습니다.
  • 이름으로 검색할 수 있는 것은 이름을 가진 객체뿐입니다. 파일처럼 이름이 있는 것에는 최강이지만, 이름 없는 이벤트나 뮤텍스는 검색에 걸리지 않습니다. 핸들 누수 조사를 “Ctrl+F로 범인을 찾는” 것부터 시작하면 허탕을 치므로, 종류별 집계(handle -s)와 Handles 뷰의 Type 순 정렬로 전환하세요.
  • “점점 느려진다”는 Handle Count・USER/GDI Objects・Private Bytes 컬럼을 추가해 기울기를 봅니다. handle -s의 정기 로그라면 GUI를 열 수 없는 장비 PC에서도 숫자를 확보할 수 있습니다.
  • Private Bytes의 증가는 VMMap으로 Heap(네이티브) / Managed Heap(.NET) / Private Data로 구분한 뒤, 전용 도구로 넘어갑니다. 32bit 프로세스라면 단편화도 의심합니다.
  • handle -c나 Close Handle에 의한 강제 종료는 공식 문서가 경고하는 대로 불안정화의 원인입니다. 운영 환경에서는 관찰에만 전념하고, 개입은 하지 않거나 하더라도 반드시 승인을 거치는 것을 철저히 지키세요.
  • 도구는 독립 실행형이라 반입하기 쉽고, Sysinternals Live를 이용하면 바로 실행할 수도 있습니다. 오프라인 환경에서는 심볼의 사전 캐시나 덤프 반출로 보완합니다.

관련 글

관련 상담 영역

합동회사 코무라소프트는 장기 가동으로 열화되는 업무용 앱・장비 연동 앱의 누수 조사, 행(hang)・”파일 사용 중”과 같은 현장 장애의 원인 분석, 증거 채취 절차 정비와 재발 방지 개선을 다루고 있습니다.

참고 링크

  1. Microsoft Learn, Process Explorer - Sysinternals. Process Explorer가 프로세스가 열고 있는 핸들과 로드한 DLL・메모리 매핑 파일을 2단(handle 모드/DLL 모드)으로 표시한다는 점, 특정 핸들이나 DLL을 가진 프로세스를 검색할 수 있다는 점, DLL 버전 문제나 핸들 누수 추적에 유용하다는 점, 심볼 서버 이용 시 dbghelp.dll과 같은 위치에 symsrv.dll이 필요하다는 점, Sysinternals Live에서 직접 실행할 수 있다는 점, 동작 요건이 클라이언트는 Windows 11 이상・서버는 Windows Server 2016 이상이라는 점에 대해.  2 3 4 5 6 7

  2. Microsoft Learn, Handle - Sysinternals. handle.exe가 열려 있는 핸들 정보를 표시하는 명령줄 도구로 관리자 권한이 필요하다는 점, 이름의 부분 일치 검색, -a로 전체 종류의 핸들을 대상으로 한다는 점, -p에 의한 프로세스 좁히기, -s에 의한 종류별 핸들 수 집계, -c로 핸들을 닫을 수 있지만 “애플리케이션이나 시스템의 불안정화를 초래할 수 있다”고 경고되어 있다는 점에 대해.  2 3 4

  3. Microsoft Learn, Microsoft Public Symbol Server. Microsoft 공용 심볼 서버의 심볼 경로를 srv*로컬캐시*https://msdl.microsoft.com/download/symbols 서식으로 지정할 수 있다는 점, 다운스트림 저장소(로컬 캐시)에는 한 번 접근한 심볼만 저장되며 두 번째부터는 로컬에서 읽힌다는 점에 대해.  2

  4. Microsoft Learn, GDI Objects. GDI 핸들에 세션당 이론상 상한 65,536개가 있다는 점, 프로세스별 기본 상한이 존재하며 레지스트리의 GDIProcessHandleQuota(256~65,536 범위)로 변경할 수 있다는 점에 대해.  2

  5. Microsoft Learn, VMMap - Sysinternals. VMMap이 프로세스의 가상・물리 메모리 분석 도구이며, 커밋된 가상 메모리의 종류별 내역과 각 종류에 할당된 물리 메모리(워킹 셋)를 표시한다는 점, 필터와 갱신(스냅샷) 기능, 데이터 내보내기와 명령줄 옵션에 의한 스크립트화를 지원한다는 점에 대해.  2 3

  6. Microsoft Learn, RAMMap - Sysinternals. RAMMap이 OS 전체의 물리 메모리 사용 현황을 분석하는 도구로, Use Counts(종류별 집계), Processes(프로세스의 워킹 셋), File Summary(RAM 상의 파일 데이터) 등의 탭으로 용도를 표시한다는 점, 스냅샷의 저장・불러오기를 지원한다는 점에 대해.  2 3

  7. Microsoft Learn, Sysinternals. Sysinternals Live가 도구를 다운로드하지 않고도 실행할 수 있는 서비스이며, live.sysinternals.com/<도구명>의 URL 또는 \\live.sysinternals.com\tools\<도구명>의 경로로 직접 실행할 수 있다는 점, live.sysinternals.com에서 도구 목록을 확인할 수 있다는 점에 대해.  2

  8. Microsoft Learn, Debugging Tools for Windows SDK and WDK. Debugging Tools for Windows가 Windows SDK 및 WDK에 포함되어 있다는 점, Windows SDK 설치 프로그램을 실행해 기능 목록에서 “Debugging Tools for Windows”만 선택하고 나머지를 모두 해제함으로써 디버거만 단독으로 설치할 수 있다는 점, 설치 프로그램의 입수처가 Windows SDK라는 점에 대해.  2

  9. Microsoft Learn, Install WinDbg. WinDbg가 크래시 덤프 분석이나 사용자 모드・커널 모드 디버깅에 사용할 수 있다는 점, 설치 프로그램을 직접 실행하는 방법・Microsoft Store 경유・winget install Microsoft.WinDbg에 의한 설치 방법이 있다는 점, 기존의 WinDbg(classic)는 Debugging Tools for Windows에 포함되어 있다는 점에 대해. 

  10. Microsoft Learn, for. 배치 파일 안에서는 %%variable, 명령 프롬프트에서 직접 실행하는 경우에는 %variable로 작성한다는 점, 그리고 for /f가 명령의 출력을 해석(파싱)할 수 있다는 점에 대해. 

같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.

이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.

이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.

자주 묻는 질문

이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.

작업 관리자가 있는데 굳이 Process Explorer를 쓰는 이유는 무엇인가요?
작업 관리자로는 '어느 프로세스가 어떤 파일이나 객체를 열고 있는가', '각 스레드가 지금 어느 코드에서 멈춰 있는가'가 보이지 않기 때문입니다. Process Explorer는 프로세스별 핸들・DLL 목록, 이름을 이용한 핸들 검색(Ctrl+F), 스레드 스택 표시까지 할 수 있어서, '파일을 지울 수 없다', '행(hang)이 걸렸다'의 조사가 작업 관리자와는 차원이 다릅니다. 핸들 수나 USER/GDI 객체 수 컬럼을 추가하면 누수 감시 도구로도 사용할 수 있습니다. Options 메뉴의 Replace Task Manager를 켜 두면 작업 관리자 호출이 Process Explorer로 대체되므로, 조사 담당자의 PC에서는 아예 대체해 버리는 것이 실무적입니다.
handle.exe의 -c 옵션으로 핸들을 닫으면 파일 잠금을 바로 해제할 수 있나요?
기술적으로는 가능하지만, 운영 환경에서는 기본적으로 사용하지 마세요. 공식 문서 자체가 '핸들을 닫으면 애플리케이션이나 시스템이 불안정해질 수 있다'고 경고하고 있습니다. 프로세스의 내부 상태를 무시하고 외부에서 핸들을 빼앗기 때문에, 그 프로세스가 나중에 같은 핸들을 사용하는 순간 다른 객체를 가리키고 있어 오동작하는(핸들 값 재사용) 최악의 사고가 일어날 수도 있습니다. 정공법은 잡고 있는 프로세스를 특정해서 올바르게 종료시키는 것입니다. 사용 중인 exe나 DLL을 교체하고 싶은 맥락이라면 Restart Manager 같은 설계적 해결책을 검토하세요.
Private Bytes가 계속 증가하고 있습니다. 가장 먼저 무엇을 해야 하나요?
대상 프로세스를 VMMap으로 열어, 증가하고 있는 것이 어느 분류인지 보는 것이 첫 번째 수순입니다. Heap이 증가한다면 malloc/new/HeapAlloc에 의한 네이티브 누수가 강하게 의심되며, 장비 벤더의 SDK나 C++/CLI・상호운용 코드가 용의자가 됩니다. Managed Heap이 증가한다면 .NET의 매니지드 힙 문제이므로, GC가 회수할 수 없는 참조 조사(이벤트 핸들러 해제 누락 등)로 넘어갑니다. Private Data(VirtualAlloc 직접 확보)가 증가하는 경우에는 이미지 버퍼 같은 큰 블록을 확보하는 코드나 SDK를 의심합니다. 구분이 끝난 뒤 각각 전용 도구(WinDbg, PerfView 등)로 넘어가면 조사가 길을 잃지 않습니다.
오프라인 장비 PC에서는 Threads 탭의 스택이 주소 나열만 보여서 읽을 수 없습니다. 어떻게 해야 하나요?
심볼(PDB)을 다운로드할 수 없기 때문이며, 대처법은 '심볼을 가지고 들어간다' 또는 '덤프를 가지고 나온다'의 두 가지 중 하나입니다. 가지고 들어가는 경우에는 인터넷에 연결할 수 있는 개발 PC에서 같은 OS 빌드・같은 앱 구성에 대해 심볼을 한 번 해결시킨 뒤, 로컬 캐시 폴더(예: C:\Symbols)째로 장비 PC에 복사하고 심볼 경로를 그 로컬 폴더만으로 설정합니다. 가지고 나오는 경우에는 행(hang) 상태에서 덤프를 채취해 개발 PC의 WinDbg로 분석하는 쪽이 더 확실하며, 매니지드 앱의 스택을 정확하게 읽고 싶을 때도 이쪽이 적합합니다. 자사 앱의 PDB는 빌드마다 보관해 두는 것이 대전제입니다.
Process Explorer나 VMMap을 운영 PC・장비 PC에 설치해도 문제없나요?
둘 다 설치가 필요 없는 독립 실행 파일이며, 레지스트리 등록이나 서비스 상주 없이 동작하기 때문에 반입 장벽이 낮은 도구입니다. 최초 실행 시 EULA 동의가 필요하며, 스크립트에서 사용할 경우 /accepteula 스위치로 동의를 명시할 수 있습니다. 네트워크를 사용할 수 있는 환경이라면 Sysinternals Live(https://live.sysinternals.com)에서 다운로드하지 않고 바로 실행할 수도 있습니다. 다만 관찰은 저위험이라도, 프로세스의 Kill/Suspend, 스레드의 Kill, 핸들 강제 종료 같은 '개입' 조작은 사고로 직결되므로, 운영 환경에서는 관찰과 기록에만 전념하고 일반적인 작업 승인 프로세스를 거쳐 실시하세요.

저자 프로필

기사 저자의 프로필 페이지입니다.

Go Komura

합동회사 코무라소프트 대표

Windows 소프트웨어 개발, 기술 상담, 장애 조사를 중심으로 재현이 어려운 장애 조사와 기존 자산이 남아 있는 프로젝트에 강점이 있습니다.

블로그 목록으로 돌아가기