sockets - 来自阻塞的意外 WSA_IO_PENDING(具有重叠的 I/O 属性)Winsock2 调用
问题描述
简短版本:使用阻塞套接字 API 调用时,我得到 WSA_IO_PENDING。我应该如何处理?套接字具有重叠的 I/O 属性并设置了超时。
长版:
平台:Windows 10。Visual Studio 2015
套接字以非常传统的简单方式创建。
s = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
默认情况下,套接字启用了重叠 I/O属性。可以使用getsockop / SO_OPENTYPE进行验证。
- 我确实需要重叠属性,因为我想使用超时功能,例如SO_SNDTIMEO。
- 而且我只会以阻塞(即同步)方式使用套接字。
- 套接字读取操作仅在单个线程中运行。
- 可以从与互斥锁同步的不同线程执行套接字写操作。
套接字启用超时和保持活动...
::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和WSASend并将其lpOverlapped
设置lpCompletionRoutine
为 NULL。
[MSDN] ... 如果 lpOverlapped 和 lpCompletionRoutine 都为 NULL,则此函数中的套接字将被视为非重叠套接字。
::WSARecv(s, &dataBuf, 1, &nBytesReceived, &flags, NULL/*lpOverlapped*/, NULL/*lpCompletionRoutine*/)
::WSASend(s, &dataBuf, 1, &nBytesSent, 0, NULL/*lpOverlapped*/, NULL/*lpCompletionRoutine*/)
问题:
那些 send / recv / WSARecv / WSASend 阻塞调用将返回带有WSA_IO_PENDING错误代码的错误!
问题:
Q0:有没有关于阻塞调用和超时的重叠属性的参考?
它的行为如何?如果我有一个启用了重叠“属性”+超时功能的套接字,并且只需使用具有“无重叠 I/O 语义”的阻塞套接字 API。
我找不到任何关于它的参考资料(例如来自 MSDN)。
Q1:这是预期的行为吗?
在将代码从 Win XP/Win 7 迁移到Win 10后,我观察到了这个问题(获取 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。它最终会成功返回,传输 0 字节。
解决方案
Q3:怎么样
WSAGetOverlappedResult
?
在[WSA]GetOverlappedResult
我们只能使用指针WSAOVERLAPPED
传递给I/O请求。使用任何其他指针都是没有意义的。有关I/O操作的所有信息WSAGetOverlappedResult
都来自lpOverlapped
(最终状态、传输的字节数、如果需要等待 - 它等待来自此重叠的事件)。笼统地说 - 每个I/O请求都必须将OVERLAPPED
(IO_STATUS_BLOCK
真正的)指针传递给内核。内核直接修改内存(最终状态和信息(通常是传输的字节)。因为此生命周期OVERLAPPED
必须在I/O未完成之前有效。并且对于每个I/O请求必须是唯一的。[WSA]GetOverlappedResult
检查此内存OVERLAPPED
(IO_STATUS_BLOCK
真的) - 首先寻找状态。如果它是另一个来自STATUS_PENDING
- 这意味着操作已完成 - api 获取传输的字节数并返回。如果还在STATUS_PENDING
这里 -I/O
尚未完成。hEvent
如果我们想要等待 - 使用重叠的API来等待。此事件句柄在I/O请求期间传递给内核,并在I/O完成时设置为信号状态。等待任何其他事件是没有意义的——它与具体的I/O请求有什么关系?现在想想必须清楚为什么我们[WSA]GetOverlappedResult
只能通过传递给I/O请求的完全重叠的指针来调用。
如果我们不将指针传递给您OVERLAPPED
自己(例如,如果我们使用recv
or send
)低级套接字 api - 您自己OVERLAPPED
在堆栈中分配为局部变量并将其指针传递给I/O。结果 - 在这种情况下,直到I/O未完成,api 才能返回。因为重叠的内存必须有效,直到I/O未完成(完成内核将数据写入此内存)。但是我们离开函数后局部变量变得无效。所以函数必须等待到位。
因为这一切我们不能[WSA]GetOverlappedResult
在send
or之后调用recv
- 起初我们根本没有指向重叠的指针。在I/O请求中使用的第二次重叠已经“销毁”(更准确地说是在顶部下方的堆栈中 - 所以在垃圾区中)。如果I/O尚未完成 - 内核已经在随机位置堆栈中修改数据,当它最终完成时 - 这将产生不可预知的影响 - 从没有任何事情发生 - 崩溃或非常不寻常的副作用。如果send
或在I/Orecv
完成之前返回- 这将对进程产生致命影响。这绝不是(如果 Windows 中没有错误)。
Q2:我应该如何处理?
我如何尝试解释是否WSA_IO_PENDING
真的由send
or返回recv
- 这是系统错误。如果设备以这样的结果完成I/O则很好(尽管不能) - 只是一些未知的(对于这种情况)错误代码。像处理任何一般错误一样处理它。不需要特殊处理(例如异步 io)。如果I/O确实尚未完成(之后send
或recv
返回) - 这意味着在随机时间(可能已经)您的堆栈可能会损坏。这种不可预知的影响。在这里什么也做不了。这是严重的系统错误。
Q1:这是预期的行为吗?
不,这绝对不例外。
Q0:有没有关于阻塞调用和超时的重叠属性的参考?
首先,当我们创建文件句柄时,我们设置或不设置异步属性:如果是CreateFileW
- FILE_FLAG_OVERLAPPED
,如果是WSASocket
- WSA_FLAG_OVERLAPPED
。万一NtOpenFile
或NtCreateFile
- FILE_SYNCHRONOUS_IO_[NO]NALERT
(反向效果比较FILE_FLAG_OVERLAPPED
)。所有这些信息存储在- (文件对象为同步 I/O 打开。)将被设置或清除。FILE_OBJECT
.Flags
FO_SYNCHRONOUS_IO
FO_SYNCHRONOUS_IO
标志的作用是下一个: I/O子系统通过调用某些驱动程序IofCallDriver
,如果驱动程序返回STATUS_PENDING
- 如果FO_SYNCHRONOUS_IO
设置了标志FILE_OBJECT
- 就地等待(所以在内核中),直到I/O未完成。否则返回此状态 -STATUS_PENDING
对于调用者 - 它可以等待自己到位,或通过APC或IOCP接收者回调。
当我们使用socket
它内部调用时WSASocket
-
创建的套接字默认具有重叠属性
这意味着文件将没有FO_SYNCHRONOUS_IO
属性,并且低级 I/O调用可以STATUS_PENDING
从内核返回。但让我们看看recv
是如何工作的:
内部WSPRecv
调用lpOverlapped = 0
. 因为这 -WSPRecv
你自己在堆栈中分配OVERLAPPED
,作为局部变量。在通过. _ 因为没有标志ZwDeviceIoControlFile
创建的文件(套接字) -从内核返回。在这种情况下,看看 - 是。如果是 - 它不能返回,直到操作未完成。它通过-开始等待事件(内部维护此套接字的用户模式)。如果您未设置,则使用与套接字关联的值或 0(无限等待)。如果 返回(这只能在您通过 设置超时的情况下) - 这意味着I/OFO_SYNCHRONOUS
STATUS_PENDING
WSPRecv
lpOverlapped == 0
SockWaitForSingleObject
ZwWaitForSingleObject
Timeout
SO_RCVTIMEO
SO_RCVTIMEO
ZwWaitForSingleObject
STATUS_TIMEOUT
SO_RCVTIMEO
操作未在例外时间内完成。在这种情况下WSPRecv
称为SockCancelIo
(与 相同的效果CancelIo
)。在文件上的所有I/O请求(来自当前线程)完成CancelIo
之前,不得返回(等待) 。在此之后从重叠中读取最终状态。这里必须是(但实际上具体驱动程序决定哪个状态完成取消)。转换为. _ 然后调用将 ntstatus 代码转换为 win32 错误。说转换为. 但如果仍然重叠,那么 - 你得到了。仅在这种情况下。看起来像设备错误,但我无法在自己的 win 10 上重现它(可能是版本发挥作用)WSPRecv
STATUS_CANCELLED
IRP
WSPRecv
STATUS_CANCELLED
STATUS_IO_TIMEOUT
NtStatusToSocketError
STATUS_IO_TIMEOUT
WSAETIMEDOUT
STATUS_PENDING
CancelIo
WSA_IO_PENDING
在这里可以做什么(如果你确定真的得到了WSA_IO_PENDING
)?首先尝试使用WSASocket
without WSA_FLAG_OVERLAPPED
- 在这种情况下ZwDeviceIoControlFile
永远不会返回STATUS_PENDING
,你永远不会得到WSA_IO_PENDING
. 检查这个 - 错误消失了吗?如果是 -返回重叠属性并删除SO_RCVTIMEO
调用(所有这些都是为了测试- 不是发布产品的解决方案)并在此错误消失后进行检查。如果是 - 看起来像设备无效取消(带有STATUS_PENDING
?!?)IRP. 这一切的意义——找出错误更具体的地方。无论如何,有趣的是构建最小的演示 exe,它可以稳定地重现这种情况并在另一个系统上测试它 - 这会持续吗?仅适用于具体版本?如果它不能在另一个组合上重现 - 需要在你的混凝土上进行调试
推荐阅读
- c# - 如何保护服务器免受用户编写的 Python 脚本的影响
- javascript - 使用 JavaScript 或 jQuery 计算 textarea 中显示的行数
- r - 在箱线图上绘制矩形(R 中的 ggplot2)
- python - 如何在redis-py中指定“>”
- docker - 在 Docker 和 nginx 反向代理上部署 Nexus3 时出现 UI 损坏和多个控制台错误
- android - 是否可以跟踪 Gitlab CI 作业的运行次数?
- python - 如何使用 GDAL 在 QGIS 中将许多栅格与 python 脚本合并?
- javascript - MongoDB Compass 如何处理 int64?
- javascript - Vuejs 自定义搜索过滤器不适用于 Computed
- swift - SwiftUI 在哪里声明扩展?