異常鏈接致使的內存泄漏排查

異常鏈接致使的內存泄漏排查


背景

在生產環境中,部署在客戶的程序在運行了將近兩個月後發生了閃退。並且兩個服務器的程序前後都出現了閃退現象。經過排查windows日誌發現是OOM異常致使的閃退。本文記錄了該異常事件完整的排查過程與解決方案。react

在本篇文章中會涉及到如下技術知識點:使用windbg對dump文件進行內存分析、使用wireshark抓包分析、powershell腳本編寫、完成端口及重疊I/O原理等。linux

詳細流程

程序崩潰後,咱們要求客戶導出一個dump文件供咱們分析,並提供程序相關的運行日誌。同時查看了windows的相關日誌肯定了是因爲OOM(Out Of Memory)異常致使的。shell

使用windbg分析dump文件

啓動windbg打開dump文件編程

20190728143557.png

因爲咱們的程序是基於.net framework 3.5開發的,所以咱們使用SOS的相關擴展命令進行分析。須要在windbg中導入mscorwks
.loadby sos mscorwksc#

想對windbg進行深刻學習,能夠查看《使用WinDbg》講解的很是詳細。windows

經過!dumpheap -stat對內存佔用狀況進行彙總統計。api

!dumpheap -stat 
...
00007ff7ffbc0d50   536240     17159680 NetMQ.Core.Utils.Proactor+Item
00007ff7ffbca7f8   536242     17159744 NetMQ.Core.IOObject
00007ff7ffbcba70   536534     34338176 AsyncIO.Windows.AcceptExDelegate
00007ff7ffbcb7f0   536534     34338176 AsyncIO.Windows.ConnectExDelegate
00007ff7ffbcbdd8  1073068     60091808 AsyncIO.Windows.Overlapped
00007ff7ffbcb600   536534     90137712 AsyncIO.Windows.Socket
Total 3839215 objects

因爲咱們的程序底層網絡通信框架時基於NetMQ自研發的框架,從內存佔用狀況來看全部內存佔用都是NetMQ底層依賴的AsyncIO的對象。所以接下來就對具體的對象進行分析。緩存

再次經過!do 抽取幾個對象查看。發現全部的對象實際已經調用過了Dispose方法釋放內存。可是對象沒有被GC回收。安全

0:000> !do 00000000238b7b48 
Name: AsyncIO.Windows.Overlapped
MethodTable: 00007ff7ffbcbdd8
EEClass: 00007ff7ffbbea30
Size: 56(0x38) bytes
 (D:\FingardFC_V2.18.2\AsyncIO.dll)
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
00007ff85e5fa7f8  4000027       18        System.IntPtr  1 instance         22b0c060 m_address
00007ff85e5f3bc0  4000028       28 ...Services.GCHandle  1 instance 00000000238b7b70 m_handle
00007ff7ffbc3210  4000029       20         System.Int32  1 instance                0 <OperationType>k__BackingField
00007ff7ffbcb600  400002a        8 ...IO.Windows.Socket  0 instance 00000000238b7a68 <AsyncSocket>k__BackingField
00007ff85e5f6fc0  400002b       24       System.Boolean  1 instance                0 <InProgress>k__BackingField
00007ff85e5f6fc0  400002c       25       System.Boolean  1 instance                1 <Disposed>k__BackingField
00007ff85e5f76e0  400002d       10        System.Object  0 instance 00000000238b7df8 <State>k__BackingField
00007ff85e5ff060  4000022       58         System.Int32  1   static               40 Size
00007ff85e5ff060  4000023       5c         System.Int32  1   static                8 BytesTransferredOffset
00007ff85e5ff060  4000024       60         System.Int32  1   static               16 OffsetOffset
00007ff85e5ff060  4000025       64         System.Int32  1   static               24 EventOffset
00007ff85e5ff060  4000026       68         System.Int32  1   static               32 MangerOverlappedOffset
0:000> !do 00000000238acc50 
Name: AsyncIO.Windows.Overlapped
MethodTable: 00007ff7ffbcbdd8
EEClass: 00007ff7ffbbea30
Size: 56(0x38) bytes
 (D:\FingardFC_V2.18.2\AsyncIO.dll)
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
00007ff85e5fa7f8  4000027       18        System.IntPtr  1 instance         22b0ad70 m_address
00007ff85e5f3bc0  4000028       28 ...Services.GCHandle  1 instance 00000000238acc78 m_handle
00007ff7ffbc3210  4000029       20         System.Int32  1 instance                1 <OperationType>k__BackingField
00007ff7ffbcb600  400002a        8 ...IO.Windows.Socket  0 instance 00000000238acba8 <AsyncSocket>k__BackingField
00007ff85e5f6fc0  400002b       24       System.Boolean  1 instance                1 <InProgress>k__BackingField
00007ff85e5f6fc0  400002c       25       System.Boolean  1 instance                1 <Disposed>k__BackingField
00007ff85e5f76e0  400002d       10        System.Object  0 instance 00000000238acf38 <State>k__BackingField
00007ff85e5ff060  4000022       58         System.Int32  1   static               40 Size
00007ff85e5ff060  4000023       5c         System.Int32  1   static                8 BytesTransferredOffset
00007ff85e5ff060  4000024       60         System.Int32  1   static               16 OffsetOffset
00007ff85e5ff060  4000025       64         System.Int32  1   static               24 EventOffset
00007ff85e5ff060  4000026       68         System.Int32  1   static               32 MangerOverlappedOffset

