用Windbg调试.NET程序的资源泄漏

2023-05-16

在产品环境中的一个Windows服务出现了异常情况。这是一个基于WCF的.NET程序,它向网络应用(Web Application)提供WCF服务,同时也调用其他WCF服务以完成任务。突然,它不能响应网络应用的WCF调用。在它的日志文件中,我发现如下异常记录:

System.Net.Sockets.SocketException: An operation on a socket could not be performed because the system lacked sufficient buffer space or because a queue was full.

该异常暗示进程耗尽了系统的socket资源。开发人员告诉我,以前他们曾经发现这个程序有句柄泄漏的情况,但是没能够修复,这次的问题很可能是句柄泄漏造成的。由于问题发生在产品环境,我没有权限进行现场调试(live debugging)。于是,我请运营团队的同事用Windbg生成该程序的内存转储文件(memory dump)。具体命令如下,其中wcf_service.exe是发生问题的程序。

c:\debuggers\windbg.exe -pn wcf_service.exe -c ".dump /ma /u C:\wcf_service.dmp;qd"

在获得内存转储文件后,我用Windbg打开该文件,并加载Windbg调试扩展项Psscor2。Psscor2是调试扩展项SOS的增强版,在原有调试命令的基础上,又增加了一批实用的命令,是.NET程序调试的利器。

首先,调用!handle,检查句柄实用情况。

0:025>!handle
 
27604 Handles
Type             Count
None             9
Event            360
Section          55
File            16439
Directory        2
Mutant           6
Semaphore        64
Key              44
Token           10539
Thread           51
IoCompletion     5
Timer            5
KeyedEvent       1
TpWorkerFactory  24

