Process Explorer / Handle / VMMap 實戰 ── 從「此刻」的狀態追查無回應、洩漏、「檔案使用中」

· · Sysinternals, Process Explorer, VMMap, 控制代碼洩漏, 記憶體洩漏, 故障調查, Windows, 長時間運轉

「星期一早上重開機就會好了」── 這是我們在裝置連動應用程式的維護諮詢中,聽過無數次的一句話。剛啟動時運作順暢,但到了星期四左右畫面切換開始變得遲鈍,星期五傍晚甚至按下按鈕後要好幾秒才有反應。偶爾還會冒出從沒見過的「控制代碼無效」錯誤。而只要重新啟動,就會像什麼事都沒發生過一樣恢復正常。演變到這個地步,維運方式就固定成了「每週重開機」,而根本原因始終沒有人知道,就這樣過了好幾年。

這類症狀,無論再怎麼盯著記錄檔看,都找不到原因。真正需要的是直接查看「此刻,這個處理程序究竟抱著什麼、抱著多少」。上一篇「Process Monitor(ProcMon)實戰指南」處理的是記錄操作時間序列的 ProcMon。這次作為續篇、Sysinternals 實戰系列的第 2 彈,將以「看狀態」這一側的工具 ── Process ExplorerHandleVMMap ── 沿著長時間運轉應用程式的三大症狀「愈用愈重」「檔案刪不掉」「無回應」來做整理。

本文的前提環境如下。

  • OS ── Process Explorer 官方的執行需求,用戶端為 Windows 11 以上,伺服器為 Windows Server 2016 以上1
  • 位元數 ── 在 64 位元環境下啟動 procexp.exe,會自動展開並執行 64 位元版本。要正確查看 64 位元處理程序的堆疊與控制代碼,必須以這個 64 位元版本執行。
  • 權限 ── 以調查為目的時,「以系統管理員身分執行」實質上是必要條件。若維持一般權限,僅限於其他使用者或服務的處理程序,執行緒堆疊與控制代碼清單都會出現「存取被拒」。CLI 版的 handle.exe 官方文件明確要求必須具備系統管理員權限。2
  • 首次啟動 ── 會出現 EULA(使用授權合約)同意對話方塊。在指令碼或無人執行的情況下,可用 /accepteula 開關明確表示同意(第 8 章)。

另外,本文雖然說明的是 GUI 工具的操作,但並未附上畫面截圖。取而代之的是,文中直接沿用實際的選單名稱與快速鍵寫法,讓讀者可以一邊在手邊開啟工具、一邊閱讀。

1. 先講結論

  • ProcMon 是「歷史」,Process Explorer 是「現在」。Process Explorer 是可以列出並搜尋每個處理程序所開啟的控制代碼與已載入 DLL 的工具,官方也明確表示它適合用於調查 DLL 版本問題與控制代碼洩漏。1
  • 「檔案刪不掉、換不掉」時,Find Handle or DLL(Ctrl+F)是最快的捷徑。只要用路徑的一部分搜尋,就能在數秒內鎖定佔用該檔案的處理程序(第 3 章)。
  • 「無回應」時,可在處理程序的 Properties → Threads 分頁查看執行緒堆疊。但若沒有設定符號(dbghelp.dll 與符號路徑),看到的就只會是一堆位址(第 4 章)。13
  • 「愈用愈重」要靠新增欄位來觀察斜率。新增 Handle Count、USER Objects、GDI Objects、Private Bytes 這 4 個欄位,經過一段時間後,找出持續增加的項目。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(64 位元環境會自動改跑 procexp64)即可。1 由於這是一款能看到其他使用者處理程序與服務內部狀態的工具,調查時務必「以系統管理員身分執行」。若以一般權限啟動,偏偏就是關鍵的那個處理程序會出現「存取被拒」,堆疊與控制代碼都看不到。

在負責調查的機器上,只要啟用 Options → Replace Task Manager,透過 Ctrl+Shift+Esc 或工作列叫出工作管理員時,就會直接改成開啟 Process Explorer。「緊急時刻打開的是自己用慣的工具」,這個效果看似不起眼,實際上影響不小,本公司的開發機也全數做了這項替換(要改回來,同樣從這個選單操作)。