查看終結隊列中的對象,能夠發現對象都在終結隊列中。

0:000> !finq -stat
Generation 0:
       Count      Total Size   Type
---------------------------------------------------------
           1             168   AsyncIO.Windows.Socket

1 object, 168 bytes

Generation 1:
       Count      Total Size   Type
---------------------------------------------------------
        1008          169344   AsyncIO.Windows.Socket
           2              48   System.Windows.Forms.VisualStyles.VisualStyleRenderer+ThemeHandle

1,010 objects, 169,392 bytes

Generation 2:
       Count      Total Size   Type
---------------------------------------------------------
           1             776   FC.Main.frmMain
           1             104   AsyncIO.Windows.CompletionPort
      535525        89968200   AsyncIO.Windows.Socket
...

查看垃圾回收器句柄的統計信息,存在大量的重疊資源對象未釋放。

0:000> !gchandles
GC Handle Statistics:
Strong Handles: 520519
Pinned Handles: 84
Async Pinned Handles: 0
Ref Count Handles: 0
Weak Long Handles: 43
Weak Short Handles: 116
Other Handles: 0
Statistics:
              MT    Count    TotalSize Class Name
...
00007ff85e5e5be0      510      2435216 System.Object[]
00007ff7ffbcbdd8   511752     28658112 AsyncIO.Windows.Overlapped
Total 520762 objects

我使用的NetMQ版本是4.0.0.1,使用的AsyncIO版本是0.1.26.0

AsyncIO重疊資源釋放代碼以下

public void Dispose()
{
    if (!InProgress)
    {
        Free();
    }

    Disposed = true;            
}
private void Free()
{
    Marshal.FreeHGlobal(m_address);

    if (m_handle.IsAllocated)
    {
        m_handle.Free();
    }        
}

InProgress=false纔會釋放相關的非託管資源句柄。在對InProgress查找全部引用。發現只有一個地方對其賦值爲ture

public void StartOperation(OperationType operationType)
{
    InProgress = true;
    Success = false;
    OperationType = operationType;
}

再對StartOperation查找引用,一共有4個地方調用。

20190728151917.png

能夠發現該字段適用於表示重疊I/O是否正在處理。在若是重疊I/O正在處理,則不釋放相關的資源,具體緣由後面講到重疊I/O時會進行說明。

使用wireshark抓包分析

與此同時,咱們對程序日誌也進行了分析。發現咱們的程序接收到了大量的Http請求。

