意外 WSA_IO_PENDING 阻塞(具有重叠的 I/O 属性)Winsock2 调用

Unexpected WSA_IO_PENDING from blocking (with overlapped I/O attribute) Winsock2 calls

短版: 使用阻塞套接字 API 调用时,我得到 WSA_IO_PENDING。我该如何处理?套接字有 overlapped I/O attribute 并设置了超时。

长版

平台: Windows 10. Visual Studio 2015

一个socket以非常传统的简单方式创建

s = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

套接字默认启用重叠 I/O 属性 。可以用getsockop / SO_OPENTYPE.

来验证

套接字启用超时并保持活动...

::setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, ...);

::setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, ...);

::WSAIoctl(s, SIO_KEEPALIVE_VALS, ...);

套接字操作完成

::send(s, sbuffer, ssize, 0);

::recv(s, rbuffer, rsize, 0);

我也尝试使用 WSARecv and WSASend 并将 lpOverlappedlpCompletionRoutine 都设置为 NULL。

[MSDN] ... If both lpOverlapped and lpCompletionRoutine are NULL, the socket in this function will be treated as a non-overlapped socket.

::WSARecv(s, &dataBuf, 1, &nBytesReceived, &flags, NULL/*lpOverlapped*/, NULL/*lpCompletionRoutine*/)

::WSASend(s, &dataBuf, 1, &nBytesSent, 0, NULL/*lpOverlapped*/, NULL/*lpCompletionRoutine*/)

问题:

那些 send / recv / WSARecv / WSASend 阻塞调用会 return 错误,错误代码为 WSA_IO_PENDING

问题:

Q0:是否有任何关于阻塞调用和超时的重叠属性的引用?

它的行为如何? 如果我有一个启用重叠 "attribute" + 超时功能的套接字,并且只使用阻塞套接字 API 和 "none-overlapped I/O semantics".

我找不到关于它的任何参考资料(例如来自 MSDN)。

Q1:这是预期的行为吗?

我在将代码从 Win XP/Win 7 迁移到 Win 10.

后发现了这个问题 (get WSA_IO_PENDING)

这里是客户端代码部分:(注:真正的代码中并没有使用断言,这里只是描述了相应的错误会被处理,一个错误的套接字会停止程序..)

    auto s = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    assert(s != INVALID_SOCKET);

    timeval timeout;
    timeout.tv_sec = (long)(1500);
    timeout.tv_usec = 0;

    assert(::setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (const char*)&timeout, sizeof(timeout)) != SOCKET_ERROR);

    assert(::setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, (const char*)&timeout, sizeof(timeout)) != SOCKET_ERROR);

    struct tcp_keepalive
    {
      unsigned long onoff;
      unsigned long keepalivetime;
      unsigned long keepaliveinterval;
    } heartbeat;
    heartbeat.onoff             = (unsigned long)true;                         
    heartbeat.keepalivetime     = (unsigned long)3000;
    heartbeat.keepaliveinterval = (unsigned long)3000;
    DWORD nob = 0;

    assert(0 == ::WSAIoctl(s, SIO_KEEPALIVE_VALS, &heartbeat, sizeof(heartbeat), 0, 0, &nob, 0, 0));

    SOCKADDR_IN connection;
    connection.sin_family = AF_INET;
    connection.sin_port = ::htons(port);
    connection.sin_addr.s_addr = ip;

    assert(::connect(s, (SOCKADDR*)&connection, sizeof(connection)) != SOCKET_ERROR);

    char buffer[100];
    int receivedBytes = ::recv(s, buffer, 100, 0);

    if (receivedBytes > 0)
    {
      // process buffer
    }
    else if (receivedBytes == 0)
    {
      // peer shutdown
      // we will close socket s
    }
    else if (receivedBytes == SOCKET_ERROR)
    {
      const int lastError = ::WSAGetLastError();
      switch (lastError)
      {
      case WSA_IO_PENDING:
          //.... I get the error!
      default:
      }
    }