畫面採上下兩窗格結構:上方是處理程序樹狀圖,下方是所選處理程序的詳細資訊。下方窗格可透過 View → Lower Pane View 切換Handles 檢視(已開啟控制代碼的清單)與DLLs 檢視(已載入 DLL 與記憶體映射檔案的清單)。1 首先該記住的是列的顏色代表的意義(可透過 Options → Configure Colors 確認、變更)。

顏色(預設) 意義
綠色 剛啟動的處理程序(預設約 1 秒)
紅色 即將結束的處理程序
淺藍色 與自己相同使用者帳戶的處理程序
粉紅色 承載服務的處理程序
紫色 疑似被封裝(壓縮、混淆)的執行檔
深灰色 已暫停的處理程序

「一瞬間冒出綠色的處理程序,隨即變紅然後消失」這種不斷重複發生的異常,光靠樹狀圖與顏色區分就能發現。由於處理程序的親子關係在樹狀圖中一目瞭然,「這個 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.csvD:\Data)。不需要輸入完整路徑,系統會以部分一致的方式搜尋
  4. 按下 Search。開啟了包含該名稱的控制代碼的處理程序,或已載入該 DLL 的處理程序,都會列在清單中
  5. 點擊結果中的某一列,上方窗格會選取對應的處理程序,下方窗格則會醒目提示對應的控制代碼

找到之後的正確做法,是「讓那個處理程序正常結束」。雖然也可以在 Process Explorer 上對控制代碼按右鍵,用 Close Handle 強制關閉,但基於與後述 handle -c 相同的理由,不建議在正式環境中這麼做。

請先掌握這種搜尋方式的限制。能命中的只有具名物件。無名的事件、Mutex、執行緒控制代碼,不管開了多少個,都無法用名稱搜尋。對於像「檔案刪不掉」這種存在路徑名稱的調查,這個方法最為有力,但在第 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. 「無回應」── 用 Threads 分頁讀取執行緒堆疊

視窗變白、沒有回應。在強制結束之前,請先在 Threads 分頁查看執行緒的堆疊。操作步驟共 4 步。

  1. 在上方窗格中對目標處理程序按兩下(或按右鍵 → Properties…),即可開啟 Properties 視窗
  2. 切換到 Threads 分頁。畫面會列出每個執行緒的 CPU 使用率、循環數、起始位址
  3. 選擇要查看的執行緒。持續佔用 CPU 的執行緒,或起始位址落在應用程式本體 EXE 的執行緒(多數情況下這就是 UI 執行緒),是最先該檢查的候選
  4. 按下 Stack 按鈕。畫面會依呼叫的新舊順序,顯示該執行緒目前停在哪個函式呼叫的途中

大多數的無回應,都是因為「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 為本機快取。第 2 次以後會從這裡讀取

這裡有兩個要點。首先,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 後產生的資料夾。取得途徑有 3 種。8

取得途徑 適合的場合
在 Windows SDK 安裝程式中只勾選「Debugging Tools for Windows」 對本文用途而言這是最快的做法。只要取消勾選其他所有功能再執行,就能不安裝 SDK 本體、只安裝偵錯器8
作為 Windows SDK / WDK 的一部分安裝 開發機上還有其他用途需要用到 SDK 時
單獨安裝 WinDbg(winget install Microsoft.WinDbg 或 Microsoft Store) 想使用 WinDbg 本體時。不過因為是以套件形式安裝,不會像上面那樣有固定路徑9

由於 Process Explorer 中設定的是 dbghelp.dll 的檔案路徑,請在安裝完成後,先確認該資料夾內 dbghelp.dllsymsrv.dll 是否並列存在,再進行指定。

.NET 應用程式的情況下,Threads 分頁的堆疊以原生框架為主,受管理方法的名稱有時無法正確顯示。若要認真追查受管理的無回應、死結,在無回應期間採集傾印檔、以 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 BytesVirtual SizeUSER ObjectsGDI 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