因爲咱們和客戶接口是經過TCP協議傳輸,而非HTTP協議,所以理論上不該該會有HTTP請求發到咱們程序端口上。又由於咱們程序有接收超時機制,即便有咱們沒法解析的無效請求,超過了超時時間咱們也會將對應的資源釋放。並且從dump文件來看也沒有咱們未釋放的資源對象。

爲了搞清楚究竟是什麼請求發到咱們程序上,所以要求客戶在服務器抓包。咱們對抓包文件進行分析。發現抓到了大量的異常鏈接,每5秒會有2個。
20190728153333.png

而後我經過計算未釋放對象的數量基本與接收到這個包數量吻合。所以初步判定內存泄漏是因爲該包引發的。這個包應該是一個服務監控程序發的,每五秒發一次,有2個地址在往咱們程序發。

完成端口和重疊IO

肯定了初步的緣由,接下來就須要進行源碼分析,排查問題點。因爲AsyncIO使用的是基於完成端口的重疊I/O,所以有必要先對重疊I/O和完成端口進行簡單介紹。

重疊I/O

通常來講咱們開發程序須要進行I/O讀寫使用同步I/O與異步I/O兩種方式。
同步I/O是大多數開發人員習慣的使用方式,從文件或網絡中讀取數據,線程會被掛起,等待數據讀取完畢後繼續執行。異步I/O則不會等待I/O調用完成,而是當即發返回,操做系統完成咱們的I/O請求後會進行通知。

在Windows下的異步I/O咱們也能夠稱之爲重疊(overlapped)I/O。重疊的意思是執行I/O請求的時間與線程執行其餘任務的時間是重疊的,即執行真正I/O請求的時候,咱們的工做線程能夠執行其餘請求,而不會阻塞等待I/O請求執行完畢。

完成端口

實際在windows上一共支持四種接收完成通知的方式。分別爲觸發設備內核對象、觸發時間內核對象、可提醒I/O以及I/O完成端口。其餘三種有或多或少的缺點,而完成端口則是在Windows上性能最佳的接收I/O完成通知的方式。

想要詳細瞭解四種接收完成通知方式的同窗能夠查閱《Windows via C/C++ 第五版》(也被稱爲Windows核心編程第五版)的第十章-同步設備I/O與異步設備I/O的10.5節。

I/O完成端口的設計理論依據是併發編程的線程數必須有一個上限,即最佳併發線程數爲CPU的邏輯線程數。I/O完成端口充分的發揮了併發編程的優點的同時又避免了線程上下文切換帶來的性能損失。

在大多數x86和x64的多處理器,線程上下文切換時間間隔大約爲15ms。
CPU每過大約15ms將CPU寄存器當前的線程上下文存回到該線程的上下文,而後該線程不在運行。而後系統檢查剩下的可調度線程內核對象,選擇一個線程的內核對象,將其上下文載入導CPU寄存器中。
關於Windows線程相關內容能夠查閱《Windows via C/C++ 第五版》的第七章

Reactor模型與Proactor模型

目前常提到的I/O多路複用主要包含兩種線程模型,Reactor模型和Procator模型。

Reactor模型是同步非阻塞線程模型。在設備可讀寫時,系統會進行通知,而後咱們從設備讀寫數據。
Proactor模型時異步線程模型。在讀寫完畢時,系統會進行通知,而後咱們就能夠處理讀寫完畢後的事件。

在windows的完成端口就是系統層面的異步I/O模型。而linux僅支持select、epoll、kqueue等同步非阻塞I/O模型。

關於Reactor和Proactor的具體處理邏輯能夠看Reactor與Proactor的概念如何深入理解reactor和proactor?兩篇文章。

完成端口處理邏輯

