句柄泄露實例分析


句柄泄露實例分析

在上篇文章.NET對象與Windows句柄(二):句柄分類和.NET句柄泄露的例子中,我們有一個句柄泄露的例子。例子中多次創建和Dispose了DataReceiver和DataAnalyzer對象,但由於忘記調用DataAnalyzer的Stop方法,導致產生句柄泄露。本文假定我們已經發現了泄露現象但還不知道原因,討論如何在這種情況下分析問題。

一、發現問題

在程序運行約一個小時以后,通過任務管理器發現句柄數超過5000,線程數也超過1000。對於一段只需要並行接收和分析數據的簡易代碼來說,這顯然太不正常了,我們可以判斷程序已經產生了泄露。

 

通過任務管理器可以非常方便的查看程序實時的資源占用情況,但無法了解到歷史數據和趨勢。程序是一開始就需要分配和使用這么多資源,還是長時間運行的結果?如果是后者,那么是運行過程中平穩持續的增長,還是在某個時間節點之后的突然增長?弄清楚這些問題是必要的,我們可以借此初步判斷出內存泄露是與用戶的特定操作相關,或者與特定時間點上產生的事件相關;是跟程序的初始化有關,還是跟某些從始至終運行的后台任務相關。

性能監視器可以很直觀的顯示這一趨勢,其中內置了很多有用的計數器,我們可以從圖形化界面中觀察這些計數器值的變化規律,了解系統和進程的運行狀況。使用Win + R組合鍵打開“運行”窗口,輸入perfmon打開性能監視器。點擊綠色加號按鈕打開“添加計數器”對話框,選擇Process中的Handle Count和Thread Count,然后選擇LeakExample進程作為實例,添加這兩個計數器。

接下來觀察這些數值的變化。在這期間,我們像往常一樣的使用程序,可以重復進行一些可能造成內存泄露的操作。在運行過一段時間后,得到了如下的圖表。句柄數和線程數在持續的增長,很容易猜測到跟Timer有關,因為Timer定期觸發,並且每次觸發都需要使用線程。即便如此,仍然需要確切的定位究竟是什么對象產生了泄露,因為實際的項目中可能用到的Timer或者后台線程的代碼遠遠不止一兩處。

 

二、分析運行中的進程

首先應該找出5000多個句柄究竟代表什么對象。利用Process Explorer查看該進程,在下方面板中檢查句柄列表,發現有大量的Event句柄和Thread句柄,更進一步的,我們想知道到底有多少Event和Thread。

 

在這個列表中難以看出各種句柄的數量。可以按下Ctrl+A組合鍵,將Process Explorer中的進程列表和選中進程的句柄列表保存為文本文件,而后利用你所習慣使用的文本查看工具統計其中特定句柄的數量,我們這里使用Chrome瀏覽器的搜索功能看到約有4063個Event句柄和1008個Thread句柄。

 

到這里,我們有一個大致的印象,即泄露的對象是Event和Thread,其中Event占大多數。下一步需要找出是誰創建出了這些對象,可以使用Windbg跟蹤對象的創建。Windbg是非常方便的Windows調試工具,可以利用強大的SOS擴展命令診斷.NET程序中的各種問題,最新的Windbg(截止2016年4月)可以從MSDN的Download the WDK, WinDbg, and associated tools頁面下載,點擊頁面上的Get Debugging Tools for Windows (WinDbg)鏈接即可。

將Windbg附加到LeakExample.exe進程,而后使用!handle和!htrace命令對進程句柄進行分析。!handle命令可以列出進程內所有句柄,也可以查看特定句柄的信息,而!htrace顯示句柄的堆棧跟蹤。我們先使用!htrace -enable啟用句柄跟蹤,然后讓進程繼續運行幾分鍾時間,再中斷程序的執行,用!htrace -diff查看自上次快照以來新打開的句柄。由於命令輸出過長,一些不重要的信息被隱去用省略號代替。

0:482> !htrace -enable

Handle tracing enabled.

