waqur: (Евро)
[personal profile] waqur
Мы рассматриваем такую постановку задачи, когда всем сетевым IO занимается выделенный поток. Он формирует некие запросы для рабочих потоков, и получает некие ответы на эти запросы от рабочих потоков, а сам занимается только сетевым вводом-выводом и стремится делать эту работу наиболее оптимальным способом.


1. UNIX



В FreeBSD и Linux'е всё устроено достаточно просто. Есть сокеты, их нужно перевести в неблокирующий режим (ioctl FIONBIO), а затем операции accept/send/sendto/recv/recvfrom, которые ранее могли ввести поток в состояние сна, начинают возвращать EWOULDBLOCK (в условиях когда буфер входящих данных пуст или буфер исходящих данных полон). Чтобы не занимать процессор на 100% операциями с неготовыми сокетами, существует механизм наблюдения за состоянием буферов сокетов — kqueue и epoll, который и сообщает коду режима пользователя, какие именно сокеты имеют готовый к IO буфер и каков его размер. (В Solaris используются ioctl-запросы к /dev/poll для той же цели.)

Немного отдельно стоит операция connect. В неблокирующем режиме она начинает асинхронный ввод-вывод, возвращает EINPROGRESS, а событие "сокет подключился" или "произошла ошибка" возвращает через тот же самый механизм kqueue или epoll. Пока длится асинхронная операция connect, попытки сделать другой connect на том же сокете будут возвращать EALREADY.

2. Windows



В Windows всё сложнее. Сетевая архитектура такова: пользовательский код линкуется с ws2_32.dll, которая в случае TCP и UDP является переходником к mswsock.dll и msafd.dll (ATM-сокеты, IrDA-сокеты и прочее мы не рассматриваем), а эти DLL-ки общаются с afd.sys посредством открытия файловых хэндлов \Device\Afd\... и выполнения на них IOCTL-запросов.

Вместо наблюдения за состоянием сокетов применяется очередь событий (IO completion port).

Создание нового сокета обходится дорого, поэтому для TCP-серверов принято создавать пул preaccept-сокетов заранее (к примеру, 128 штук) и привязывать их к listen-сокету через механизм AcceptEx, который будет сигналить в порт завершения о подключении нового клиента и переводе сокета из режима "только что открыт" в режим "принял входящее соединение". Приняв подключение, вы сразу же создаёте новый preaccept-сокет и включаете его в "пул AcceptEx". Этот механизм действует вместо очереди listen backlog в UNIX. Только нужно задавать параметр dwReceiveDataLength = 0 во избежание DoS-атак на сервер.

Примерно так же хорошо работает и ConnectEx: операция подключения к удалённому хосту выполняется как асинхронная, по её завершению ядро сигналит в порт завершения новый статус сокета или ошибку. Аналогично dwSendDataLength всегда = 0 во избежание DoS.

Однако то, что хорошо подходит для accept и connect, подходит плохо для send и recv. В Microsoft в своё время этого не поняли и создали функции WSASend и WSARecv, которые работают точно так же: передают или принимают заданное количество байт и сигналят в порт завершения. Проблема в том, что заранее неизвестно, сколько именно байт будет получено от удалённого хоста. Другая проблема заключается в том, что страницы с буферами принимаемых/отправляемых данных включаются в невыгружаемый пул на неопределённо долгое время, и после исчерпания квоты этого пула из winsock2 начинают сыпаться ошибки WSAENOBUFS = ERROR_INSUFFICIENT_RESOURCES. Вопреки распространённому мнению, подобная ситуация — не редкость, ведь пребывая в неведении относительно размера будущих принятых данных, программисты предпочитают распределить буфер побольше.

От overlapped-операций WSASend и WSARecv можно отказаться и вернуться к неблокирующему IO в стиле UNIX, однако нужен какой-то механизм уведомлений от ядра о том, что буфер входящих данных непуст, а буфер исходящих данных неполон. Для первого может применяться операция WSARecv с буфером нулевого размера. Она начинается как асинхронная и завершается через IOCP как только сокет готов к неблокирующему чтению. Если вы выбрали из сокета не всё или туда успело что-то прийти между последним синхронным неблокирующим recv и следующим за ним асинхронным WSARecv — это не проблема и гонок не будет: в ответ на ваш новый WSARecv с буфером нулевого размера ядро сразу пришлёт новый пакет на IOCP. Ну а если наступил таймаут, подвисший запрос можно отменить через CancelIo или CancelIoEx.

