【问题标题】:How to safely stop a IOCP WSARecv() task, and free the WSAOVERLAPPED structure?如何安全地停止 IOCP WSARecv() 任务并释放 WSAOVERLAPPED 结构?
【发布时间】:2020-09-20 17:38:12
【问题描述】:

我的 IOCP 服务器程序在运行时会消耗越来越多的内存。在跟踪内存泄漏后,我发现一些 WSAOVERLAPPED 结构提要到 WSARecv() 永远不会被回收。我认为这是因为一些恶意客户端套接字只建立连接但从不发送数据或关闭。所以我在每个客户端套接字上设置了一个 TimerQueueTimer() 来识别超时套接字并删除它们。但是如果我在删除恶意套接字时释放 WSAOVERLAPPED 结构,过了一会儿,我得到“释放后在 014C7DA8 修改的 Free Heap block 014C7D80”。

以下是一些相关代码:

typedef struct _SocketState
{
   char operation; 
   SOCKET socket;   
   DWORD length;
   HANDLE hTimer;
   HANDLE hCompletion;
   WSAOVERLAPPED* thisOvl;
   char buf[MAX_BUF];
} SocketState;

static WSAOVERLAPPED* new_overlapped(void)
{
   return (WSAOVERLAPPED*)calloc(1, sizeof(WSAOVERLAPPED));
}

static void create_io_completion_port(void)
{
   cpl_port = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
   if (!cpl_port)
   {
        int err = WSAGetLastError();
        exit(1);
   }
}

static void post_reading(SocketState* socketState, WSAOVERLAPPED* ovl)
{
   DWORD flags = 0;
   WSABUF wsabuf = { MAX_BUF, socketState->buf };
   int err=0;
   memset(ovl, 0, sizeof(WSAOVERLAPPED));
   socketState->operation = OP_READ;
   socketState->thisOvl=ovl;
   if (WSARecv(socketState->socket, &wsabuf, 1, NULL, &flags, ovl, NULL)== SOCKET_ERROR)
   {
      err = WSAGetLastError();
      if (err != WSA_IO_PENDING)
      {
        printf("[%s:%d]WSARecv error\n", __FUNCTION__, __LINE__);
        destroy_connection(socketState, ovl);
        return ;
      }
   }
   c_WSARecv++;
}

static void destroy_connection(SocketState* socketState, WSAOVERLAPPED* ovl)
{
    int err=0;
    if(socketState->hTimer != NULL)
    {
        DeleteTimerQueueTimer(hTimerQueue,socketState->hTimer,INVALID_HANDLE_VALUE);
        socketState->hTimer = NULL; 
    }
    socketState->hCompletion=NULL; //newSocketState->hCompletion = cpl_port
    closesocket(socketState->socket);
    free(socketState);
    if(ovl!=0)
    {
       free(ovl);
    }
}
VOID CALLBACK TimerRoutine(PVOID lpParam, BOOLEAN TimerOrWaitFired)
{
    SocketState* clientSocketState=(SocketState*)lpParam;
    if (lpParam != NULL)
    {
        if(clientSocketState->hCompletion != NULL)
        {
            PostQueuedCompletionStatus(clientSocketState->hCompletion,-2,(ULONG_PTR)clientSocketState,clientSocketState->thisOvl);
            //should last parameter be NULL?
            //the "-2" is for identify this timeout io after GetQueuedCompletionStatus()
        }
    }
}