補充說明第 3 行的 %date:~0,4%。這是環境變數的子字串展開,不管是在批次檔內還是直接在命令提示字元中輸入,寫法都是同一個 %(需要把 % 加倍寫成 %% 的,是 for 的變數,以及想輸入字面上的 % 時,這個寫法並不適用)。

不過,%date% 的內容取決於作業系統的短日期格式,所以像 ~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……這種按類別統計的結果)追加寫入日誌,即使是只能遠端維護的裝置 PC,也能用數字掌握「哪種控制代碼一天會增加多少」。只要知道類別,就能一口氣縮小容疑範圍:Event 的話懷疑事件釋放遺漏、File 的話懷疑忘記關閉、Thread 的話懷疑執行緒結束後控制代碼未清理,大致是這樣的對應。

另外,雖然可以用 -c 選項強制關閉指定的控制代碼,但官方文件本身就警告「關閉控制代碼可能導致應用程式或系統變得不穩定」。2 即使是作為解鎖的應急處置,本公司的原則也是正式環境基本上不使用。在確認「哪種類別在洩漏」之後,接下來鎖定「是哪段程式碼配置的」的步驟,將延續到「工業相機控制應用跑一個月後突然崩潰時(前篇) - handle leak 的找法與長時間運轉用的日誌設計」與「工業相機控制應用跑一個月後突然崩潰時(後篇) - 什麼是 Application Verifier 與異常系測試基盤的做法」。

6. VMMap ── 拆解「Private Bytes 持續增加」的組成

控制代碼持平,卻只有 Private Bytes 持續增加 ── 這時候該打開的就是 VMMap。這是一款分析處理程序虛擬記憶體與實體記憶體(工作集)的工具,能把已認可(commit)的虛擬記憶體按種類拆解顯示出來5 啟動後選擇目標處理程序,上段會顯示以顏色區分的摘要,下段則是詳細的記憶體對應表。主要的分類判讀方式如下。

分類 內容 持續增加時的典型犯人
Image EXE/DLL 本體 外掛 DLL 一直沒卸載
Heap 原生堆積(new/malloc/HeapAlloc) C/C++ 程式碼、裝置 SDK、Interop 的釋放遺漏
Managed Heap .NET 的 GC 堆積 受管理端的參照洩漏(事件處理常式等)
Private Data 透過 VirtualAlloc 直接配置等 影像畫格緩衝區、SDK 內部緩衝區
Stack 執行緒的堆疊 執行緒建立後一直沒回收
Shareable / Mapped File 共用記憶體、記憶體映射檔案 跨處理程序協作用的區段(Section)釋放遺漏

操作步驟共 4 步。

  1. 以系統管理員身分執行 VMMap。啟動後會立即出現處理程序選擇對話方塊,選取目標處理程序後按下 OK
  2. 查看上段的摘要表。列對應上表中的 Type(Image / Heap / Managed Heap / Private Data / Stack……),欄則是 SizeCommitted 等大小數值。這個時間點的數值,一定要先記下來,或從 File 選單匯出保存,這一步很關鍵
  3. 將有問題的操作(裝置的一次量測循環、畫面的開關等)重複執行固定次數
  4. F5(Refresh)更新快照,並與步驟 2 記錄的數值比較。點擊有增加的 Type 那一列,下段就會顯示該區域的清單

使用方式的固定套路是「一邊按 F5 更新快照、一邊重複有問題的操作,觀察哪個分類在增加」。由於 VMMap 支援快照比較、時間軸顯示與結果匯出5,因此可以當場取得「量測 100 次後 Heap 增加 40MB,Managed Heap 持平」這樣的證據。一旦確定了擂台,接下來就輪到專用工具上場:若是 Managed Heap,就進行 .NET 端的調查(「用 PerfView 與 dotnet-trace 找出「變慢」的原因」);若是 Heap/Private Data,則進行原生端的調查(Application Verifier 或傾印檔解析)。在 .NET + 原生 SDK 構成的裝置應用程式中,最常見的繞遠路,就是不做這樣的切分,一開始就懷疑 GC,白白耗掉好幾天

