Process Explorer / Handle / VMMap 实战 ── 从此刻的状态追查挂起・泄漏・「文件正在使用」问题

· · Sysinternals, Process Explorer, VMMap, 句柄泄漏, 内存泄漏, 故障排查, Windows, 长期运行

「周一早上重启一下就好了」── 这是在设备联动应用的维护咨询中,我们反复听到的一句话。刚启动时运行流畅,可到了周四左右画面切换开始变得迟钝,周五傍晚甚至按下按钮几秒钟都没有反应。偶尔还会出现从未见过的「句柄无效」错误。而只要重启,就会像什么都没发生过一样恢复正常。到了这一步,运维方式就固定为「每周重启」,而根本原因谁也不知道,就这样过了好几年。

这类症状,无论把日志看多少遍都找不到原因。真正需要的是直接查看「此刻这一瞬间,该进程究竟抱着多少什么样的东西」。上一篇《Process Monitor(ProcMon)实战指南》讲的是记录操作时间序列的 ProcMon。这次作为其续篇、Sysinternals 实战第二弹,我们来梳理专门查看状态一侧的工具 ── Process ExplorerHandleVMMap ── 并围绕长期运行应用的三大症状「逐渐变卡」「文件删不掉」「挂起无响应」来展开说明。

本文的前提环境如下。

  • 操作系统 ── 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 个步骤。

  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 相同的理由,并不推荐在生产环境中使用。

这里先说明一下这种搜索方式的局限性:命中的只有具名对象。无名的事件(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 个步骤。

  1. 在上方栏中双击目标进程(或右键点击 → 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 是本地缓存。第二次起会从这里读取

这里有两个要点。首先,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.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 的环境,或者需要用脚本进行定点观测的场景,可以使用命令行版的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 个步骤。

  1. 以管理员身份运行 VMMap。启动后会立即弹出进程选择对话框,选中目标进程后点击 OK
  2. 查看上方的汇总表。行是上表中的Type(Image / Heap / Managed Heap / Private Data / Stack……),列是 SizeCommitted 等大小信息。把这一时刻的数值记下来,或者从 File 菜单导出留存是关键所在
  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 组合而成的设备应用中最常见的绕远路情形。

对于 32 位进程,碎片化同样是观察对象。Fragmentation View 能显示地址空间中空闲区域的分散情况,可以直观地看到「Free 总量有数百 MB,但最大连续空闲(Largest)只有几十 MB」这样的状态。「内存明明应该是够用的却 OutOfMemory」「唯独大块图像缓冲区分配失败」,其真相多半就在于此,这也能成为对策(64 位化、缓冲区复用设计)的依据资料。

当可疑的不是某个进程,而是整个操作系统的内存(每个进程看起来都不胖,却仍然内存不足)时,就切换到按选项卡展示整个物理内存用途的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 出现了增长,就用 VMMap 拆分构成(第 6 章)来代替第 3 步,Heap 增长就分流到原生调查,Managed Heap 增长就分流到 .NET 调查。
  5. 如果所有数字都持平却只是发生挂起,就在 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 还能直接运行。离线环境则靠事先缓存符号或带回转储来弥补。

相关文章

相关咨询领域

合同会社小村软件承接因长期运行而性能下滑的业务应用・设备联动应用的泄漏排查,挂起・「文件正在使用」等现场故障的原因分析,以及证据采集流程的完善与防止复发的改造。

参考链接

  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。关于微软公共符号服务器的符号路径可以按 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 是一款分析整个操作系统物理内存使用情况的工具,通过 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),还能查看线程堆栈,因此「文件删不掉」「挂起了」这类排查会与任务管理器完全不在一个层次上。只要添加句柄数或 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、强制关闭句柄这类「介入」操作却直接关系到事故,因此在生产环境中请只做观察和记录,并纳入常规的作业审批流程后再实施。

作者简介

本文作者的个人简介页面。

Go Komura

小村软件有限公司 代表

以 Windows 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。

返回博客列表