原文地址:What on earth caused my process to crash?
发布时间:Monday, November 28, 2005 8:27 AM
作  者:Tess  

 

你在事件查看器中看到w3wp.exe意外地停止了1000次,或者你的进程以一种未定义的方式神秘地退出了,可你不知道为什么。

当进程崩溃或退出时,一个特殊的事件将会被触发,这个事件叫做EPR(Exit Process)。因此借助调试器,如windbg.exe,可以把它附在进程上,等待EPR被抛出异常,做一个memory dump。在windows下安装好调试工具后,会得到叫做adplus的vbscript(http://support.microsoft.com/default.aspx?scid=kb;en-us;286350) ,它会自动运行,打印在整个进程生存期内发生的大多数异常的日志。

调试技巧!:在crash模式下打开一个dump,当程序崩溃时,它会在活动线程上自动定位崩溃处。若你切换线程到出错的线程,输入 ~ 将所有的线程列举出来,一个断点会标记有问题的线程。

若dump只显示在进程里有一个活动线程,并且该线程是主线程,那么该进程可能因外部因素被杀死,譬如健康监视器(health monitoring),低系统内存,IIS重启等等。

在后续的帖子里,我会更为深入地讨论一些情景。我是一个自始自终的人,因此我想以一些常见的情形开始本帖子。当你看到一个托管进程退出时,你会对在dump文件里该查看些什么心中有点数。

这是一些在支持中的最常见情形,并不按照特殊顺序排列:


堆栈溢出异常

当一个线程分配的堆栈内存用尽了,就会发生堆栈溢出。默认分配1MB,故堆栈调用可以调得相当深。在大多数情况下,堆栈溢出是因无穷递归引起的, 譬如,函数A调用函数B,函数B又调用函数A... 以至无穷,没有停止的条件。

异常处理应用程序块使用不恰当,这是常见的一个隐晦的无穷递归情形。想象一下这个情形:应用程序发生了一个异常,异常处理跟踪了该异常,并建立了一个日志文件。在登陆时遇到了一个这种类型的异常(禁止登陆),此时你会使用异常处理去处理它。在这个案例中,在处理该异常时,会产生一个无穷的递归循环,它抛出另一个异常,处理该异常时又抛出一个异常...此时你该明白了。在这儿想说明的是,不要再用异常处理去处理先前异常处理语句中产生的异常:)

运行"kb 2000(参阅本地堆栈)和"!clrstack"(从sos.dll 可以查看托管堆栈),可以跟踪到递归在哪里发生的,为什么发生的。


内存溢出异常

 发生内存溢出在大多数情况下是因设计问题引起的,太多的内存存储在缓存或Session中。如果使用恰当,缓存可以极大地提升性能,譬如,将经常使用的数据缓存,按需要的时间设置过期。相信我,在老的asp技术中,将对象存入Session中有时会出现问题。开发人员应仅将最必要的东西存入Session。但是,譬如将大的数据集存放在Session中,这反而有害于应用程序的性能,因为这将降低网站能够处理的并发用户数量。当内存使用率足够高时,最先要做的事情可能是花时间进行垃圾对象回收。通过使用缓存搜索数据,你可以避免从数据库中取得所需数据。

是否需要将数据存储在Session/缓存中,这没有一个固定的方法适合所有的情形。最好的办法是早期评估确定,确定应用程序有多少用户以及基于此分析出每个用户允许存储量是多少。 然后按照最大数量的用户做一下压力测试,确保没有问题。对session中存储和不存储对象都做一下压力测试,看看哪个更好,将会是更好的。不同的用户数得出的结果是不同的。

软件产品中的内存问题是非常难以修复的,因它常常需要做很多的重新设计。因此未雨绸缪将省下后期的很多工作。

测试技巧!:先运行 !dumpheap -type System.Web.Caching.Cache 得到cache的根源,然后在相应地址上使用!objsize ,查看cache存储了多少资源。(注释: InProc Session 模式也存储在Cache中)

关于为什么会发生内存溢出异常的更多深入的讨论,可以参看我先前的帖子。


COM 组件中未处理的异常

若应用程序调用了本地的COM组件,因COM组件中有未处理的异常,应用程序也会崩溃。例如,引用了某块内存,但它已经释放了。


本地堆损毁
 

 这和GC漏洞都是一些最烦人的问题。向一个非假定的地址写入时,将发生本地堆损毁。执行写错地址的代码时,不会得到错误提示信息,然而开发人员并不知试图写入的内存地址是错误的。换句话说,那儿早已经有“贼”了。错误写入处可能是堆,但更糟糕的是,可能写入的是存放代码指令处,因此先前指令会被重写,这样代码执行到中间就无法继续调用了。向缓冲区的边界(或其他类似存储区)写入时,这种情况发生最频繁。
 
阅读Geoff Gray's 的有关堆损毁的文章,有对此的介绍。因堆损毁而造成的崩溃,出问题处通常会在ntdll中的堆分配调用函数。需要一同运行GFlags或PageHeap,抓住这个“贼”,才能解决问题。然而,发生地址错误写入的情况很难捕获到,因为它的发生时机很任意且难以再现。


托管堆损毁

托管堆损毁是一种发生在托管堆上的堆损毁。这个问题也是很难被捕获。向托管堆上覆写一块不允许写入的内存时,将发生托管堆损毁。通常不会在托管代码中发生缓冲溢出。对于byte[]这样一个数组,若对其赋值超出了边界,将会发生一个IndexOutOfRange异常。托管堆损毁的一个最常见的原因是一段叫做PInvoke的函数代码传入了按某种条件排序的缓冲区,但缓冲区容量太小,PInvoked函数向缓冲区写操作时,超出了缓冲区的边界,写到了托管堆的下一个对象上。然后垃圾回收器工作,试图穿过该托管堆,此时进程就会崩溃了。

若在一个含有垃圾回收器功能的活动堆栈上发生崩溃,应查询代码中的PInvokes函数,查是否因缓冲区太小而发生穿越行为。


致命执行引擎异常

致命执行引擎异常非常罕见,发生致命执行引擎异常通常是一个bug。这意味着由于某些原因,代码执行进入了CLR中一些未预料到的代码段中。CLR会因这些不可靠事件进入而抛出一个致命执行引擎异常并且崩溃,因为它不能从该断点再恢复。它会作为发生致命执行引擎异常记录在事件日志中,列出的地址将是发生崩溃的准确地址。若发生致命执行引擎异常,找不到相关技术文章,联系技术支持,最好附上一个崩溃的dump,这样易于技术支持决解决该问题。


GC漏洞

 这也是非常罕见的。CLR的非托管部分有一个指针,它指向托管代码,但它“忘记”告诉垃圾回收器相关信息了。 因此垃圾回收器不知道该如何保存现场或者不知如何跟踪指令。上述的意思是:垃圾回收执行清理的时机有误,此时那个指针可指向任何一个地方,并引起了许多破坏。Yun Jin在这儿对此有一点点讨论:http://blogs.msdn.com/yunjin/archive/2004/02/08/69906.aspx