Handle tracing information snapshot successfully taken.

0:482> g

(1988.2f3c): Break instruction exception - code 80000003 (first chance)

eax=7fbc0000 ebx=00000000 ecx=00000000 edx=779fd23d esi=00000000 edi=00000000

eip=77993540 esp=5a75ff28 ebp=5a75ff54 iopl=0         nv up ei pl zr na pe nc

cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246

ntdll!DbgBreakPoint:

77993540 cc              int     3

0:015> !htrace -diff

Handle tracing information snapshot successfully taken.

0x6 new stack traces since the previous snapshot.

Ignoring handles that were already closed...

Outstanding handles opened since the previous snapshot:

--------------------------------------

Handle = 0x00000b68 - OPEN

Thread ID = 0x00002a68, Process ID = 0x00001988

 

0x779a4b7c: ntdll!ZwCreateThreadEx+0x0000000c

0x75d3bc5d: KERNELBASE!CreateRemoteThreadEx+0x00000161

0x7643281d: KERNEL32!CreateThreadStub+0x00000020

0x6c54b51f: clr!Thread::CreateNewOSThread+0x0000009b

0x6c54b358: clr!Thread::CreateNewThread+0x000000a8

0x6c54b8ad: clr!ThreadpoolMgr::CreateUnimpersonatedThread+0x00000275

0x6c54b9fc: clr!ThreadpoolMgr::MaybeAddWorkingWorker+0x00000129

0x6c53f298: clr!ManagedPerAppDomainTPCount::SetAppDomainRequestsActive+0x0000002f

--------------------------------------

Handle = 0x00000b64 - OPEN

Thread ID = 0x00002a68, Process ID = 0x00001988

 

0x779a49fc: ntdll!ZwCreateEvent+0x0000000c

0x75d376a0: KERNELBASE!CreateEventExW+0x0000006e

0x75d376f0: KERNELBASE!CreateEventW+0x00000027

0x6c54a106: clr!CLREventBase::CreateManualEvent+0x00000036

0x6c54a84f: clr!Thread::AllocHandles+0x00000064

0x6c54b4f4: clr!Thread::CreateNewOSThread+0x00000074

0x6c54b358: clr!Thread::CreateNewThread+0x000000a8

0x6c54b8ad: clr!ThreadpoolMgr::CreateUnimpersonatedThread+0x00000275

0x6c54b9fc: clr!ThreadpoolMgr::MaybeAddWorkingWorker+0x00000129

0x6c53f298: clr!ManagedPerAppDomainTPCount::SetAppDomainRequestsActive+0x0000002f

0x6ae49bd3: mscorlib_ni+0x00389bd3

0x6adcd38c: mscorlib_ni+0x0030d38c

--------------------------------------

Handle = 0x00000b60 - OPEN

Thread ID = 0x00002a68, Process ID = 0x00001988

 

0x779a49fc: ntdll!ZwCreateEvent+0x0000000c

… …

--------------------------------------

Handle = 0x00000b70 - OPEN

Thread ID = 0x00002a68, Process ID = 0x00001988

 

0x779a49fc: ntdll!ZwCreateEvent+0x0000000c

… …

--------------------------------------

Handle = 0x00000b54 - OPEN

Thread ID = 0x00002a68, Process ID = 0x00001988

 

0x779a49fc: ntdll!ZwCreateEvent+0x0000000c

… …

--------------------------------------

Handle = 0x00000b50 - OPEN

Thread ID = 0x000011f8, Process ID = 0x00001988

 

0x779a49fc: ntdll!ZwCreateEvent+0x0000000c

… …

--------------------------------------

Displayed 0x6 stack traces for outstanding handles opened since the previous snapshot.