爲了更好的分析問題,還須要清楚重疊I/O和完成端口的完整處理流程。
I/O設備包含了如文件、目錄、套接字、邏輯/物理磁盤驅動器等等。因爲windows下異步I/O設計的通用性,因此I/O設備都能充分利用重疊I/O和完成端口提高性能。因爲目前咱們的場景是使用套接字(socket)進行I/O讀寫,所以後面直接使用套接字來表示設備,實際其餘I/O的處理流程也是同樣的。

建立完成端口。

在外面建立網絡監聽的時候,首先咱們須要建立一個完成端口,後續設備的通知都須要經過該完成端口進行通知。
建立完成端口的時候能夠指定容許併發執行線程的數量,在應用程序初始化時,就會建立線程池,並初始化線程,以便提升應用程序的性能。

註冊套接字

相比同步I/O,使用完成端口須要咱們先將設備註冊到完成端口。
首先咱們建立一個用於監聽的套接字,而後將其綁定到完成端口上。該操做會將套接字添加到完成端口的設備列表中,這樣當該套接字的I/O請求處理完成時,I/O線程就會將該套接字的完成事件加入到完成端口的I/O完成隊列中。
註冊完以後就能夠綁定並開始監聽端口了。

接收客戶端請求

同步I/O是在設備可讀寫的時候會通知咱們,而後在建立一個套接字用於處理客戶端I/O讀寫。
異步I/O則須要先建立一個套接字,而後將其綁定到完成端口上,當咱們接收到新的客戶端請求時,實際的I/O操做已經完成。
因爲建立套接字的開銷很是大,所以異步I/O提早準備好一個套接字相比同步I/O接收到請求之後再建立,性能會更好。

處理I/O請求

讀請求

同步I/O能夠斷的查看設備是否可讀。當設備可讀時,再從設備緩衝區讀取數據到內存中。
異步I/O首先須要初始化一個內存空間用於接收數據,而後調用重疊讀操做,當系統接收到數據時,I/O線程將數據直接寫入到咱們提供的內存地址中,完成後就會將I/O請求加入I/O完成隊列,咱們就能夠接收到I/O讀完成通知。當咱們收到通知時,若是沒有發生錯誤,實際數據已經從系統緩衝取加載到內存了。

寫請求

同步I/O在發送數據的時候同步的將數據寫入到緩衝區。這個過程咱們的線程實際是阻塞的。
異步I/O在發送數據的時候,先發起重疊寫操做,當數據寫入到緩衝區後,就會將I/O請求加入到I/O完成隊列。咱們就能夠收到I/O寫完成的通知。因此實際數據寫入緩衝區時咱們的工做線程仍然能夠併發處理其餘事情。

根據WSK_SEND文檔描述,WSK子系統在經過套接字發送數據時不執行任何數據緩衝。所以,在實際發送全部數據以前,WSK子系統不會完成對WskSend函數的調用。根據我的對該描述的理解,異步I/O發生請求接收到完成通知時,數據應該已經成功發送到對端。若是有誰能有明確的結論,麻煩告知我一下。

問題排查

在簡單介紹了重疊I/O和完成端口後,回到問題排查中。因爲前面咱們已經發現全部內存泄漏點都是因爲重疊資源未釋放致使的,而實際咱們已經調用過Dipose釋放資源

首先來看下建立套接字、接收數據、發送數據和釋放套接字的時候分別作了什麼

建立套接字

