Неожиданный 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. смысл всего этого - найдите где ошибка конкретнее. В любом случае интересно будет собрать минимальный демо-файл, который сможет стабильно воспроизвести эту ситуацию и протестировать ее на других системах - это сохраняется? только для конкретных версий? если это не может быть воспроизведено на другом компе - нужно отладить на вашем конкретном

Другие вопросы по тегам