可以看到,在兩次!htrace命令之間有6個handle被打開,由調用堆棧可知其中有1個Thread對象和5個Event對象,並且在第1個Thread對象之后的4個Event都屬於該線程。如果重復!htrace -diff多次,可以發現一個規律,即每個Thread對象被創建之后,緊接着就會有4個Event對象在同一線程中被打開,說明在本例中泄露的根源在於Thread對象,這也解釋了為什么Event句柄數大致是Thread的4倍。實際上每個線程在創建的時候的確會創建4個Manual Event,從上面句柄打開時的調用堆棧也能看出,clr!Thread::CreateNewOSThread方法除了創建Thread對象,也會創建幾個Manual Reset Event用於控制線程的掛起和恢復。

查看Event和Thread句柄的詳細信息,下面的輸出顯示了Thread句柄所指向的線程Id,以及其后的Event句柄信息。

0:015> !handle 0x00000b68 f

Handle b68

  Type            Thread

  Attributes         0

  GrantedAccess    0x1fffff:

         Delete,ReadControl,WriteDac,WriteOwner,Synch

         Terminate,Suspend,Alert,GetContext,SetContext,SetInfo,QueryInfo,SetToken,Impersonate,DirectImpersonate

  HandleCount     4

  PointerCount      6

  Name                   <none>

  Object Specific Information

    Thread Id   1988.261c

    Priority    10

    Base Priority 0

    Start Address 6c54a086 clr!Thread::intermediateThreadProc

0:015> !handle 0x00000b64 f

Handle b64

  Type            Event

  Attributes         0

  GrantedAccess    0x1f0003:

         Delete,ReadControl,WriteDac,WriteOwner,Synch

         QueryState,ModifyState

  HandleCount     2

  PointerCount      3

  Name                   <none>

  Object Specific Information

    Event Type Manual Reset

    Event is Set

接下來查看這個新啟動的線程在執行什么代碼,這個信息將幫助我們找到是哪里的代碼創建了該線程。我們需要加載SOS擴展,並利用上面輸出的Thread Id信息。

0:015> .loadby sos clr

0:015> !threads

ThreadCount:      323

UnstartedThread:  0

BackgroundThread: 266

PendingThread:    0

DeadThread:       56

Hosted Runtime:   no

                                                                         Lock 

       ID OSID ThreadOBJ    State GC Mode     GC Alloc Context  Domain   Count Apt Exception

   0    1  fb8 005015e8     26020 Preemptive  4EEC2A44:00000000 004f9540 0     STA

   2    2  a20 0050e080     2b220 Preemptive  00000000:00000000 004f9540 0     MTA (Finalizer)

   8    5 14cc 00553c48   102a220 Preemptive  00000000:00000000 004f9540 0     MTA (Threadpool Worker)

 284  280  f34 1178fa50   3029220 Preemptive  00000000:00000000 004f9540 0     MTA (Threadpool Worker)

 286  283 1ff4 117bd278   3029220 Preemptive  00000000:00000000 004f9540 0     MTA (Threadpool Worker)

 761  764 229c 24cfc070   3029220 Preemptive  00000000:00000000 004f9540 0     MTA (Threadpool Worker)

 849  865 1bc8 490eb860   3029220 Preemptive  00000000:00000000 004f9540 0     MTA (Threadpool Worker)

XXXX  868    0 490e82f0   1039820 Preemptive  00000000:00000000 004f9540 0     Ukn (Threadpool Worker)

 900  900 1054 490edd58   3029220 Preemptive  00000000:00000000 004f9540 0     MTA (Threadpool Worker)

 898  901  654 490d9370   3029220 Preemptive  00000000:00000000 004f9540 0     MTA (Threadpool Worker)

 903  903  828 490d9e00   3029220 Preemptive  00000000:00000000 004f9540 0     MTA (Threadpool Worker)

XXXX  904    0 490ead30   1039820 Preemptive  00000000:00000000 004f9540 0     Ukn (Threadpool Worker)

XXXX 1004    0 11758b70   1039820 Preemptive  00000000:00000000 004f9540 0     MTA (Threadpool Worker)

  10 1005 2844 117590b8   3029220 Preemptive  00000000:00000000 004f9540 0     MTA (Threadpool Worker)

   7 1006  314 11759600   3029220 Preemptive  00000000:00000000 004f9540 0     MTA (Threadpool Worker)