public Socket(AddressFamily addressFamily, SocketType socketType, ProtocolType protocolType)
        : base(addressFamily, socketType, protocolType)
{
    m_disposed = false;

    m_inOverlapped = new Overlapped(this);
    m_outOverlapped = new Overlapped(this);

    m_sendWSABuffer = new WSABuffer();
    m_receiveWSABuffer = new WSABuffer();

    InitSocket();
    InitDynamicMethods();
}
public Overlapped(Windows.Socket asyncSocket)
{
    Disposed = false;
    InProgress = false;
    AsyncSocket = asyncSocket;
    m_address = Marshal.AllocHGlobal(Size);
    Marshal.WriteIntPtr(m_address, IntPtr.Zero);
    Marshal.WriteIntPtr(m_address,BytesTransferredOffset, IntPtr.Zero);
    Marshal.WriteInt64(m_address, OffsetOffset, 0);
    Marshal.WriteIntPtr(m_address, EventOffset, IntPtr.Zero);

    m_handle = GCHandle.Alloc(this, GCHandleType.Normal);

    Marshal.WriteIntPtr(m_address, MangerOverlappedOffset, GCHandle.ToIntPtr(m_handle));            
}
  1. 建立重疊資源。在建立重疊資源的時候,會經過GCHandle.Alloc分配句柄,防止託管對象被GC回收致使非託管資源被回收。只有調用Free才能被回收。
  2. 初始化輸入輸出對象WSABuffer。當發送或接收數據時會直接使用該對象地址,而不會發生內存複製。
  3. 初始化一個套接字對象
private void InitSocket()
{
    Handle = UnsafeMethods.WSASocket(AddressFamily, SocketType, ProtocolType,
        IntPtr.Zero, 0, SocketConstructorFlags.WSA_FLAG_OVERLAPPED);

    if (Handle == UnsafeMethods.INVALID_HANDLE_VALUE)
    {
        throw new SocketException();
    }
}

初始化接收擴展方法和鏈接的擴展方法

internal static class UnsafeMethods
{
    public static readonly Guid WSAID_CONNECTEX = new Guid("25a207b9-ddf3-4660-8ee9-76e58c74063e");
    public static readonly Guid WSAID_ACCEPT_EX = new Guid("b5367df1-cbac-11cf-95ca-00805f48a192");
    ...
}
private void InitDynamicMethods()
{
    m_connectEx =
        (ConnectExDelegate)LoadDynamicMethod<ConnectExDelegate>(UnsafeMethods.WSAID_CONNECTEX);

    m_acceptEx =
        (AcceptExDelegate)LoadDynamicMethod<AcceptExDelegate>(UnsafeMethods.WSAID_ACCEPT_EX);
}

異步接收套接字

public void AcceptInternal(AsyncSocket socket)
{
    if (m_acceptSocketBufferAddress == IntPtr.Zero)
    {
        m_acceptSocketBufferSize = (m_boundAddress.Size + 16) * 2;

        m_acceptSocketBufferAddress = Marshal.AllocHGlobal(m_acceptSocketBufferSize);
    }

    int bytesReceived;

    m_acceptSocket = socket as Windows.Socket;

    m_inOverlapped.StartOperation(OperationType.Accept);

    if (!m_acceptEx(Handle, m_acceptSocket.Handle, m_acceptSocketBufferAddress, 0,
            m_acceptSocketBufferSize / 2,
            m_acceptSocketBufferSize / 2, out bytesReceived, m_inOverlapped.Address))
    {
        var socketError = (SocketError)Marshal.GetLastWin32Error();

        if (socketError != SocketError.IOPending)
        {
            throw new SocketException((int)socketError);
        }                
    }
    else
    {                
        CompletionPort.PostCompletionStatus(m_inOverlapped.Address);
    }
}
  1. 首先初始化用於接收客戶套接字的地址。m_boundAddress是當前監聽的套接字對象。
    m_boundAddressm_boundAddress.Size則是根據IPV4仍是IPV6決定的,具體細節不作分析。經過Marshal.AllocHGlobal分配非託管內存,返回一個地址。
  2. 執行重疊操做異步接收客戶端鏈接。經過調用m_acceptEx異步接收客戶鏈接。前面提到異步I/O接收,先建立套接字用於接收,這樣真正到接收客戶端鏈接時就無需再建立套接字了。
  3. 判斷返回執行結果。重疊操做執行完畢須要調用GetLastWin32Error判斷操做是否執行成功。
    • 當返回SUCCESS時,表示I/O操做完成。若在讀取數據時,數據已經在緩存中,則系統不會將I/O請求添加到設備驅動程序的隊列,而是直接以同步的方式從高速緩存中的數據複製到咱們的緩存中,從而完成I/O操做。
    • 若返回爲ERROR_IO_PENDING時,則表示I/O請求已經被成功的加入到了設備驅動程序的隊列,會在晚些時候完成。
    • 若返回其餘值時,則表示I/O請求沒法被添加到設備驅動程序的隊列。

