Process Explorer / Handle / VMMap 实战 ── 从此刻的状态追查挂起・泄漏・「文件正在使用」问题
· Go Komura · Sysinternals, Process Explorer, VMMap, 句柄泄漏, 内存泄漏, 故障排查, Windows, 长期运行
「周一早上重启一下就好了」── 这是在设备联动应用的维护咨询中,我们反复听到的一句话。刚启动时运行流畅,可到了周四左右画面切换开始变得迟钝,周五傍晚甚至按下按钮几秒钟都没有反应。偶尔还会出现从未见过的「句柄无效」错误。而只要重启,就会像什么都没发生过一样恢复正常。到了这一步,运维方式就固定为「每周重启」,而根本原因谁也不知道,就这样过了好几年。
这类症状,无论把日志看多少遍都找不到原因。真正需要的是直接查看「此刻这一瞬间,该进程究竟抱着多少什么样的东西」。上一篇《Process Monitor(ProcMon)实战指南》讲的是记录操作时间序列的 ProcMon。这次作为其续篇、Sysinternals 实战第二弹,我们来梳理专门查看状态一侧的工具 ── Process Explorer・Handle・VMMap ── 并围绕长期运行应用的三大症状「逐渐变卡」「文件删不掉」「挂起无响应」来展开说明。
本文的前提环境如下。
- 操作系统 ── Process Explorer 官方给出的运行要求是:客户端为Windows 11 及以上,服务器为Windows Server 2016 及以上。1
- 位数 ── 在 64 位环境下启动
procexp.exe,会自动展开并运行 64 位版本。要正确查看 64 位进程的堆栈和句柄,必须运行在这个 64 位版本上。 - 权限 ── 出于调查目的,「以管理员身份运行」实际上是必需的。以普通权限启动时,仅针对其他用户或服务的进程,线程堆栈和句柄列表都会显示「拒绝访问」。命令行版的
handle.exe官方也要求管理员权限。2 - 首次启动 ── 会弹出 EULA(最终用户许可协议)同意对话框。在脚本或无人值守执行中,可用
/accepteula开关明确表示同意(第 8 章)。
另外,本文虽然讲解的是 GUI 工具的操作,但并未附上截图。取而代之的是,菜单名称和快捷键都按实际表述原样写出,方便读者一边打开工具一边对照阅读。
1. 先说结论
- ProcMon 是「历史」,Process Explorer 是「现在」。Process Explorer 是一款可以列出并搜索各进程所打开句柄与已加载 DLL 的工具,官方也明确表示它适合用于排查 DLL 版本问题和句柄泄漏。1
- 「文件删不掉・替换不了」用 Find Handle or DLL(Ctrl+F)最快。只要用路径的一部分进行搜索,几秒钟就能定位到占用该文件的进程(第 3 章)。
- 「挂起了」要在进程属性 → Threads 选项卡中查看线程堆栈。不过如果不设置符号(dbghelp.dll 与符号路径),显示出来的只会是一堆地址(第 4 章)。13
- 「逐渐变卡」要添加列,观察其变化趋势。添加 Handle Count・USER Objects・GDI Objects・Private Bytes 这 4 列,间隔一段时间观察哪个数值在持续增长。GDI/USER 对象都有各进程独立的上限,一旦达到上限,绘图和窗口创建就会开始出错(第 5 章)。4
- 命令行版的 handle.exe 适合用脚本进行定点观测。用
handle -s -p <进程>可以定期记录各类型的句柄数。-c强制关闭句柄这一操作,官方警告可能导致系统不稳定,基本不使用。2 - 「Private Bytes 持续增长」的构成用 VMMap 来拆分。Heap 增长指向原生代码,Managed Heap 增长指向 .NET,先确定嫌疑犯所在的阵地,再进入专用工具(第 6 章)。5
- 如果可疑的不是某个进程而是整个操作系统的内存,就用 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 个步骤。
- 以管理员身份运行 Process Explorer(忘记这一步的话,如果占用者是服务或其他用户的进程,就看不到)
- 打开菜单中的 Find > Find Handle or DLL…(快捷键 Ctrl+F)
- 在搜索字符串输入框中输入文件名或文件夹名的一部分(例如:
report.csv、D:\Data)。不需要输入完整路径,系统会按部分匹配进行搜索 - 点击 Search。打开了包含该名称的句柄,或加载了对应 DLL 的进程就会列出来
- 点击结果中的某一行,上方栏会选中对应进程,下方栏会高亮对应句柄
找到之后的正确做法是「让该进程正常退出」。虽然也可以在 Process Explorer 中右键点击句柄,通过 Close Handle 强制关闭,但出于和后文 handle -c 相同的理由,并不推荐在生产环境中使用。
这里先说明一下这种搜索方式的局限性:命中的只有具名对象。无名的事件(Event)、互斥体(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 个步骤。
- 在上方栏中双击目标进程(或右键点击 → Properties…)。属性窗口随即打开
- 切换到 Threads 选项卡。会列出各线程的 CPU 使用率・周期数・起始地址
- 选择要查看的线程。持续占用 CPU 的线程,或者起始地址位于应用本体 EXE 内的线程(多数情况下这就是 UI 线程),是首先要考虑的候选
- 点击 Stack 按钮。该线程当前停在哪个函数的调用途中,会按调用顺序从最新的一层开始依次显示出来
挂起的绝大多数情况都是「UI 线程在等待某样东西」,因此如果在堆栈上方看到 WaitForSingleObject、EnterCriticalSection,或是同步的网络・串口 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 这种书写格式,以及微软的公共符号服务器 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 本体的情况。不过由于以打包(package)形式安装,不会像上面那样形成固定路径9 |
由于 Process Explorer 中设置的是dbghelp.dll 的文件路径,安装完成后,请先确认该文件夹内 dbghelp.dll 和 symsrv.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 Bytes、Virtual Size、USER Objects、GDI Objects
要看的不是绝对值,而是变化趋势。健康的应用,句柄数会随着操作而增减,但始终维持在一定范围内;而一旦发生泄漏,就会呈现「每次操作都增加,再也不会减少」的持续上扬曲线。哪怕只是每隔 1 小时,或者每天早晚留一张截图,第二天也能看出趋势。GDI 对象和 USER 对象都存在各进程独立的上限(默认为 1 万个,可通过注册表的 GDIProcessHandleQuota 等更改)以及会话全体理论上限 65,536 个,一旦达到上限,画笔・画刷的创建、窗口的创建就会开始失败。4 「一到周五画面就开始花屏」,真相多半就是这个。
对于无法打开 GUI 的环境,或者需要用脚本进行定点观测的场景,可以使用命令行版的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 泄漏篇》和《用 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 | 共享内存・映射文件 | 进程间通信的节(section)释放遗漏 |
操作分为以下 4 个步骤。
- 以管理员身份运行 VMMap。启动后会立即弹出进程选择对话框,选中目标进程后点击 OK
- 查看上方的汇总表。行是上表中的Type(Image / Heap / Managed Heap / Private Data / Stack……),列是 Size、Committed 等大小信息。把这一时刻的数值记下来,或者从 File 菜单导出留存是关键所在
- 把有问题的操作(设备测量 1 个周期、画面开关等)重复固定的次数
- 按 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 组合而成的设备应用中最常见的绕远路情形。
对于 32 位进程,碎片化同样是观察对象。Fragmentation View 能显示地址空间中空闲区域的分散情况,可以直观地看到「Free 总量有数百 MB,但最大连续空闲(Largest)只有几十 MB」这样的状态。「内存明明应该是够用的却 OutOfMemory」「唯独大块图像缓冲区分配失败」,其真相多半就在于此,这也能成为对策(64 位化、缓冲区复用设计)的依据资料。
当可疑的不是某个进程,而是整个操作系统的内存(每个进程看起来都不胖,却仍然内存不足)时,就切换到按选项卡展示整个物理内存用途的RAMMap。它连文件缓存・驱动程序・内核的使用量都能看到,「应用以外的元凶」就靠它来查找。6
7. 案例研究 ── 切分「每逢周五就变卡的设备 PC」
用到目前为止介绍的这些工具,对开头提到的那台设备 PC 实际进行切分,过程如下。由于运维方式是周一早上重启,周五就相当于「运行第 5 天」。也就是说,初始假设是存在某种与时间成比例持续增长的东西。
- 在周三前后放上 Process Explorer,布置好观测列。添加 Handle Count・USER Objects・GDI Objects・Private Bytes,记录目标进程的数值。同时把
handle -s -p 目标应用.exe的每小时日志注册到任务计划程序中(第 5 章)。 - 第二天查看变化趋势。在这个案例中,Handle Count 一天内增加了约 2 万,按类型分类的日志显示 Event 句柄呈单调递增。Private Bytes 和 GDI/USER 则几乎持平。到这一步就可以锁定为「内核对象(事件)的释放遗漏」。
- 在 Handles 视图中查看实物。把下方 Handles 视图按 Type 排序,会看到数万个无名的 Event。由于没有名称,用 Find Handle 追踪不到,但只要知道类型和增长速度就已经足够。根据与测量周期(此案例中设备每轮询 1 次就产生 2 个)的对应关系,提出了「通信库的等待事件释放遗漏」这一假设,并通过代码审查得到确认。如果要用工具进一步定位分配位置,接下来就轮到 Application Verifier 或 !htrace 登场了。
- 如果 Private Bytes 出现了增长,就用 VMMap 拆分构成(第 6 章)来代替第 3 步,Heap 增长就分流到原生调查,Managed Heap 增长就分流到 .NET 调查。
- 如果所有数字都持平却只是发生挂起,就在 Threads 选项卡查看堆栈(第 4 章),锁定等待对象。
关键在于,在修复之前先确保拿到「曲线趋势」这一证据。修复之后进行相同的测量,如果能证明趋势变成了零,就能用数字来汇报「已经修好了」。对于需要等待数天才能复现的长期运行问题,布置好这套测量本身,才是整个调查的核心所在。
8. 携带进入生产 PC 与运维注意事项 ── Sysinternals Live・EULA・离线符号
Sysinternals 工具都是无需安装的独立可执行文件,把 ZIP 用 U 盘带过去就能直接运行。首次启动时会弹出 EULA 同意对话框,无人值守执行或脚本执行时,用 /accepteula 开关明确表示同意。如果能联网,借助名为Sysinternals Live 的服务,无需下载,直接通过 https://live.sysinternals.com/procexp.exe 这样的 URL,或 \\live.sysinternals.com\tools\<工具名> 这样的 UNC 路径就能直接运行。7 这是「现在就想在这台机器上看一眼」时最快捷的途径。
在离线的设备 PC 上会卡住的是符号(第 4 章的堆栈显示用不了)。应对方式有两种:(1)在能联网的开发机上,针对相同的操作系统构建版本先解析出符号,连同本地缓存(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 | 属性 → 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 来获取。
- 能按名称搜索到的,只有具名对象。对于像文件这种有名称的东西是最强的,但无名的事件和互斥体搜索不到。如果从「用 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 还能直接运行。离线环境则靠事先缓存符号或带回转储来弥补。
相关文章
- Process Monitor(ProcMon)实战指南 —— 10 分钟定位「配置未生效」「ACCESS DENIED」问题
- 如何替换正在使用中的 exe/DLL ── Restart Manager 与自动更新的「文件被占用」问题
- 工业相机长期运行崩溃调查 - handle 泄漏篇
- 工业相机控制应用运行一个月后突然崩溃时(下篇) - 什么是 Application Verifier 与异常路径测试基础设施的做法
- Windows应用的crash dump收集入门 - 先搞清楚 WER / ProcDump / WinDbg怎么分工
- 用 WinDbg + SOS 解读崩溃转储 ── 采集之后的实务分析入门
- 用 PerfView 与 dotnet-trace 定位“变慢”的原因 ── .NET 性能排查实务入门
相关咨询领域
合同会社小村软件承接因长期运行而性能下滑的业务应用・设备联动应用的泄漏排查,挂起・「文件正在使用」等现场故障的原因分析,以及证据采集流程的完善与防止复发的改造。
参考链接
-
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
-
Microsoft Learn, Handle - Sysinternals。关于 handle.exe 是一款显示已打开句柄信息的命令行工具,需要管理员权限,支持按名称部分匹配搜索,-a 表示对全部类型的句柄进行操作,-p 用于按进程筛选,-s 用于按类型汇总句柄数,-c 可以关闭句柄但官方警告「可能导致应用程序或系统变得不稳定」等内容。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Microsoft Public Symbol Server。关于微软公共符号服务器的符号路径可以按 srv本地缓存https://msdl.microsoft.com/download/symbols 的格式指定,下游存储(本地缓存)只会保存曾经访问过的符号,第二次起会从本地读取等内容。 ↩ ↩2
-
Microsoft Learn, GDI Objects。关于 GDI 句柄存在每个会话理论上限 65,536 个,存在按进程划分的默认上限,可以通过注册表的 GDIProcessHandleQuota(256~65,536 范围)进行更改等内容。 ↩ ↩2
-
Microsoft Learn, VMMap - Sysinternals。关于 VMMap 是一款进程虚拟内存・物理内存分析工具,可以显示已提交虚拟内存按类型划分的构成,以及各类型所分配的物理内存(工作集),支持筛选与刷新(快照)功能,支持数据导出与通过命令行选项实现脚本化等内容。 ↩ ↩2 ↩3
-
Microsoft Learn, RAMMap - Sysinternals。关于 RAMMap 是一款分析整个操作系统物理内存使用情况的工具,通过 Use Counts(按类型汇总)、Processes(进程的工作集)、File Summary(RAM 中的文件数据)等选项卡显示用途,支持快照的保存与加载等内容。 ↩ ↩2 ↩3
-
Microsoft Learn, Sysinternals。关于 Sysinternals Live 是一项无需下载即可运行工具的服务,可以通过 live.sysinternals.com/<工具名> 的 URL 或 \\live.sysinternals.com\tools\<工具名> 的路径直接运行,可以在 live.sysinternals.com 上浏览工具列表等内容。工具名>工具名> ↩ ↩2
-
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
-
Microsoft Learn, Install WinDbg。关于 WinDbg 可用于崩溃转储的分析以及用户模式・内核模式的调试,安装方式包括直接运行安装程序・通过 Microsoft Store・使用
winget install Microsoft.WinDbg,旧版 WinDbg(classic)包含在 Debugging Tools for Windows 中等内容。 ↩ -
Microsoft Learn, for。关于在批处理文件内要写成
%%variable,直接在命令提示符中执行时要写成%variable,以及for /f可以解析命令输出等内容。 ↩
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
Windows的「内存使用量」究竟表示什么 ── 正确解读 Working Set・Private Bytes・Commit・页面文件
任务管理器中的内存、Working Set、Private Bytes、Commit并不是同一个值。本文讲解Windows虚拟内存与物理内存的关系、页面文件的作用,以及在排查内存不足或泄漏时应该关注的指标。
多线程实务最佳实践 C 语言篇 ── 以 Win32 API 的方式安全编写
C 语言 × Win32 的多线程有其定式:用 _beginthreadex 创建线程、SRW 锁与条件变量、Interlocked、以停止事件 + WaitForMultipleObjects 设计停止流程。本文还将梳理 TerminateThread 的危险性与 Dll...
多线程实务最佳实践 C++ 篇 ── 用 RAII 和 jthread 从结构上消除事故
C++ 的多线程是数据竞争会变成未定义行为的世界。本文梳理 std::thread 析构函数的陷阱、jthread 与 stop_token 的停止设计、scoped_lock 的死锁规避、atomic 的正确定位,直至与 Win32 同步 API 的使用区分。
卷影复制服务(VSS)的原理与实务 ── 使用中文件为何能够备份
使用中的文件明明会因共享冲突而无法复制,备份软件为什么却能正常备份?本文将解说卷影复制服务(VSS)中请求者・编写器・提供程序的角色分工、写时复制的原理、vssadmin 的实务操作,以及差异区域的陷阱。
Windows I/O 的深层(第6回·最终回) ── 过滤器驱动程序与迷你过滤器:Procmon 与病毒扫描为何能够介入 I/O
本文是通过图解讲解 Windows 过滤器驱动程序与迷你过滤器的系列最终回。整理过滤器管理器与高度、pre/post 回调、Procmon 与杀毒软件能够检查全部 I/O 的机制,直至「唯独那个环境很慢」的排查步骤。
相关主题
与本文相近的主题页面。以本文为起点,可以进一步了解相关服务和其他文章。
Windows 技术主题
汇整 KomuraSoft LLC 关于 Windows 开发、故障调查与既有资产活用文章的主题中心。
故障调查 & 长期运行故障
整理间歇性故障、通信诊断、长期运行崩溃、失败路径测试基础的主题页面。
与本主题相关的服务
本文与以下服务页面相关联,欢迎从最接近的入口查看。
Windows 应用程序开发
支持包含常驻处理、设备联动、运行日志与可维护结构的 Windows 桌面应用程序。
故障调查 & 根本原因分析
调查难以复现的故障、长时间运行后的问题、内存泄漏、通信停滞等棘手的生产环境问题。
常见问题
汇总了咨询这一主题时常见的问题。
- 既然已经有任务管理器,为什么还要特意使用 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 或互操作代码都是嫌疑对象。如果是 Managed Heap 在增长,那就是 .NET 托管堆的问题,需要进入 GC 无法回收的引用调查(如事件处理程序解除遗漏等)。如果是 Private Data(VirtualAlloc 直接分配)在增长,就要怀疑分配图像缓冲区之类大块内存的代码或 SDK。先完成这一步切分,再分别进入专用工具(WinDbg、PerfView 等),调查才不会迷失方向。
- 在离线的设备 PC 上,Threads 选项卡的堆栈变成了一堆地址,读不懂。该怎么办?
- 这是因为无法下载符号(PDB),应对方式只有两种:「带入符号」或「带回转储」。带入符号的做法是,在能联网的开发机上针对相同的操作系统构建版本・相同的应用构成解析一次符号,把本地缓存文件夹(例如 C:\Symbols)连同符号一起复制到设备 PC,并把符号路径只指向这个本地文件夹。带回转储的做法是,在挂起期间采集转储,带回开发机用 WinDbg 分析,这种方式更加可靠,想要准确读取托管应用的堆栈时也更适合这种方法。前提是自家应用的 PDB 必须按每次构建妥善保存下来。
- 把 Process Explorer 或 VMMap 带入生产 PC・设备 PC 有问题吗?
- 两者都是无需安装的独立可执行文件,不涉及注册表注册或服务常驻,因此携带的门槛很低。首次启动时需要同意 EULA,从脚本中使用时可以用 /accepteula 开关明确表示同意。如果网络可用,还能通过 Sysinternals Live(https://live.sysinternals.com)不下载而直接运行。不过,观察操作本身风险虽低,但进程的 Kill/Suspend、线程的 Kill、强制关闭句柄这类「介入」操作却直接关系到事故,因此在生产环境中请只做观察和记录,并纳入常规的作业审批流程后再实施。