32 位元處理程序的情況下,片段化也是觀察對象。透過 Fragmentation View 能看到位址空間中空閒區塊的分散情形,可視化呈現「Free 合計有數百 MB,但最大連續空閒區塊(Largest)卻只有幾十 MB」這種狀態。「明明記憶體應該還夠用,卻發生 OutOfMemory」「只有大型影像緩衝區的配置會失敗」,真相多半就是這個,這也能作為對策(改用 64 位元、緩衝區重複使用設計)的佐證資料。

當可疑的不是單一處理程序、而是整個 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 追蹤,但只要知道類別與增加速度就已經足夠。從測量週期(這個案例中裝置每輪詢一次會增加 2 個)的對應關係,可以推導出「通訊函式庫的等待事件釋放遺漏」這個假設,並透過程式碼審查確定。若要用工具鎖定配置位置,接下來就輪到 Application Verifier 或 !htrace 出場。
  4. 如果 Private Bytes 有增加,就取代步驟 3,改用 VMMap 拆解組成(第 6 章),Heap 分支到原生端調查,Managed Heap 分支到 .NET 端調查。
  5. 如果所有數字都持平、只有出現無回應,就在 Threads 分頁查看堆疊(第 4 章),鎖定等待的對象。

重點在於,修正前要先確保「圖表斜率」這項證據。修正後再做一次相同的測量,只要能證明斜率歸零,就能用數字報告「已經修好了」。對於需要等待數天才會重現的長時間運轉問題,這個測量的事前布置,才是調查的本體。

8. 帶入正式環境 PC 與維運注意事項 ── Sysinternals Live、EULA、離線環境的符號

Sysinternals 工具全都是不需要安裝的獨立執行檔,只要用 USB 隨身碟把 ZIP 帶進去,就能直接執行。首次啟動時會出現 EULA 同意對話方塊,若是無人執行或指令碼呼叫,可用 /accepteula 開關明確表示同意。若環境可以使用網路,還有一項名為 Sysinternals Live 的服務,可以不必下載,直接從 https://live.sysinternals.com/procexp.exe 這樣的網址,或 \\live.sysinternals.com\tools\<工具名稱> 的 UNC 路徑執行。7 這是「現在就想在這一台上看看」時最快的路徑。

離線裝置 PC 上會卡住的是符號(第 4 章的堆疊顯示無法使用)。因應方式有 2 種:(1)在能連上網路的開發機上,針對相同的 OS 組建先解析好符號,連同本機快取(C:\Symbols)一起複製到裝置 PC,並把符號路徑只指向那個本機資料夾。(2)放棄堆疊解析,現場只專注於採集傾印檔(ProcDump)與數值記錄,帶回開發機解析。確實可靠的是 (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 章)
無回應、沒有回應 Process Explorer Properties → 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)幾秒鐘就能鎖定,「無回應」則用 Threads 分頁的堆疊找出頭緒。要讀懂堆疊,前提是設定好 dbghelp.dll(附帶 symsrv.dll 的那個位置)與符號路徑,而這個 dbghelp.dll 要透過安裝 Debugging Tools for Windows 取得。
  • 能用名稱搜尋的,只有具名的物件。對於像檔案這種有名稱的東西是最強力的手段,但無名的事件或 Mutex 不會被搜尋命中。若把控制代碼洩漏的調查,從「用 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,再進入專用工具。若是 32 位元處理程序,也要懷疑片段化。
  • 用 handle -c 或 Close Handle 強制關閉,正如官方所警告,是導致不穩定的根源。正式環境中請徹底貫徹「只做觀察、不做介入,要介入就一定要走核准流程」。
  • 這些工具都是獨立執行檔,容易帶入現場,用 Sysinternals Live 還能直接執行。在離線環境中,則要靠事前快取符號,或把傾印檔帶回去解析來補足。

相關文章

相關諮詢領域