Проблема тут в другом: zero byte receive — это не документированная особенность WSARecv, её нет в MSDN. Эта особенность впервые описана в книге Network Programming for Microsoft Windows, Second Edition за авторством Anthony Jones and Jim Ohlund, издано Microsoft Press в 2002 году; также использование этой техники можно проследить в libuv. Ну и WSASend такое не умеет. Следовательно, для отправки данных всё равно придётся использовать асинхронные операции вместо неблокирующих, и получать все сопустствующие проблемы — исчерпание невыгружаемого пула и ошибки WSAENOBUFS. Кстати, возврат WSAENOBUFS из функций winsock2 — это ещё "цветочки", а "ягодки" — это когда в то же самое время какой-нибудь драйвер неожиданно получает ошибку типа "out of memory" и с грохотом валит всю систему в BSoD.

Заставить Windows работать как UNIX можно по-другому. MSDN описывает функцию WSAAsyncSelect — уродливый реликт для совместимости с Windows 3.1, который отправляет события на сокетах окнам через подсистему GDI. Однако afd.sys, разумеется, не работает с окнами. Вместо этого afd.sys принимает от msafd.dll IOCTL-запросы с кодом IOCTL_AFD_POLL, которые сигналят в специальный IOCP. Ну а msafd.dll запускает в пользовательском режиме отдельный поток, который слушает этот самый IOCP и перебрасывает уведомления в очередь GDI для соответствующего окна.

Разумеется, нет причин быть хуже msafd: мы и сами можем вызвать DeviceIoControl IOCTL_AFD_POLL с хэндлом сокета и получать все нужные уведомления напрямую в наш порт. Исходные коды Windows 2000 содержат для этого всё необходимое, впрочем можно пользоваться источником и второй свежести — epoll_windows или libuv (uv_msafd_poll).

poll-запросы, касающиеся разных хэндлов, можно отправлять драйверу afd.sys пачкой. Забавно, что для этого ioctl-вызова подойдёт хэндл любого сокета. Или можно открыть \Device\Afd\AsyncSelectHlp: так делает msafd.dll, чтобы не портить пользовательские хэндлы привязкой к своему IOCP. Разумеется, ваш хэндл открытого \Device\Afd\AsyncSelectHlp будет привязан к вашему IOCP.

В любом случае, какой бы способ вы не выбрали — zero-byte WSASend или IOCTL_AFD_POLL — вам придётся полагаться на недокументированные особенности Windows. Вы ведь не хотите, чтобы ваш софт работал медленнее, чем MS IIS или MS SQL Server? Кстати, этот недокументированный интерфейс не реализован ни в ReactOS, ни в Wine.


3. Важное дополнение (от 29.11.2014)



В операционных системах от Windows NT 4.0 до Windows XP медленный IOCTL_AFD_POLL. Внутренняя функция AfdIndicatePollEvent() из AFD.SYS занимает глобальную спин-блокировку и начинает перебирать все poll-контексты ядра по любому событию на любом из polled сокетов. Таким образом, сложность обработки n сокетов есть O(n^2). Исходные коды этого безобразия вы можете посмотреть в private\ntos\afd\poll.c из Windows NT 4 Source Code Leak, а для более поздних версий подтвердить с помощью дизассемблера/декомпилятора (например, IDA Pro with Hex-Rays).

Начиная с Висты, реализация AfdIndicatePollEvent() была полностью переписана, уже больше не занимает глобальную спин-блокировку, и имеет сложность O(n) для n сокетов.

Это обстоятельство делает недействительными все замеры производительности, в которых участвуют IOCTL_AFD_POLL и WSAAsyncSelect(), сделанные до выпуска Windows NT 6.0.

Именно по этой причине, начиная с Висты, в Winsock API появилась новая функция WSAPoll(), реализация которой задействует IOCTL_AFD_POLL (к сожалению, без поддержки OVERLAPPED IO). Этот интерфейс ядра стал достаточно быстрым, и поэтому его сделали публичным.

Date: 2014-11-23 06:29 pm (UTC)
From: [identity profile] alexfifer.livejournal.com
Одно (мне) непонятно: зачем вообще кому-либо может понадобиться делать высокопроизводительный сетевой ввод-вывод на Windows.

March 2024

S M T W T F S
     12
3456789
10111213141516
17181920212223
24252627282930
31      

На этой странице

Автор стиля

Развернуть

No cut tags
Page generated 2026-05-07 10:23 pm
Powered by Dreamwidth Studios