Неожиданный WSA_IO_PENDING от блокировки (с перекрывающимся атрибутом ввода / вывода) вызовы Winsock2
Короткая версия: я получаю WSA_IO_PENDING при использовании блокирующих вызовов API сокетов. Как мне справиться с этим? Сокет имеет перекрывающийся атрибут ввода / вывода и устанавливается с таймаутом.
Длинная версия:
Платформа: Windows 10. Visual Studio 2015
Сокет создается очень традиционным простым способом.
s = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
В сокете по умолчанию включен атрибут перекрывающегося ввода-вывода. Это можно проверить с помощью 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*/)
Проблема:
Эти вызовы блокировки отправки / recv / WSARecv / WSASend вернут ошибку с кодом ошибки WSA_IO_PENDING!
Вопросы:
Q0: есть ссылка на перекрывающийся атрибут с блокировкой вызова и тайм-аутом?
Как это ведет себя? в случае, если у меня есть сокет с перекрывающимся "атрибутом" + функция тайм-аута, и я просто использую блокирующий API сокета с "не перекрывающейся семантикой ввода-вывода".
Я не мог найти какую-либо ссылку об этом (например, из MSDN).
Q1: это ожидаемое поведение?
Я наблюдал эту проблему (получить WSA_IO_PENDING) после переноса кода из Win XP/ Win 7 в Win 10.
Вот часть кода клиента: (примечание: assert не используется в реальном коде, а просто описывает здесь, что соответствующая ошибка будет обработана и неисправный сокет остановит процедуру..)
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 переданным байтом.
1 ответ
Q3: как насчет
WSAGetOverlappedResult
?
в [WSA]GetOverlappedResult
мы можем использовать только указатель на WSAOVERLAPPED
перешел на запрос ввода / вывода. Использовать любой другой указатель бессмысленно. вся информация о работе ввода / вывода WSAGetOverlappedResult
получить от lpOverlapped
(окончательный статус, количество переданных байтов, если нужно подождать - это ожидание по событию из этого перекрывается). в общих словах - каждый запрос ввода / вывода должен пройти OVERLAPPED
(IO_STATUS_BLOCK
действительно) указатель на ядро. память прямого изменения ядра (окончательный статус и информация (обычно передаются байты)). OVERLAPPED
должен быть действителен, пока ввод / вывод не завершен. и должен быть уникальным для каждого запроса ввода / вывода. [WSA]GetOverlappedResult
проверь эту память OVERLAPPED
(IO_STATUS_BLOCK
действительно) - в первую очередь ищите статус. если это другой из STATUS_PENDING
- это означает, что операция завершена - API принимает количество переданных байтов и возвращает их. если еще STATUS_PENDING
Вот - I/O
еще не завершено. если мы хотим подождать - использовать API hEvent
от перекрытия ждать. этот дескриптор события передается ядру во время запроса ввода-вывода и будет установлен в состояние сигнала после завершения ввода-вывода. ждать какого-либо другого события бессмысленно - как оно связано с конкретным запросом ввода-вывода? думаю, теперь должно быть ясно, почему мы можем позвонить [WSA]GetOverlappedResult
только с точно перекрытым указателем, переданным на запрос ввода / вывода.
если мы не передадим указатель на OVERLAPPED
себя (например, если мы используем recv
или же send
) сокет низкого уровня api - сами выделяйте OVERLAPPED
как локальная переменная в стеке и передать ей указатель на ввод / вывод. как результат - API не может вернуться в этом случае, пока ввод / вывод не завершен. потому что перекрывающаяся память должна быть действительной до тех пор, пока ввод / вывод не завершится (при завершении ядро записывает данные в эту память). но локальная переменная стала недействительной после выхода из функции. поэтому функция должна ждать на месте.
потому что все это мы не можем назвать [WSA]GetOverlappedResult
после send
или же recv
- сначала у нас просто нет указателя на перекрытие. на втором этапе перекрытие, используемое в запросе ввода-вывода, уже "уничтожено" (точнее, в стеке ниже вершины - значит, в мусорной зоне). если ввод / вывод еще не завершен - ядро уже модифицирует данные в произвольном месте стека, когда оно окончательно завершится - это будет иметь непредсказуемый эффект - ни от чего не происходит - до сбоя или очень необычных побочных эффектов. если send
или же recv
возврат до завершения ввода / вывода - это будет иметь фатальный эффект для процесса. это никогда не должно быть (если нет ошибки в Windows).
Q2: как я должен справиться с этим?
как я пытаюсь объяснить, если WSA_IO_PENDING
действительно вернулся send
или же recv
- это системная ошибка. хорошо, если ввод / вывод завершен устройством с таким результатом (несмотря на то, что не должен) - просто какой-то неизвестный (для такой ситуации) код ошибки. обрабатывать это как любую общую ошибку. не требует специальной обработки (как в случае асинхронного ввода-вывода). если ввод / вывод действительно еще не завершен (после send
или же recv
возвращено) - это означает, что в случайное время (возможно, уже) ваш стек может быть поврежден. Эффект от этого непредсказуем. и тут ничего не поделаешь. это критическая системная ошибка.
Q1: это ожидаемое поведение?
нет, это абсолютно не исключено.
Q0: есть ссылка на перекрывающийся атрибут с блокировкой вызова и тайм-аутом?
Прежде всего, когда мы создаем дескриптор файла, мы устанавливаем или не устанавливаем асинхронный атрибут для него: в случае CreateFileW
- FILE_FLAG_OVERLAPPED
, в случае WSASocket
- WSA_FLAG_OVERLAPPED
, в случае NtOpenFile
или же NtCreateFile
- FILE_SYNCHRONOUS_IO_[NO]NALERT
(обратный эффект сравнения FILE_FLAG_OVERLAPPED
). вся эта информация хранится в FILE_OBJECT
.Flags
- FO_SYNCHRONOUS_IO
(Объект файла открыт для синхронного ввода-вывода.) Будет установлен или очищен.
эффект FO_SYNCHRONOUS_IO
флаг следующий: подсистема ввода / вывода вызывает некоторый драйвер через IofCallDriver
и если водитель вернется STATUS_PENDING
- в случае FO_SYNCHRONOUS_IO
флаг установлен в FILE_OBJECT
- ждать на месте (так в ядре), пока ввод / вывод не завершится. в противном случае верните этот статус - STATUS_PENDING
для вызывающего абонента - он может ждать себя на месте или обратного вызова получателя через APC или IOCP.
когда мы используем socket
это внутренний звонок WSASocket
-
Созданный сокет будет иметь перекрывающийся атрибут по умолчанию
этот средний файл не будет иметь FO_SYNCHRONOUS_IO
Атрибутивные и низкоуровневые вызовы ввода / вывода могут возвращаться STATUS_PENDING
из ядра. но давайте посмотрим, как recv
работает:
внутренне WSPRecv
называется с lpOverlapped = 0
, потому что это - WSPRecv
Выделите себя OVERLAPPED
в стеке, как локальная переменная. прежде чем сделать фактический запрос ввода-вывода через ZwDeviceIoControlFile
, потому что файл (сокет) создан без FO_SYNCHRONOUS
флаг - STATUS_PENDING
возвращается из ядра. в этом случае WSPRecv
смотри - есть lpOverlapped == 0
, если да - возврат невозможен, пока операция не завершена. начинается ожидание события (внутренне поддерживается в режиме пользователя для этого сокета) через SockWaitForSingleObject
- ZwWaitForSingleObject
, на месте Timeout
используемое значение, которое вы связали с сокетом через SO_RCVTIMEO
или 0 (бесконечное ожидание), если вы не установили SO_RCVTIMEO
, если ZwWaitForSingleObject
вернуть STATUS_TIMEOUT
(это может быть только в том случае, если вы установите тайм-аут через SO_RCVTIMEO
) - это означает, что операция ввода-вывода не завершена в течение ожидаемого времени. в этом случае WSPRecv
называется SockCancelIo
(тот же эффект, что и CancelIo
). CancelIo
не должен возвращать (ждать), пока все запросы ввода / вывода для файла (из текущего потока) не будут выполнены. после этого WSPRecv
читать окончательный статус из перекрытия. здесь должно быть STATUS_CANCELLED
(но на самом деле конкретный водитель решает, какой статус полностью отменен IRP
). WSPRecv
перерабатывать STATUS_CANCELLED
в STATUS_IO_TIMEOUT
, затем позвоните NtStatusToSocketError
для преобразования кода ntstatus в win32 ошибка. сказать STATUS_IO_TIMEOUT
конвертировано в WSAETIMEDOUT
, но если все еще был STATUS_PENDING
в перекрытии, после CancelIo
- ты получил WSA_IO_PENDING
, только в этом случае. похоже на ошибку устройства, но я не могу воспроизвести ее на собственном win 10 (может быть, версия играет роль)
что тут можно сделать (если вы уверены, что действительно получили WSA_IO_PENDING
) сначала попробуйте использовать WSASocket
без WSA_FLAG_OVERLAPPED
- в этом случае ZwDeviceIoControlFile
никогда не вернуться STATUS_PENDING
и ты никогда не должен получить WSA_IO_PENDING
, проверить это - ошибка ушла? если да - вернуть перекрывающийся атрибут и удалить SO_RCVTIMEO
вызов (все это для тестирования - не решение для выпуска продукта) и проверка после устранения этой ошибки. если да - похоже, что устройство недействительно отменить (с STATUS_PENDING
?!?) IRP. смысл всего этого - найдите где ошибка конкретнее. В любом случае интересно будет собрать минимальный демо-файл, который сможет стабильно воспроизвести эту ситуацию и протестировать ее на других системах - это сохраняется? только для конкретных версий? если это не может быть воспроизведено на другом компе - нужно отладить на вашем конкретном