- PVSM.RU - https://www.pvsm.ru -
В этой статье мы попробуем разобраться чем на практике отличается механизм epoll от портов завершения (Windows I/O Completion Port или IOCP). Это может быть интересно системным архитекторам, проектирующим высокопроизводительные сетевые сервисы или программистам, портирующим сетевой код с Windows на Linux или наоборот.
Обе эти технологии весьма эффективны для обработки большого количества сетевых соединений.
Они отличаются от других методов по следующим пунктам:
Если перефразировать всё вышесказанное, обе данные технологии созданы для разработки сетевых сервисов, обрабатывающих множество входящих соединений от клиентов. Но в то же время между ними есть существенная разница и при разработке тех же сервисов её важно знать.
Первой и наиболее важной разницей между epoll и IOCP является то, как вы получаете извещение о случившемся событии.
При использовании epoll приложение:
При использовании IOCP приложение:
Разница в типе нотификаций делает возможным (и достаточно тривиальным) эмуляцию IOCP с помощью epoll. Например, проект Wine [2] именно так и делает. Однако, проделать обратное не так просто. Даже если у вас получится — это, вероятно, приведёт к потере производительности.
Если вы планируете читать данные, то в вашем коде должен быть какой-то буфер, куда вы планируете их читать. Если вы планируете отправлять данные, то должен быть буфер с данными, готовыми к отправке.
Типичный сетевой сервис оперирует объектами соединений, который включат в себя дескрипторы и связанные с ними буферы для чтения/записи данных. Обычно эти объекты уничтожаются при закрытии соответствующего сокета. И это накладывает некоторые ограничения при использовании IOCP.
IOCP работает методом добавления в очередь запросов на чтение и запись данных, эти запросы выполняются в порядке очереди (т.е. когда-нибудь потом). В обоих случаях передаваемые буферы должны продолжать существовать до самого завершения требуемых операций. Более того, нельзя даже модифицировать данные в этих буферах во время ожидания. Это накладывает важные ограничения:
IOCP-операции также требуют передачи указателя на структуру OVERLAPPED, которая также должна продолжать существовать (и не переиспользоваться) до окончания завершения ожидаемой операции. Это означает, что, если вам необходимо одновременно читать и писать данные — вы не можете унаследоваться от структуры OVERLAPPED (часто приходящая в голову идея). Вместо этого вам нужно хранить две структуры OVERLAPPED в собственном отдельном классе, передавая одну из них в запросы на операцию чтения, а другую — в запросы на запись.
epoll не использует никаких передаваемых ему из пользовательского кода буферов, так что все эти проблемы его никак не касаются.
Добавление нового типа ожидаемых событий (например, мы ждали возможности прочитать данные из сокета, а теперь захотели ещё и получить возможность отправить их) возможно и достаточно просто и для epoll и для IOCP. epoll позволяет изменить маску ожидаемых событий (в любое время, даже из другого потока), а IOCP позволяет запустить ещё одну операцию ожидания нового типа событий.
Изменение или удаление уже ожидаемых событий, однако, отличается. epoll всё так же позволяет модифицировать условие с помощью вызова epoll_ctl (в том числе из других потоков). С IOCP всё сложнее. Если операция ввода/вывода была запланирована — её можно отменить вызовом функции CancelIo() [3]. Что хуже, вызвать эту функцию может лишь тот же поток, который запустил первоначальную операцию. Все идеи организации отдельного управляющего потока разбиваются об это ограничение. Кроме того, даже после вызова CancelIo() мы не можем быть уверены, что операция будет немедленно отменена (возможно, она уже выполняется, использует структуру OVERLAPPED и переданный буфер для чтения/записи). Нам всё равно придётся дождаться завершения операции (её результат будет возвращён функцией GetOverlappedResult()) и лишь после этого мы сможем освободить буфер.
Ещё одна проблема с IOCP в том, что как только операция была запланирована к выполнению — она уже не может быть изменена. Например, вы не можете изменить запланированный запрос ReadFile и сказать, что хотите прочитать лишь 10 байт, а не 8192. Вам нужно отменять текущую операцию и запускать новую. Это не проблема для epoll, который при запуске ожидания понятия не имеет, сколько данных вы захотите прочитать на тот момент, когда придёт нотификация о возможности чтения данных.
Некоторые реализации сетевых сервисов (связанные сервисы, FTP, p2p) требуют организации исходящих соединений. И epoll, и IOCP поддерживают неблокируемый запрос на соединение, но по-разному.
При использовании epoll код, в общем, такой же как и для select или poll. Вы создаёте неблокируемый сокет, вызываете для него connect() и ждёте нотификации о его доступности для записи.
При использовании IOCP вам нужно использовать отдельную функцию ConnectEx, поскольку вызов connect() не принимает структуру OVERLAPPED, а значит не может позже сгенерировать нотификацию об изменении состояния сокета. Так что код инициирования соединения будет отличаться не только от кода с использованием epoll, он будет отличаться даже от Windows-кода, использующего select или poll. Однако, изменения можно считать минимальными.
Что интересно, accept() работает с IOCP как обычно. Есть и функция AcceptEx, но её роль совершенно не связанная с неблокируемым соединением. Это не «неблокируемый accept», как можно было бы подумать по аналогии с connect/ConnectEx.
Часто после срабатывания какого-то события очень быстро приходят дополнительные данные. Например, мы ожидали прихода входных данных из сокета при помощи epoll или IOCP, получили событие о первых нескольких байт данных и тут же, пока мы их читали, пришла ещё сотня байт. Можно ли прочитать их без перезапуска мониторинга событий?
При использовании epoll это возможно. Вы получаете событие «что-то теперь можно прочитать» — и вы читаете из сокета всё, что можно прочитать (пока не получите ошибку EAGAIN). То же самое и с отправкой данных — получив сигнал к готовности сокета отправлять данные, вы можете писать в него что-то, пока функция записи не вернёт EAGAIN.
С IOCP это не сработает. Если вы попросили сокет прочесть или отправить 10 байт данных — именно столько и будет прочитано/отправлено (даже если уже можно было бы и больше). Для каждого следующего блока нужно делать отдельный запрос с помощью ReadFile или WriteFile, а потом ждать, пока он будет выполнен. Это может создать дополнительный уровень сложности. Рассмотрим следующий пример:
С объектами синхронизации в данном случае вообще сложно. Хорошо, если он один. Но если у нас будет 100 000 соединений и в каждом будет по какому-то объекту синхронизации — это может серьёзно ударить по ресурсам системы. А если ещё держать по 2 (на случай разделения обработки запросов на чтение и запись)? Ещё хуже.
Обычным решением здесь является создание класса менеджера соединений, который будет ответственным за вызов ReadFile или WriteFile для класса соединения. Это работает лучше, но делает код более сложным.
И epoll, и IOCP подходят (и используются на практике) для написания высокопроизводительных сетевых сервисов, способных обрабатывать большое количество соединений. Сами технологии отличаются способами обработки событий. Эти отличия столь существенны, что вряд ли стоит пытаться писать их на какой-то общей базе (количество одинакового кода будет минимально). Я несколько раз работал над попытками привести оба подхода к какому-то универсальному решению — и каждый раз полученный результат получался хуже в плане сложности, читабельности и поддержки по сравнению с двумя независимыми реализациями. От полученного универсального результата каждый раз приходилось в итоге отказываться.
При портировании кода с одной платформы на другую обычно оказывается проще портировать IOCP-код на использование epoll, чем наоборот.
Советы:
Автор: tangro
Источник [4]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/sistemnoe-programmirovanie/284242
Ссылки в тексте:
[1] GetQueuedCompletionStatus(): https://msdn.microsoft.com/ru-ru/library/windows/desktop/aa364986(v=vs.85).aspx
[2] Wine: http://www.winehq.org/
[3] CancelIo(): http://msdn.microsoft.com/en-us/library/windows/desktop/aa363791%28v=vs.85%29.aspx
[4] Источник: https://habr.com/post/415403/?utm_campaign=415403
Нажмите здесь для печати.