Q2:我该如何处理?

忽略它?或者像通常的错误情况一样关闭套接字?

根据观察,一旦我得到 WSA_IO_PENDING,如果我只是忽略它,套接字最终将不再响应..

Q3:WSAGetOverlappedResult怎么样?

有意义吗?

我应该给什么 WSAOVERLAPPED 对象?由于没有这样的一个,我将其用于所有那些阻塞套接字调用。

我试过只创建一个新的空 WSAOVERLAPPED 并用它来调用 WSAGetOverlappedResult。它最终会 return 成功传输 0 字节。

Q3: How about WSAGetOverlappedResult?

[WSA]GetOverlappedResult中我们只能使用指向WSAOVERLAPPED的指针传递给I/O请求。使用任何其他指针是没有意义的。所有关于 I/O 操作 WSAGetOverlappedResult 的信息都来自 lpOverlapped (最终状态,传输的字节数,如果需要等待 - 它等待事件发生重叠)。一般而言 - 每个 I/O 请求都必须将 OVERLAPPEDIO_STATUS_BLOCK 真的)指针传递给内核。内核直接修改内存(最终状态和信息(通常是传输的字节)。因为OVERLAPPED的这个生命周期必须有效,直到I/O不完整。并且必须是唯一的对于每个 I/O 请求。[WSA]GetOverlappedResult 检查此内存 OVERLAPPEDIO_STATUS_BLOCK 真的) - 首先查找状态。如果另一个来自 STATUS_PENDING - 这意味着操作已完成 - api 获取传输的字节数和 return。如果这里仍然 STATUS_PENDING - I/O 尚未完成。如果我们想要等待 - api 使用 hEvent 从重叠到等待。此事件句柄在 I/O 请求期间传递给内核,并将设置为I/O 完成时的信号状态。等待任何其他事件是没有意义的 - 它与具体的 I/O 请求有何关系?想想现在必须清楚为什么我们只能通过 exactly 重叠指针传递给 I/O 请求来调用 [WSA]GetOverlappedResult

如果我们自己不传递指向 OVERLAPPED 的指针(例如,如果我们使用 recvsend)低级套接字 api - 你自己分配 OVERLAPPED作为堆栈中的局部变量并将其指针传递给I/O。结果 - 在这种情况下 api 不能 return 直到 I/O 未完成。因为重叠内存必须有效,直到 I/O 未完成(完成时内核将数据写入此内存)。但是在我们离开函数后局部变量变得无效。所以函数必须原地等待。

因为所有这些我们不能在 sendrecv 之后调用 [WSA]GetOverlappedResult - 起初我们根本没有指向重叠的指针。在 I/O 中使用的第二个重叠请求已经 "destroyed" (更确切地说在顶部下方的堆栈中 - 所以在垃圾区)。如果 I/O 尚未完成 - 内核已经在随机位置堆栈中修改数据,当它最终完成时 - 这将产生不可预测的影响 - 从什么都没有发生 - 崩溃或非常不寻常的副作用。如果 sendrecv return 在 I/O 完成之前 - 这将对进程产生致命影响。这绝不是必须的(如果 windows 中没有错误)。

Q2: How should I handle it?

如果 WSA_IO_PENDING 真的 return 被 sendrecv 编辑,我将如何尝试解释 - 这是系统错误。如果 I/O 由设备完成并具有这样的结果(尽管它不能),则很好 - 只是一些未知的(对于这种情况)错误代码。像处理任何一般错误一样处理它。不需要特殊处理(比如异步 io)。如果 I/O 确实尚未完成(在 sendrecv returned 之后) - 这意味着在随机时间(可能是已经)您的堆栈可能已损坏。这种效果不可预测。在这里什么也做不了。这是严重的系统错误。

Q1: is it expected behavior?

不,这绝对不例外。

Q0: any reference on overlapped attribute with blocking call and timeout?