接收數據

public override void Receive(byte[] buffer, int offset, int count, SocketFlags flags)
{
    if (buffer == null)
        throw new ArgumentNullException("buffer");

    if (m_receivePinnedBuffer == null)
    {
        m_receivePinnedBuffer = new PinnedBuffer(buffer);
    }
    else if (m_receivePinnedBuffer.Buffer != buffer)
    {
        m_receivePinnedBuffer.Switch(buffer);
    }


    m_receiveWSABuffer.Pointer = new IntPtr(m_receivePinnedBuffer.Address + offset);
    m_receiveWSABuffer.Length = count;

    m_inOverlapped.StartOperation(OperationType.Receive);

    int bytesTransferred;
    SocketError socketError = UnsafeMethods.WSARecv(Handle, ref m_receiveWSABuffer, 1,
        out bytesTransferred, ref flags, m_inOverlapped.Address, IntPtr.Zero);

    if (socketError != SocketError.Success)
    {
        socketError = (SocketError)Marshal.GetLastWin32Error();

        if (socketError != SocketError.IOPending)
        {
            throw new SocketException((int)socketError);
        }
    }
}

接收時首先將接收數據轉換爲WSABuffer對象。因爲異步I/O請求完成以前,必定不能移動或銷燬所使用的數據緩存和重疊接口,所以咱們須要將數據緩存釘住,防止它被垃圾回收,且防止垃圾回收內存整理時對象被移動致使地址發生變化。

class PinnedBuffer : IDisposable
{
    private GCHandle m_handle;
    public PinnedBuffer(byte[] buffer)
    {
        SetBuffer(buffer);
    }

    public byte[] Buffer { get; private set; }
    public Int64 Address { get; private set; }

    public void Switch(byte[] buffer)
    {
        m_handle.Free();

        SetBuffer(buffer);
    }

    private void SetBuffer(byte[] buffer)
    {
        Buffer = buffer;
        m_handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
        Address = Marshal.UnsafeAddrOfPinnedArrayElement(Buffer, 0).ToInt64();
    }
    public void Dispose()
    {
        m_handle.Free();
        Buffer = null;
        Address = 0;
    }
}

因爲咱們傳遞的值數據緩存地址,所以異步I/O不會發生內存複製,提升了性能。
當標記了Pinned或Normal,GC都不會回收資源,可是標記爲Normal時因爲垃圾回收內存整理地址可能會變,而Pinned則表示該對象不要移動。這樣就保證了重疊操做不會發生錯誤。

所以在重疊操做處理的時候,咱們經過m_inOverlapped.StartOperation(OperationType.Receive);設置重疊對象的InProgress屬性爲true,表示重疊操做正在處理中。

發送數據

發送數據和接收數據相似,這裏不作具體說明。下面將與接收數據不一樣的代碼列出來。

public override void Send(byte[] buffer, int offset, int count, SocketFlags flags)
{
    ...
    m_sendWSABuffer.Pointer = new IntPtr(m_sendPinnedBuffer.Address + offset);
    m_sendWSABuffer.Length = count;

    m_outOverlapped.StartOperation(OperationType.Send);
    int bytesTransferred;
    SocketError socketError = UnsafeMethods.WSASend(Handle, ref m_sendWSABuffer, 1,
        out bytesTransferred, flags, m_outOverlapped.Address, IntPtr.Zero);
    ...
}

釋放套接字

當網絡傳輸完成時,須要釋放套接字,同時還須要釋放相關的非託管資源。