该程序竟然拥有1万6千多个文件句柄(File)、1万多个令牌句柄(Token),果然存在严重的句柄泄漏。这些句柄对应了操作系统的本地资源(native resource),对于.NET程序,它们被称为非托管资源(unmanaged resource)。大多数.NET程序不直接向操作系统申请、释放非托管资源,它们通过调用.NET Framework Library的类来完成相应的操作。这些类大多实现了特殊的终结函数(Finalize函数)和Dispose惯用法。Finalize函数供CLR的Finalizer线程调用,Dispose函数供程序员调用,它们都会释放非托管资源(技术细节请参考《Effective C#》条款18:实现标准Dispose模式)。

实现了Finalize函数的对象被称为可终结对象。CLR(Common Language Runtime)在创建可终结对象时候,会在一个特殊的全局队列中保持它们的引用,这个队列被称为终结队列(Finalization Queue)。

于是,调用!FinalizeQueue来查看Finalization Queue,看看进程中有多少可终结对象。

0:025> !FinalizeQueue

SyncBlocks to be cleaned up: 0
MTA Interfaces to be released: 0
STA Interfaces to be released: 0
----------------------------------
generation 0 has 56 finalizable objects (0000000020099428->00000000200995e8)
generation 1 has 36 finalizable objects (0000000020099308->0000000020099428)
generation 2 has 98971 finalizable objects (000000001ffd7e30->0000000020099308)
Ready for finalization 0 objects (00000000200995e8->00000000200995e8)
Statistics:
        MT    Count    TotalSize Class Name
0x000007fef8dea958        1           24 System.Threading.OverlappedDataCache
0x000007fef8dd90c0        1           32 System.Security.Cryptography.SafeProvHandle
0x000007fef820e5d8        1           32 Microsoft.Win32.SafeHandles.SafeProcessHandle

...

0x000007fef8dbf730       16        1,664 System.Threading.Thread
0x000007fef81f7280       12        2,208 System.Net.Sockets.OverlappedAsyncResult
0x000007fef8db7a90      151        4,832 System.WeakReference
0x000007fef8db7b30      125        8,000 System.Threading.ReaderWriterLock
0x000007fef81efea0      254       14,224 System.Net.AsyncRequestContext
0x000007fef89994d8      256       30,720 System.Threading.OverlappedData
0x000007fef8dfa148      500       36,000 System.Reflection.Emit.DynamicResolver
0x000007fef8dd9298  10,539      337,248 Microsoft.Win32.SafeHandles.SafeTokenHandle
0x000007fef81f8730   16,341      522,912 System.Net.SafeCloseSocket+InnerSafeCloseSocket
0x000007fef81f93a8   16,339      653,560 System.Net.SafeCloseSocket
0x000007fef81f2850   16,362      785,376 System.Net.SafeFreeCredential_SECURITY
0x000007fef820d7a0    5,288      888,384 System.Diagnostics.PerformanceCounter
0x000007fef81f2be0   16,333      914,648 System.Net.SafeDeleteContext_SECURITY
0x000007fef81f84c8  16,339    1,960,680 System.Net.Sockets.Socket
Total 99,063 objects, Total size: 6,169,424

由输出可知,该程序拥有16个线程对象(线程也是非托管资源)、1万多个SafeTokenHandle对象、1万6千多个Socket对象。联系!handle的输出可以推知:SafeTokenHandle对象对应于非托管的令牌句柄(该程序对WCF调用实施Windows认证,这些令牌应该是认证所需要的安全令牌),Socket对象对应于非托管的文件句柄。这些对象的拥有者没有及时调用它们的Dispose方法,Finalizer线程也没有调用其Finalize方法,导致非托管资源没有得到及时地释放,终于导致socket资源耗尽。

那么是谁拥有这些对象呢?调用!dumpheap命令,获得所有Socket对象的地址。

0:025> !dumpheap -mt0x000007fef81f84c8
Loading the heap objects into our cache.
         Address               MT     Size
0000000001200270000007fef81f84c8      120    2 System.Net.Sockets.Socket

...

00000000012401e8 000007fef81f84c8      120    2 System.Net.Sockets.Socket
00000000012544d8 000007fef81f84c8      120    2 System.Net.Sockets.Socket

对于其中一个Socket对象,调用!gcroot命令,看看它是否被栈变量或全局变量所引用。

0:025> !gcroot0000000001200270

DOMAIN(0000000000ABF100):HANDLE(Pinned):121790:Root:  00000000111c78e8(System.Object[])->
  00000000011f43e8(System.ServiceModel.ChannelFactoryRefCache`1[[IJobServiceServer, JobServiceClientProxy]])->
  000000000121b480(System.ServiceModel.ChannelFactoryRef`1[[IJobServiceServer, JobServiceClientProxy]])->
  00000000011f4578(System.ServiceModel.ChannelFactory`1[[IJobServiceServer, JobServiceClientProxy]])->
  00000000016f6710(System.ServiceModel.Channels.ServiceChannelFactory+ServiceChannelFactoryOverDuplexSession)->
  0000000001672ea0(System.ServiceModel.Channels.TcpChannelFactory`1[[System.ServiceModel.Channels.IDuplexSessionChannel, System.ServiceModel]])->
  0000000001515978(System.ServiceModel.Channels.TcpConnectionPoolRegistry+TcpConnectionPool)->
 
  ...
 
  00000000012097b8(System.ServiceModel.Channels.BufferedConnection)->
  0000000001209678(System.ServiceModel.Channels.SocketConnection)->
  0000000001200270(System.Net.Sockets.Socket)

这些对象被WCF的ChannelFactoryRefCache所引用。它是一个全局的缓存,保存了可用的WCF通道(channel)。这些通道所对应的WCF调用与JobService有关。JobService是一个WCF服务,出问题的程序要定时轮询该服务以获取计算结果。IJobServiceServer是JobService的客户端代理对象所实现的接口。于是用“IJobServiceServer”在源代码树上搜索,找到了代理对象的定义:

class JobServiceServerClient : System.ServiceModel.ClientBase<IJobServiceServer>, IJobServiceServer { ...

JobServiceServerClient继承了WCF提供的ClientBase, 拥有非托管的socket资源,并实现了Finalize函数和Dispose函数。于是,搜索调用JobServiceServerClient的代码。很快,在源代码树上发现如下代码:

private static void CheckJobStatus(object state)
{
    JobServerClient client = new JobServerClient ();
    ...

该函数被定时地调用,以轮询JobService的计算结果。不幸的是,每次调用都会产生非托管资源的泄露,积少成多以至于难以为继。当JobServiceServerClient的对象创建时,它会被加入ChannelFactoryRefCache。由于程序员忘记调用Dispose函数,该对象拥有的scoket资源没有被释放。忘记调用Dispose函数的另一个后果是,该对象没有从ChannelFactoryRefCache中移除。这使得该对象始终是可达对象(reachable object),垃圾回收器(Garbage Collector) 不会处理它,这导致Finalizer线程不会调用它的Finalize函数,使得socket资源始终得不到释放。关于垃圾回收和Finalizer线程的技术细节请参考《CLR via C#》(第3版,第21章)。

对于函数CheckJobStatus,正确的实作是利用C#的using语句,确保对象client在退出using作用域时,其Dispose函数被CLR调用。

private static void CheckJobStatus(object state)
{
    using(JobServerClient client = new JobServerClient ())
    {
    ...

实际上,所有涉及非托管资源管理的文献,几乎都“严厉要求”正确地实现Dispose模式,并尽可能地利用using语句来确保Dispose函数被及时调用。这次遇到的问题就是违反了这条基本的资源管理规则。

回顾这次调试过程,可获得如下小结。

1. 命令!handle可以查看本地资源(非托管资源)的句柄。

2. 命令!FinalizeQueue可以查看终结队列,对于调试非托管资源泄漏很有帮助。

3. 正确的实现Dispose模式并严格地使用using语句,可以避免非托管资源的泄露。

最后,介绍一个调试句柄泄漏的好工具ProceXp。以管理员权限启动该程序,选中目标进程,按下快捷键Ctrl+H(或点击View > Lower Pane View > Handles),可以在下方面板中看到该进程所拥有的全部句柄。此时,按下快捷键Ctrl + S(或点击File > Save),可以将目标进程及其句柄信息保存为文本文件,以供深入分析。


http://www.51testing.com/?uid-298785-action-viewspace-itemid-218741

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

用Windbg调试.NET程序的资源泄漏 的相关文章

随机推荐

  • 以CSDN为例解释尼尔森十大交互原则

    一 状态可见原则 用户在网页上的任何操作 xff0c 不论是单击 滚动还是按下键盘 xff0c 页面应即时给出反馈 即时 是指 xff0c 页面响应时间小于用户能忍受的等待时间 举例 xff1a CSDN上文章底部都会有一个 喜欢 按钮 x
  • 游戏化思维——核心驱动力

    游戏是一个令人着迷 xff0c 并且能够让人沉迷于此的东西 xff0c 而游戏之所以如此迷人 xff0c 不但是游戏的制作精良和剧情引人入胜 除此之外还有些其他原因 xff0c 激励人民玩游戏的原因是 xff1a 游戏能够触及到人性的核心驱
  • 从产品设计到用户设计

    从产品设计到用户设计 一说起产品设计 xff0c 人们往往想到两个方面 感官方面 功能方面 感官方面 xff1a 精心设计的产品能够给用户带来赏心悦目的感觉 xff0c 当然极大部分是属于触感方面 xff08 嗅觉和味觉因为局限问题无法在产
  • 为体验设计——使用第一

    产品设计和用户体验设计有什么不同呢 xff1f 每个产品都是以用户是人类为前提而设计出来的 xff0c 而产品的每一次使用 xff0c 都会产生相应的体验 用户体验设计并完全不等同于产品设计 但是对于一个简单的情况下 xff0c 创建一个良
  • 用户体验和网站

    用户体验对于所有的产品和服务来讲 xff0c 都是至关重要的 现在讨论一种特殊产品的用户体验 xff1a 网站 xff08 这里的 网站 一词包括以内容为主的网站产品和以交互为主的网站应用 xff09 在网站上 xff0c 用户体验比任何一
  • .net C# 堆 栈 垃圾回收 GC

    NET C NET C NET C NET C NET C NET C NET C 栈 堆 垃圾回收 GC 1 尽管在 NET framework下我们并不需要担心内存管理和垃圾回收 Garbage Collection xff0c 但是我
  • 值类型总是分配在栈上吗?

    不是 xff0c 比如下面三种情况 xff1a 1 引用类型内部的变量 xff0c 即使是值类型 xff0c 也会随同引用类型的实例一起被分配在堆上 2 对于值类型的数组 xff0c 由于数组是引用类型 xff0c 数组内的值类型元素 xf
  • .NET垃圾回收机制 转

    在 NET Framework中 内存中的资源 即所有二进制信息的集合 分为 34 托管资源 34 和 34 非托管资源 34 托管资源必须接受 NET Framework的CLR 通用语言运行时 的管理 诸如内存类型安全性检查 而非托管资
  • Spring Boot 升级所遇到的坑们s 1.5.x升级到2.1.x

    下面总结从Spring Boot 1 5 15 Release版本升级到2 1 1 Release版本所遇到的问题 xff0c 因为每个项目所依赖库的多少不同 xff0c 所以有些我列出的问题可能你的产品没有遇到 xff0c 或者你的问题我
  • A simple Binary Search Tree written in C# and the case at the bottom

    Introduction In Computer Science a binary tree is a hierarchical structure of nodes each node referencing at most to two
  • vim学习资源

    http www vimer cn http coolshell cn http vimcdoc sourceforge net doc quickfix html 就这两个资源用好了 xff0c 就足够了
  • asp.net 获取客户端IP地址

    private string GetClientIP string result 61 HttpContext Current Request ServerVariables 34 HTTP X FORWARDED FOR 34 if nu
  • log4net 使用示例 asp.net + winform

    log4net 是 apache org 在 log4j的基础上推出的针对 NET程序的开源的日志组件 log4net目前的最新版本是 1 2 10 xff0c log4net支持的日志保存方式 xff0c 可谓丰富之极 xff0c 包括
  • Log4net 配置写不同文件

    以下配置了二种写文件 xff0c 第一种根据日期写文件yyyyMMdd txt xff0c 第二种是写固定文件login txt 1 xff0c 下载Log4net组件 xff1a http logging apache org log4n
  • Log4Net使用详解(续)

    说明自从上次在2008年在博客上发表过有关log4net的用法介绍文章之后 xff08 网址 xff1a http blog csdn net zhoufoxcn archive 2008 03 26 2220533 aspx xff09
  • httpWebRequest 通过代理 连接网络

    via Proxy connect the website WebProxy myProxy 61 new WebProxy myProxy Address 61 new Uri 34 http XXXXXXX com 9000 34 my
  • 使用windbg排查一个内存溢出的问题

    发现有一个服务占用大量的内存 奇怪的是服务一开始的时候只占用100M左右内存 xff0c 随着时间推移越来越大 xff0c 最后导致服务器内存吃紧 这可以算是一种内存泄漏的问题 xff0c 之所以标题不说是内存泄漏 xff0c 最后就会知道
  • 用WinDbg排除“内存溢出”故障

    文章摘要 内存溢出有时像 魔鬼 一样缠绕着我们的程序 xff0c 用一般的方法不易驱除 主要难点是搜查 魔鬼 的藏身之处 这时 xff0c 我们可以请来 WinDbg xff08 Debugging Tools for Windows xf
  • windbg 的常用命令--强大!常用!

    如何手工抓取dump文件 在生产环境下进行故障诊断时 xff0c 为了不终止正在运行的服务或应用程序 xff0c 有两种方式可以对正在运行的服务或应用程序的进程进行分析和调试 首先一种比较直观简洁的方式就是用WinDbg等调试器直接atta
  • 用Windbg调试.NET程序的资源泄漏

    在产品环境中的一个Windows服务出现了异常情况 这是一个基于WCF的 NET程序 xff0c 它向网络应用 xff08 Web Application xff09 提供WCF服务 xff0c 同时也调用其他WCF服务以完成任务 突然 x