由于我在服务器程序中使用 C 而不是 C++,因此我将自己置于非常尴尬的境地。基本上我在 C 中找不到一个非常好的 IOCP 示例 :(.

【问题讨论】:

  • 所以使用任何异步版本的读取超时来检测流氓客户端。
  • 解决方案 - 使用引用计数。您将指针传递给WSAOVERLAPPED - 添加对它的引用。 I/O 完成 - 调用释放。
  • 然后socketState->operation = OP_READ; socketState->thisOvl=ovl; 当然是设计错误。 socketState 不得持有指向重叠的指针。和状态读取 - 无效(按设计)套接字可以立即进行读取和写入。所以有几个 I/O 请求。需要签证 vesra - 重叠必须有指向 socketState 的指针。并且参考必须在上面
  • 您需要创建继承自OVERLAPPED 的self 类,并在此处持有指向SocketStateoperation 的指针
  • 传统上,one-off socket对象和many-off buffer+ovl是不同的结构体。然后每个缓冲区对象都有自己的重叠块,hEvent 指向自己的缓冲区对象,缓冲区对象带有指向套接字对象的指针。您的数据是 umm... 非最佳:(

标签: c++ c iocp


【解决方案1】:

OVERLAPPED 只能在 I/O 完成后释放。所以真的所有需要​​取消 I/O 操作。这可以通过调用CancelIoEx 来完成,或者更好地通过调用closesocket 来完成(closesocket 函数将取消未完成的 I/O 操作)。当 I/O 完成时 - 您将指向 OVERLAPPED 的指针传递给此 I/O 并在处理 I/O 结果后 - 您可以释放或重用 OVERLAPPED


您有与每个套接字关联的结构 - SocketState 这是正确的。 但是什么必须和不可以有这种结构?

它必须实现引用计数(因为它以复杂且不可预测的顺序从多个线程访问)。并握住插座的把手。在closesocket/CancelIoEx 调用后,强制对套接字句柄进行某种破坏保护,以便不使用它(它的实现已经单独的问题)。这是必要的,因为 Winsock 客户端绝不能在套接字上同时发出 closesocket 和另一个 Winsock 函数调用,但我们需要随时能够调用 closesocket 以在丢失的远程端取消 I/O。

另一方面,它不能有指向OVERLAPPED(或它的shell 类)的指针,因为套接字上可以同时存在多个I/O。我们可以并行读写。由于同样的原因,它也不能有operation 成员 - 例如,读取和写入操作可以并行激活。所以像

这样的代码
socketState->operation = OP_READ;
socketState->thisOvl=ovl;

设计错误。在SocketState 内有hCompletion 也没有意义,因为hCompletion 不是每个套接字。这里放错地方了。

我们还需要强制使用非裸OVERLAPPED 结构将其传递给I/O,而是从OVERLAPPED 继承自类。你需要在这里有额外的成员——引用指向你的SocketState的指针——因为当I/O完成时——你得到了指向OVERLAPPED的指针,并且需要从中得到指向你的socketState的指针。 operation 也必须在此处(而不是 SocketState),因为操作是按 I/O 而不是按套接字的。所以总的来说可以是下一个:

struct SocketState
{
    SOCKET socket;   
    HANDLE hTimer;
    ULONG dwRefCount;

    void AddRef();
    void Release();

    _NODISCARD SOCKET LockHandle();
    void Rundown();
    void UnlockHandle();// call closesocket on last unlock

    void OnIoComplete(ULONG operation, ULONG dwErrorCode, ULONG dwBytesTransfered, PVOID buf);

    void StartSomeIo(ULONG operation, PVOID buf, ULONG cb);

    void Close()
    {
        if (LockHandle())
        {
            Rundown();
            UnlockHandle();
        }
    }
};

struct UIrp : OVERLAPPED
{
    SocketState* socketState;
    ULONG operation;
    PVOID buf;

    UIrp(SocketState* socketState, ULONG operation, PVOID buf) 
        : socketState(socketState), operation(operation), buf(buf)
    {
        RtlZeroMemory(static_cast<OVERLAPPED*>(this), sizeof(OVERLAPPED));
        socketState->AddRef();
    }

    ~UIrp()
    {
        socketState->Release();
    }

    void OnIoComplete(ULONG dwErrorCode, ULONG dwBytesTransfered)
    {
        socketState->OnIoComplete(operation, dwErrorCode, dwBytesTransfered, buf);
        delete this;
    }
};

void SocketState::StartSomeIo(ULONG operation, PVOID buf, ULONG cb)
{
    if (UIrp* irp = new UIrp(this, operation, buf))
    {
        ULONG dwError = ERROR_INVALID_HANDLE;

        if (SOCKET s = LockHandle())
        {
            dwError = WSA*(s,... irp, 0) == 0 ? NOERROR : WSAGetLastError();
            UnlockHandle();
        }

        switch (dwError)
        {
        case NOERROR:
        case WSA_IO_PENDING:
            break;
        default:
            irp->OnIoComplete(dwError, 0);
        }
    }
}

void PortLoop(HANDLE hCompletionPort)
{
    for (;;)
    {
        OVERLAPPED* lpOverlapped;
        ULONG dwBytesTransfered;
        ULONG_PTR CompletionKey;

        ULONG dwError = GetQueuedCompletionStatus(hCompletionPort, &dwBytesTransfered, 
            &CompletionKey, &lpOverlapped, INFINITE) ? NOERROR : GetLastError();

        // probably somehow use CompletionKey

        if (!lpOverlapped)
        {
            break;
        }

        static_cast<UIrp*>(lpOverlapped)->OnIoComplete(dwBytesTransfered, dwError);
    }
}

关于

在每个客户端套接字上设置一个TimerQueueTimer()

这在设计上是可能的并且是正确的,但如果您有很多插槽,则认为这不是最好的。我将从LIST_ENTRY 继承SocketState 并将所有活动套接字插入到某个列表中。并通过计时器定期检查超时并关闭套接字

【讨论】:

  • 感谢您如此耐心地回答我的问题。我将花一些时间重写我的代码并检查是否正常。
  • 对不起☹,我不擅长C++,你有没有机会把你的代码变成C形式?
  • @Dery - 我的代码实际上是更快的“c with classes”风格。如果明确使用 this 指针,你当然可以用纯 c 编写这个,而不是使用继承,而是将OVERLAPPED 作为UIrp 等中的第一个成员包含在内。但想想如果您可以使用 c++ 编译器,那么这样做毫无意义。这里主要不是语言(c 或 c++),而是理解我做什么和为什么做。如果您有更具体的问题 - 问,请尝试给出更具体的答案
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-05-11
相关资源
最近更新 更多