合同會社小村軟體處理長時間運轉後會逐漸劣化的業務應用程式、裝置連動應用程式的洩漏調查,無回應、「檔案使用中」等現場故障的原因分析,以及證據採集程序的建立與再發防止的改修。

參考連結

  1. Microsoft Learn,Process Explorer - Sysinternals。關於 Process Explorer 以雙窗格(handle 模式/DLL 模式)顯示處理程序已開啟的控制代碼,以及已載入的 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 次以後會從本機讀取的說明。  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/<工具名稱> 的網址,或 \\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),甚至顯示執行緒堆疊,因此「檔案刪不掉」「無回應」的調查,會跟工作管理員完全是不同的層次。只要新增控制代碼數、USER/GDI 物件數等欄位,也能當成洩漏監控工具使用。啟用 Options 選單中的 Replace Task Manager,呼叫工作管理員時就會改成開啟 Process Explorer,所以在負責調查的機器上,直接替換掉是比較實務的做法。
用 handle.exe 的 -c 選項關閉控制代碼,可以馬上解除檔案的鎖定嗎?
技術上是可行的,但正式環境中基本上請不要使用。官方文件本身就警告「關閉控制代碼可能導致應用程式或系統變得不穩定」。因為這是無視處理程序的內部狀態、從外部強行奪取控制代碼,最壞的情況下,可能發生該處理程序之後再度使用同一個控制代碼時,卻指向了另一個物件而誤動作(控制代碼值被重複使用)這種事故。正確做法是鎖定佔用的處理程序,讓它正常結束。若是想替換使用中的 exe 或 DLL 這種情境,請考慮 Restart Manager 等設計層面的解決方案。
Private Bytes 持續增加。最先該做的是什麼?
第一步是用 VMMap 開啟目標處理程序,查看是哪個分類在增加。若是 Heap 增加,很可能是 malloc/new/HeapAlloc 造成的原生洩漏,裝置廠商的 SDK 或 C++/CLI、Interop 程式碼會是嫌疑對象。若是 Managed Heap 增加,那就是 .NET 受管理堆積的問題,接下來要調查 GC 無法回收的參照(例如事件處理常式解除訂閱遺漏)。若是 Private Data(透過 VirtualAlloc 直接配置)增加,則要懷疑配置了影像緩衝區這類大型區塊的程式碼或 SDK。先完成這樣的切分,再進入各自的專用工具(WinDbg、PerfView 等),調查就不會迷路。
在離線的裝置 PC 上,Threads 分頁的堆疊會變成一堆位址而無法閱讀。該怎麼辦?
這是因為無法下載符號(PDB),因應方式只有「帶入符號」與「帶回傾印檔」二選一。帶入符號的做法,是在能連上網路的開發機上,針對相同的 OS 組建、相同的應用程式組態,先解析一次符號,再連同本機快取資料夾(例如 C:\Symbols)一起複製到裝置 PC,並將符號路徑只指向那個本機資料夾。帶回傾印檔的做法,則是在無回應期間採集傾印檔,帶回開發機用 WinDbg 解析,這樣更確實可靠,若想精確讀取受管理應用程式的堆疊,也更適合用這個方法。自家應用程式的 PDB 按建置版本保存,是一切的大前提。
把 Process Explorer 或 VMMap 帶進正式環境 PC、裝置 PC,有沒有問題?
這兩者都是不需要安裝的獨立執行檔,不需要登錄檔登記或服務常駐即可運作,因此帶入現場的門檻很低。首次啟動時需要同意 EULA,若從指令碼呼叫,可用 /accepteula 開關明確表示同意。若環境可以使用網路,也能不下載、直接從 Sysinternals Live(https://live.sysinternals.com)執行。不過,即使觀察本身風險低,處理程序的 Kill/Suspend、執行緒的 Kill、控制代碼的強制關閉這類「介入」操作,卻會直接導致事故,因此正式環境中請徹底只做觀察與記錄,並走一般的作業核准流程來實施。

作者檔案

本文作者的個人檔案頁面。

Go Komura

小村軟體有限公司 代表

以 Windows 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。

回到部落格一覽