… …

… …

316  804 2164 0054f960   3029220 Preemptive  00000000:00000000 004f9540 0     MTA (Threadpool Worker)

 318  803 1758 24a3e810   3029220 Preemptive  00000000:00000000 004f9540 0     MTA (Threadpool Worker)

 317  802 27bc 116e1540   3029220 Preemptive  00000000:00000000 004f9540 0     MTA (Threadpool Worker)

   5  801 261c 117152d0   3029220 Preemptive  4EEC0C44:00000000 004f9540 0     MTA (Threadpool Worker)

0:015> ~5s

eax=00000000 ebx=00000258 ecx=00000001 edx=4fa6bc17 esi=0465ee48 edi=00000000

eip=779a64f4 esp=0465ee04 ebp=0465ee6c iopl=0         nv up ei pl nz na pe nc

cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000206

ntdll!KiFastSystemCallRet:

779a64f4 c3              ret

0:005> !clrstack

OS Thread Id: 0x261c (5)

Child SP       IP Call Site

0465eef4 779a64f4 [HelperMethodFrame: 0465eef4] System.Threading.Thread.SleepInternal(Int32)

0465ef68 6ad83365 System.Threading.Thread.Sleep(Int32)

0465ef6c 001d04cd LeakExample.DataAnalyzer.DoAnalyze(System.Object) [D: \TimerLeak\TimerLeak\Form1.cs @ 88]

0465ef7c 6adede48 System.Threading.TimerQueueTimer.CallCallbackInContext(System.Object)

0465ef80 6adc2367 System.Threading.ExecutionContext.RunInternal(… …)

0465efec 6adc22a6 System.Threading.ExecutionContext.Run(… …)

0465f000 6adedd91 System.Threading.TimerQueueTimer.CallCallback()

0465f034 6adedc4c System.Threading.TimerQueueTimer.Fire()

0465f074 6ade11a5 System.Threading.TimerQueue.FireQueuedTimerCompletion(System.Object)

0465f078 6adcdd34 System.Threading.QueueUserWorkItemCallback.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem()

0465f08c 6adcd509 System.Threading.ThreadPoolWorkQueue.Dispatch()

0465f0dc 6adcd3a5 System.Threading._ThreadPoolWaitCallback.PerformWaitCallback()

0465f300 6c432652 [DebuggerU2MCatchHandlerFrame: 0465f300]

從調用堆棧可以看出,新線程是由Timer觸發的,回調函數是DoAnalyze,參照上篇文章中的代碼,得知它就是DataAnalyzer中的analyzeTimer。這本身沒有什么問題,但是檢查多個線程的調用堆棧,重復以上的步驟進行多次分析后,發現所有新增的線程都是由這個timer觸發的。Timer本身被設置為每秒觸發一次,而每次觸發的執行時間都小於一秒。出現大量的線程,說明timer對象本身產生了泄露,即進程中有大量的timer實例在運行,而程序設計的本意是進程中只存在一個analyzeTimer。到這里問題已經比較明顯了,往往已經可以從代碼審查中找出問題,即analyzeTimer沒有被Dispose。

三、小結

針對有句柄泄露的程序,本文描述了一種分析的思路。分析的對象是運行中的進程,因此這是一種動態分析,即我們可以在它運行的過程中,反復的重現問題,而后觀察新的泄露情況。實際的項目中,這個過程是尋找問題復現關鍵點的過程,也是反復猜測和證實,以及發現新線索的過程。可以進行動態分析實際上是比較幸運的,因為另一些情況下,問題發生之后很難再次重現,或者現場環境不允許我們進行反復的嘗試。這時我們需要快速的搜集環境數據,並打好內存轉儲Dump文件,事后進行靜態分析。下一篇文章,我們仍然用這個例子,探討如何進行Dump分析,並討論一點Timer的實現細節。

作者: 文禾


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM