Ну или почти вся...  Я считаю, что проблема в современном интернете — это переизбыток информации разного качества. Найти материал по интересующей теме не проблема, проблема отличить хороший материал от плохого, если у вас мало опыта в данной области. Я наблюдаю картину, когда очень много обзорной информации "по верхам" (практически на уровне простого перечисления), очень мало углубленных статей и совсем нет переходных статей от простого к сложному. Тем не менее именно знание особенностей того или иного механизма и позволяет нам сделать осознанный выбор при разработке. В статье я постараюсь раскрыть то, что является фундаментальным отличием epoll от других механизмов, то что делает его уникальным, а так же привести статьи, которые просто необходимо прочитать для более глубокого осмысления возможностей и проблем epoll. Я предполагаю, что читатель знаком с epoll, по крайней мере прочел страницу man. О epoll, poll, select написано достаточно много, чтобы каждый кто разрабатывал под Linux, хоть раз о нем слышал. Когда люди говорят о epoll в основном я слышу тезис, что его "производительность выше, когда много файловых дескрипторов". Сразу хочется задать вопрос — а много это сколько? Сколько нужно соединений, а главное при каких условиях epoll начнет давать ощутимый выигрыш по производительности? Для тех, кто изучал epoll (материала достаточно много в том числе и научных статей) ответ очевиден — он лучше тогда и только тогда, когда число "ожидающих события" соединений существенно превышает число "готовых к обработке". Отметкой же количества, когда выигрыш становиться настолько существенным, что уже просто мочи нету игнорировать данный факт, считается 10к соединений [4]. Предположение о том, что большинство соединений будет ожидающими исходит из здравой логики и наблюдений за нагрузкой серверов, находящихся в активной эксплуатации. Если число активных соединений стремиться к общему количеству, никакого выигрыша не будет никакого существенного выигрыша не будет, существенный выигрыш происходит из-за того и только из-за того, что epoll возвращает только дескрипторы требующие внимания, а poll возвращает все дескрипторы, которые были добавлены для наблюдения. Очевидно, что в последнем случае мы тратим время на обход всех дескрипторов + накладные расходы по копированию массива событий из ядра. Действительно, в изначальном замере производительности, который прилагался к пачту [9], данный момент не подчеркнут и догадаться можно только по присутствию утилиты deadcon упомянутой в статье (к сожалению, код утилиты pipetest.c утерян). С другой стороны, в других источниках [6, 8] это очень сложно не заметить, так данный факт практически выпячивается. Сразу возникает вопрос, а что же теперь если не планируется обслуживать такое количество файловых дескрипторов epoll, как бы, и не нужен? Несмотря на то, что epoll изначально создавался именно для таких ситуаций [5, 8, 9], это далеко не единственное отличие epoll. Для начала разберемся в чем же отличие срабатывания по фронту (edge-triggered) от срабатывания по уровню (level-triggered), на данную тему есть очень хорошее высказывание в статье Edge Triggered Vs Level Triggered interrupts — Venkatesh Yadav : Если в level-triggered поведении epoll (так же, как и poll/select) разблокируется если дескриптор находится в заданном состоянии и будет полагать его активным пока данное состояние не будет снято, то edge-triggered разблокируется только по изменению текущего данного заказанного состояния. Это позволяет заняться обработкой события позже, а не сразу по получении (практически прямая аналогия c верхней половиной (top half) и нижней половиной (bottom half) обработчика прерывания). Конкретный пример с epoll: Level triggered И так будет продолжаться пока мы полностью не считаем или не обнулим данные из дескриптора. Edge triggered простой пример: epollet_socket.c Данный механизм сделан, чтобы предотвратить возврат epoll_wait() из-за события которое уже находится в обработке. Если в случае level при вызове epoll_wait() ядро проверяет не находится ли fd в данном состоянии, то edge пропускает данную проверку и тут же переводит вызваший процесс в состояние сна. Собственно EPOLLET это то, что делает epoll O(1) мультиплексором для событий. Небходимо пояснить насчёт EAGAIN и EPOLLET — рекомендация с EAGAIN не относиться к byte-stream, опасность в последнем случае возникает только если вы не вычитали дескриптор до конца, а новые данные не пришли. Тогда в дескрипторе будет висеть "хвост", а нового уведомления вы не получите. С accept() как раз ситуация другая, там вы обязаны продолжать пока accept() не вернет EAGAIN, только в этом случае гарантируется корректная работа. С данным свойством достаточно просто получить голодание (starvation): Таки образом EAGAIN мы получим не скоро, а можем и вообще не получить. Таким образом другие файловые дескрипторы не получают времени для обработки, а мы заняты чтением постоянно приходящих, небольших порций данных. Для того, чтобы перейти к последнему флагу необходимо понять для чего он, собственно, создавался и одна их каких проблем возникла перед разработчиками с эволюцией техники и программного обеспечения. Thundering herd problem Нас в данном случае интересует проблема распределенных по потокам accept() и read() в связке с epoll. Собственно, с блокирующимся вызовом accept() никаких проблем давно уже нет. Ядро само позаботится, что только один процесс был разблокирован по данному событию, а все входящие соединения сериализуются. А вот с epoll такой фокус не пройдет. Если у нас сделан listen() на неблокирующем сокете при установке соединения будут разбужены все epoll_wait() ожидающие событие от данного дескиптора. Конечно accept() получится сделать только одному потоку, остальные получат EAGAIN, но это напрасная трата ресурсов. Более того EPOLLET нам так же не поможет, поскольку нам неизвестно сколько именно соединений находится в очереди на подсоединение (backlog). Как мы помним при использовании EPOLLET обработка сокета должна продолжаться до возврата с кодом ошибки EAGAIN, поэтому есть шанс, что все accept() будут обработаны одним потоком, и остальным работы не достанется. И это опять приводит нас к ситуации, когда соседний поток был разбужен зря. Так же мы можем получить starvation другого рода — у нас будет загружен только один поток, а остальные не получат соединений для обработки. До версии 4.5 единственным корректным способом обработки распределенного по потокам epoll на неблокирующий listen() дескриптор с послежующим вызовом accept(), было задание флага EPOLLONESHOT, что опять приводило нас к тому, что accept() обрабатывался одновременно только в одном потоке. В кратце — в случае применения EPOLLONESHOT событие ассоциированное с конкретным дескриптором сработает только один раз, после чего необходимо заново взвести флаги с помощью epoll_ctl(). Здесь нам на помощь приходит EPOLLEXCLUSIVE и level-triggered. EPOLLEXCLUSIVE разблокирует один ожидающий epoll_wait() за раз на одно событие. Схема достаточно простая (на самом деле нет): Таким образом все обслуживание равномерно распределено по потокам. код примера: epollexclusive.c (будет работать только с версией ядра от 4.5) Получаем pre-fork модель на epoll. Такая схема хорошо применима для TCP поключений с малым временем жизни (short life-time TCP connections). А вот с read() в случае с byte-streaming, EPOLLEXCLUSIVE, как и EPOLLET нам не помогут. По понятным причим без EPOLLEXCLUSIVE мы level-triggered использовать не можем, совсем. С EPOLLEXCLUSIVE все не лучше, так как мы может получиться посылку, размазанную по потокам, к тому же с неизвестным порядком пришедших байт. C EPOLLET ситуация такая же. И здесь выходом будет EPOLLONESHOT с реинициализацией по завершению работы. Так, как только один поток будет работать с данным файловым дескриптором и буфером: Данный пункт, пожалуй, единственное в статье моё личное ИМХО. Возможность использовать указатель или число является полезным. Например с использованием указателя при использовании epoll позволяет делать трюк наподобие этого : Я думаю все знают откуда пришел данный прием. Я надеюсь, что нам удалось приоткрыть тему epoll. Тем кто желает использовать данный механизм осознанно, просто необходимо прочитать статьи в списке литературы [1, 2, 3, 5]. На основе данного материала (а еще лучше вдумчиво прочитав материалы из списка литературы) вы можете сделать многопоточный pre-fork (заблаговременное порождение процесса) lockfree (без блокировочный) сервер или пересмотреть существующие стратегии на базе особенных свойств epoll()). epoll один из уникальных механизмов, который людям, выбравшим своей стезей программирования под Linux, надо знать, так как именно они дают серьёзное преимущество над другими операционными системами), и, возможно, позволит отказаться от кросс-платформенности для конкретного данного случая (пусть будет работать только под Linux но будет делать это хорошо). Прежде чем кто-то скажет о специфичности данных флагов и моделях использования, я хочу задать вопрос: "А ничего, что мы пытаемся обсуждать специфичность для механизма, который создавался для специфичных задач изначально [9, 11]? Или у нас обслуживание даже 1к соединений вполне повседневная задача для программиста?" Я не понимаю концепцию "специфичности задачи", это мне напоминает разного рода крики про полезность и бесполезность различных преподаваемых дисциплин. Позволяя себе рассуждать таким образом, мы присваиваем себе право решать за других, какая именно информация им полезна, а какая бесполезна, при этом, заметьте, не участвуя в процессе образования в целом. Для скептиков пара ссылок: Увеличиваем производительность с помощью SO_REUSEPORT в NGINX 1.9.1 — VBart
Learning from Unicorn: the accept() thundering herd non-problem — Chris Siebenmann
Serializing accept(), AKA Thundering Herd, AKA the Zeeg Problem — Roberto De Ioris
How does epoll's EPOLLEXCLUSIVE mode interact with level-triggering? Большое спасибо Сергею (dlinyj) и Петру Овченкову за ценные дискусии, замечания и помощь! Систематический программист