首先,当我们创建文件句柄时,我们设置或不设置它的异步属性:如果 CreateFileW - FILE_FLAG_OVERLAPPED,如果 WSASocket - WSA_FLAG_OVERLAPPED .如果 NtOpenFileNtCreateFile - FILE_SYNCHRONOUS_IO_[NO]NALERT(反向效果比较 FILE_FLAG_OVERLAPPED)。 FILE_OBJECT.Flags - FO_SYNCHRONOUS_IO (The file object is opened for synchronous I/O.) 中存储的所有这些信息将被设置或清除.

接下来是 FO_SYNCHRONOUS_IO 标志的作用:I/O 子系统通过 IofCallDriver 调用某些驱动程序,如果驱动程序 return STATUS_PENDING - 如果在 FILE_OBJECT 中设置了 FO_SYNCHRONOUS_IO 标志 - 在原地等待(因此在内核中)直到 I/O 未完成。否则 return 这个状态 - STATUS_PENDING 对于调用者 - 它可以在原地等待,或者通过 APCIOCP[=164= 接收回调].

当我们使用 socket 它内部调用 WSASocket -

The socket that is created will have the overlapped attribute as a default

这意味着文件将没有 FO_SYNCHRONOUS_IO 属性并且 低级别 I/O 调用可以 return STATUS_PENDING 来自内核。但让我们看看 recv 是如何工作的:

内部 WSPRecv is called with lpOverlapped = 0. because this - WSPRecv yourself allocate OVERLAPPED in stack, as local variable. before do actual I/O request via ZwDeviceIoControlFile. because file (socket) created without FO_SYNCHRONOUS flag - the STATUS_PENDING is returned from kernel. in this case WSPRecv look - are lpOverlapped == 0. if yes - it can not return, until operation not complete. it begin wait on event (internally maintain in user mode for this socket) via SockWaitForSingleObject - ZwWaitForSingleObject. in place Timeout used value which you associated with socket via SO_RCVTIMEO or 0 (infinite wait) if you not set SO_RCVTIMEO. if ZwWaitForSingleObject return STATUS_TIMEOUT (this can be only in case you set timeout via SO_RCVTIMEO) - this mean that I/O operation not finished in excepted time. in this case WSPRecv called SockCancelIo (same effect as CancelIo). CancelIo 不能 return (等待)直到所有 I/O 文件请求(来自当前线程)完成。在此之后 WSPRecv 从重叠中读取最终状态。这里必须是 STATUS_CANCELLED(但实际上具体的驱动程序决定以哪个状态完全取消 IRP)。 WSPRecvSTATUS_CANCELLED 转换为 STATUS_IO_TIMEOUT。然后调用 NtStatusToSocketError 将 ntstatus 代码转换为 win32 错误。说 STATUS_IO_TIMEOUT 转换为 WSAETIMEDOUT。但如果仍然是 STATUS_PENDING 重叠,在 CancelIo 之后 - 你得到 WSA_IO_PENDING。只有在这种情况下。看起来像设备错误,但我无法在自己的 win 10 上重现它(可能是版本扮演角色)


这里可以做什么(如果你确定真的得到了WSA_IO_PENDING)?首先尝试在没有 WSA_FLAG_OVERLAPPED 的情况下使用 WSASocket - 在这种情况下 ZwDeviceIoControlFile 永远不会 return STATUS_PENDING 并且你永远不会得到 WSA_IO_PENDING。检查这个 - 错误消失了吗?如果是 - return 重叠属性 并删除 SO_RCVTIMEO 调用(所有这些 用于测试 - 不是发布产品的解决方案)并在此错误消失后进行检查。如果是 - 看起来设备无效取消(使用 STATUS_PENDING ?!?)IRP。所有这一切的意义 - 找出错误更具体的地方。无论如何有趣的是构建最小的演示 exe,它可以稳定地重现这种情况并在另一个系统上测试它 - 这是否持续存在?仅适用于具体版本?如果它不能在另一个 comps 上复制 - 需要在你的混凝土上调试