天天看點

異常連接配接導緻的記憶體洩漏排查

本文記錄了一次真實生産環境的記憶體洩漏事件進行分析過程。最終通過記憶體分析、抓包分析、源碼分析等方式确定了最終問題産生的原因。在本次分析中對于非托管資源釋放、重疊I/O和完成端口進行了深入的學習。

目錄

  • 背景
  • 詳細流程
    • 使用windbg分析dump檔案
    • 使用wireshark抓包分析
  • 完成端口和重疊IO
    • 重疊I/O
    • 完成端口
    • Reactor模型與Proactor模型
    • 完成端口處理邏輯
      • 建立完成端口。
      • 注冊套接字
      • 接收用戶端請求
      • 處理I/O請求
        • 讀請求
        • 寫請求
  • 問題排查
    • 建立套接字
    • 異步接收套接字
    • 接收資料
    • 發送資料
    • 釋放套接字
    • 分析問題
    • 确認問題
  • 修複問題
  • 重制及驗證
  • 總結
  • 參考文檔

在生産環境中,部署在客戶的程式在運作了将近兩個月後發生了閃退。而且兩個伺服器的程式先後都出現了閃退現象。通過排查windows日志發現是OOM異常導緻的閃退。本文記錄了該異常事件完整的排查過程與解決方案。

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

程式崩潰後,我們要求客戶導出一個dump檔案供我們分析,并提供程式相關的運作日志。同時檢視了windows的相關日志确定了是由于OOM(Out Of Memory)異常導緻的。

啟動windbg打開dump檔案

異常連接配接導緻的記憶體洩漏排查

由于我們的程式是基于

.net framework 3.5

開發的,是以我們使用

SOS

的相關擴充指令進行分析。需要在windbg中導入

mscorwks

.loadby sos mscorwks

想對windbg進行深入學習,可以檢視《使用WinDbg》講解的非常詳細。

通過

!dumpheap -stat

對記憶體占用情況進行彙總統計。

!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個地方調用。

異常連接配接導緻的記憶體洩漏排查

可以發現該字段适用于表示重疊I/O是否正在處理。在如果重疊I/O正在處理,則不釋放相關的資源,具體原因後面講到重疊I/O時會進行說明。

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

由于我們和客戶接口是通過TCP協定傳輸,而非HTTP協定,是以理論上不應該會有HTTP請求發到我們程式端口上。又因為我們程式有接收逾時機制,即使有我們無法解析的無效請求,超過了逾時時間我們也會将對應的資源釋放。而且從dump檔案來看也沒有我們未釋放的資源對象。

為了搞清楚到底是什麼請求發到我們程式上,是以要求客戶在伺服器抓包。我們對抓封包件進行分析。發現抓到了大量的異常連接配接,每5秒會有2個。

異常連接配接導緻的記憶體洩漏排查

然後我通過計算未釋放對象的數量基本與接收到這個包數量吻合。是以初步斷定記憶體洩漏是由于該包引起的。這個包應該是一個服務監控程式發的,每五秒發一次,有2個位址在往我們程式發。

确定了初步的原因,接下來就需要進行源碼分析,排查問題點。由于

AsyncIO

使用的是基于完成端口的重疊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++ 第五版》的第七章

目前常提到的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和完成端口後,回到問題排查中。由于前面我們已經發現所有記憶體洩漏點都是由于重疊資源未釋放導緻的,而實際我們已經調用過

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_boundAddress

    m_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. 在調用重疊操作的時候,當時傳回的結果就不是SUCCESS和ERROR_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;          
}
           

以接收資料為例,可以對問題的原因進行确認。

當我們調用重疊操作的時候。若重疊操作傳回的結果是SUCCESS和ERROR_IO_PENDING以外的值,則重疊操作并沒有被真正的送出。就如我們前面所将,重疊操作送出到裝置驅動隊列時會傳回ERROR_IO_PENDING,而以同步方式執行完成時則直接傳回SUCCESS。

在發生和接收時判斷以下傳回結果的若不是SUCCESS和ERROR_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
}
           

  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
異常連接配接導緻的記憶體洩漏排查

微信掃一掃二維碼關注訂閱号傑哥技術分享

本文位址:https://www.cnblogs.com/Jack-Blog/p/11295815.html

作者部落格:傑哥很忙

歡迎轉載,請在明顯位置給出出處及連結

每天收獲一點點