private void Dispose(bool disposing)
{
    if (!m_disposed)
    {
        m_disposed = true;                

        m_inOverlapped.Dispose();
        m_outOverlapped.Dispose();

        // for Windows XP
#if NETSTANDARD1_3
        UnsafeMethods.CancelIoEx(Handle, IntPtr.Zero);
#else
        if (Environment.OSVersion.Version.Major == 5)
            UnsafeMethods.CancelIo(Handle);
        else
            UnsafeMethods.CancelIoEx(Handle, IntPtr.Zero);
#endif

        int error = UnsafeMethods.closesocket(Handle);

        if (error != 0)
        {
            error = Marshal.GetLastWin32Error();
        }
        ...
        if (m_acceptSocket != null)  
            m_acceptSocket.Dispose();                    
    }
}

釋放套接字資源的時候首先須要釋放相關的重疊資源。前面已經看過釋放重疊資源的代碼,這裏爲了方便分析,再次列一下。

public void Dispose()
{
    if (!InProgress)
    {
        Free();
    }

    Disposed = true;            
}

private void Free()
{
    Marshal.FreeHGlobal(m_address);

    if (m_handle.IsAllocated)
    {
        m_handle.Free();
    }        
}
  1. 前面提到過,在重疊操做正在進行的時候,不能將數據緩存和重疊結構釋放掉,不然系統處理可能出現異常。假設發生了垃圾回收將資源釋放了,可是此時發生了I/O讀寫,可能該地址指向是其餘的對象,所以可能會形成內存溢出等問題。同時出現了該問題還很是難以排查緣由。
  2. 取消完成端口通知。
  3. 關閉套接句柄。

分析問題

前面詳細的介紹和分析了異步(重疊)I/O和完成端口的緣由,那麼接下來對內存泄露的具體緣由進行分析。咱們經過dump文件已經知道了套接字對象實際已經被釋放了。套接字對象和重疊資源對象造成了循環引用,可是GC是很是聰明的,可以識別這種狀況,仍然是能夠將其回收掉。可是爲何套接字對象和重疊資源仍是沒有被回收掉呢?

這是由於因爲咱們的重疊操做正在處理,所以InProgress設置成了true,可是因爲釋放重疊資源的時候重疊操做正在處理,所以咱們不能經過Free釋放重疊資源的句柄。而是要等重疊操做成後才能釋放。而以後就沒有在收到I/O完成通知。那麼分析如下沒有I/O完成通知的可能狀況有如下:

  1. 在調用重疊操做的時候,當時返回的結果就不是SUCCESSERROR_IO_PENDING,所以實際I/O操做並無加入到設備驅動隊列中,天然不會有I/O請求完成的通知。
  2. 在咱們釋放I/O資源的時候,經過調用了CancelIoEx function取消文件句柄的I/O完成端口。調用了取消操做會有如下三種狀況
    • I/O操做仍處理完成。當取消時,可能以前提交的I/O操做已經完成。
    • I/O操做已取消。此時經過GetLastError將會返回ERROR_OPERATION_ABORTED
    • 其餘錯誤。

      須要注意的是,若異步I/O操做已經待處理,此時取消操做將會進入到I/O完成隊列。所以若取消I/O操做後重疊資源能夠被安全釋放。

處理I/O完成操做事件的代碼以下

private void HandleCompletionStatus(out CompletionStatus completionStatus, IntPtr overlappedAddress, IntPtr completionKey, int bytesTransferred)
{
    ...
    var overlapped = Overlapped.CompleteOperation(overlappedAddress);
    ...
}

在處理完成事件時,會判斷當前重疊資源是否已經釋放,若已經釋放則將相關句柄釋放掉,此時就能夠被GC回收。

public static Overlapped CompleteOperation(IntPtr overlappedAddress)
{
    IntPtr managedOverlapped = Marshal.ReadIntPtr(overlappedAddress, MangerOverlappedOffset);

    GCHandle handle = GCHandle.FromIntPtr(managedOverlapped);

    Overlapped overlapped = (Overlapped) handle.Target;
    overlapped.Complete();
    if (overlapped.Disposed)
    {
        overlapped.Free();
        overlapped.Success = false;
    }
    else
    {
        overlapped.Success = Marshal.ReadIntPtr(overlapped.m_address).Equals(IntPtr.Zero);
    }

    return overlapped;          
}

確認問題

以接收數據爲例,能夠對問題的緣由進行確認。
當咱們調用重疊操做的時候。若重疊操做返回的結果是SUCCESSERROR_IO_PENDING之外的值,則重疊操做並無被真正的提交。就如咱們前面所將,重疊操做提交到設備驅動隊列時會返回ERROR_IO_PENDING,而以同步方式執行完成時則直接返回SUCCESS

修復問題

在發生和接收時判斷如下返回結果的若不是SUCCESSERROR_IO_PENDING,則經過m_outOverlapped.Complete();設置InProgress對象值爲true。這樣在釋放資源的時候就直接將重疊資源釋放掉。

public override void Send(byte[] buffer, int offset, int count, SocketFlags flags)
{
    ...
    m_outOverlapped.StartOperation(OperationType.Send);
    int bytesTransferred;
    SocketError socketError = UnsafeMethods.WSASend(Handle, ref m_sendWSABuffer, 1,
        out bytesTransferred, flags, m_outOverlapped.Address, IntPtr.Zero);

    if (socketError != SocketError.Success)
    {
        socketError = (SocketError)Marshal.GetLastWin32Error();

        if (socketError != SocketError.IOPending)
        {
            m_outOverlapped.Complete();
            throw new SocketException((int)socketError);
        }
    }
}

public override void Receive(byte[] buffer, int offset, int count, SocketFlags flags)
{
    ...
    m_inOverlapped.StartOperation(OperationType.Receive);

    int bytesTransferred;
    SocketError socketError = UnsafeMethods.WSARecv(Handle, ref m_receiveWSABuffer, 1,
        out bytesTransferred, ref flags, m_inOverlapped.Address, IntPtr.Zero);

    if (socketError != SocketError.Success)
    {
        socketError = (SocketError)Marshal.GetLastWin32Error();

        if (socketError != SocketError.IOPending)
        {
            m_outOverlapped.Complete();
            throw new SocketException((int)socketError);
        }
    }
}

重現及驗證

因爲這並非必現的,所以寫一個腳本發生大量的鏈接後客戶立刻重置的包進行重現及驗證是否解決。
RSTTEST.ps1內容以下,在建立了socket以後不要正常關閉,採用exit退出的方式,讓GC直接回收對象。

$endpoint = "127.0.0.1" 
$port =12345
$IP = [System.Net.Dns]::GetHostAddresses($EndPoint) 
$Address = [System.Net.IPAddress]::Parse($IP) 
$Socket = New-Object System.Net.Sockets.TCPClient($Address,$Port) 
exit

MUTIRSTTEST.ps1,經過調用屢次RSTTEST.ps1達到不斷的發生異常鏈接包。

param([int]$count,[string]$path)

$command = (Join-Path $path RSTTEST.ps1)
for($i = 1;$i -le $count;$i++ ){
    powershell . $command
    Write-Host $i
}

總結

本文記錄了一次真實生產環境的內存泄漏事件進行分析過程。最終經過內存分析、抓包分析、源碼分析等方式肯定了最終問題產生的緣由。在本次分析中對於非託管資源釋放、重疊I/O和完成端口進行了深刻的學習。

參考文檔

  1. 使用WinDbg
  2. 手把手教你玩轉SOCKET模型:完成端口(Completion Port)詳解
  3. Reactor與Proactor的概念
  4. 如何深入理解reactor和proactor?
  5. Handling IRPs
  6. CancelIoEx function
  7. I/O Completion Ports
  8. 《Windows via C/C++ 第五版》
  9. When to Complete an IRP
  10. WSASend function

本文地址:http://www.javashuo.com/article/p-xsspoxda-kx.html 做者博客:傑哥很忙 歡迎轉載,請在明顯位置給出出處及連接

相關文章
相關標籤/搜索