Вся правда о linux epoll

    Ну или почти вся...



    Я считаю, что проблема в современном интернете — это переизбыток информации разного качества. Найти материал по интересующей теме не проблема, проблема отличить хороший материал от плохого, если у вас мало опыта в данной области. Я наблюдаю картину, когда очень много обзорной информации "по верхам" (практически на уровне простого перечисления), очень мало углубленных статей и совсем нет переходных статей от простого к сложному. Тем не менее именно знание особенностей того или иного механизма и позволяет нам сделать осознанный выбор при разработке.


    В статье я постараюсь раскрыть то, что является фундаментальным отличием epoll от других механизмов, то что делает его уникальным, а так же привести статьи, которые просто необходимо прочитать для более глубокого осмысления возможностей и проблем epoll.


    Anyone can wield an axe, but it takes a true warrior to make it sing melees melody.

    Я предполагаю, что читатель знаком с epoll, по крайней мере прочел страницу man. О epoll, poll, select написано достаточно много, чтобы каждый кто разрабатывал под Linux, хоть раз о нем слышал.


    Многа fd


    Когда люди говорят о epoll в основном я слышу тезис, что его "производительность выше, когда много файловых дескрипторов".


    Сразу хочется задать вопрос — а много это сколько? Сколько нужно соединений, а главное при каких условиях epoll начнет давать ощутимый выигрыш по производительности?


    Для тех, кто изучал epoll (материала достаточно много в том числе и научных статей) ответ очевиден — он лучше тогда и только тогда, когда число "ожидающих события" соединений существенно превышает число "готовых к обработке". Отметкой же количества, когда выигрыш становиться настолько существенным, что уже просто мочи нету игнорировать данный факт, считается 10к соединений [4].


    Предположение о том, что большинство соединений будет ожидающими исходит из здравой логики и наблюдений за нагрузкой серверов, находящихся в активной эксплуатации.


    Если число активных соединений стремиться к общему количеству, никакого выигрыша не будет никакого существенного выигрыша не будет, существенный выигрыш происходит из-за того и только из-за того, что epoll возвращает только дескрипторы требующие внимания, а poll возвращает все дескрипторы, которые были добавлены для наблюдения.


    Очевидно, что в последнем случае мы тратим время на обход всех дескрипторов + накладные расходы по копированию массива событий из ядра.


    Действительно, в изначальном замере производительности, который прилагался к пачту [9], данный момент не подчеркнут и догадаться можно только по присутствию утилиты deadcon упомянутой в статье (к сожалению, код утилиты pipetest.c утерян). С другой стороны, в других источниках [6, 8] это очень сложно не заметить, так данный факт практически выпячивается.


    Сразу возникает вопрос, а что же теперь если не планируется обслуживать такое количество файловых дескрипторов epoll, как бы, и не нужен?


    Несмотря на то, что epoll изначально создавался именно для таких ситуаций [5, 8, 9], это далеко не единственное отличие epoll.


    EPOLLET


    Для начала разберемся в чем же отличие срабатывания по фронту (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


    • дескриптор добавлен в epoll с флагом EPOLLIN
    • epoll_wait() блокируется на ожидании события
    • пишем в файловый дескриптор 19 байт
    • epoll_wait() разблокируется с событием EPOLLIN
    • мы ничего не делаем с пришедшими данными
    • epoll_wait() опять разблокируется с событием EPOLLIN

    И так будет продолжаться пока мы полностью не считаем или не обнулим данные из дескриптора.


    Edge triggered


    • дескриптор добавлен в epoll с флагами EPOLLIN | EPOLLET
    • epoll_wait() блокируется на ожидании события
    • пишем в файловый дескриптор 19 байт
    • epoll_wait() разблокируется с событием EPOLLIN
    • мы ничего не делаем с пришедшими данными
    • epoll_wait() блокируется в ожидании нового события
    • пишем в файловый дескриптор еще 19 байт
    • epoll_wait() разблокируется с новым событием EPOLLIN
    • epoll_wait() блокируется в ожидании нового события

    простой пример: epollet_socket.c


    Данный механизм сделан, чтобы предотвратить возврат epoll_wait() из-за события которое уже находится в обработке.


    Если в случае level при вызове epoll_wait() ядро проверяет не находится ли fd в данном состоянии, то edge пропускает данную проверку и тут же переводит вызваший процесс в состояние сна.


    Собственно EPOLLET это то, что делает epoll O(1) мультиплексором для событий.


    Небходимо пояснить насчёт EAGAIN и EPOLLET — рекомендация с EAGAIN не относиться к byte-stream, опасность в последнем случае возникает только если вы не вычитали дескриптор до конца, а новые данные не пришли. Тогда в дескрипторе будет висеть "хвост", а нового уведомления вы не получите. С accept() как раз ситуация другая, там вы обязаны продолжать пока accept() не вернет EAGAIN, только в этом случае гарантируется корректная работа.


        // TCP socket (byte stream)
        // читаем fd возвращенный с событием EPOLLIN в режиме срабатывания по фронту
        int len = read(fd, buffer, BUFFER_LEN);
        if(len < BUFFER_LEN) {
            // все хорошо
        } else {
            // нет гарантии что не осталось данных в дескрипторе
            // если что-то осталось то мы останемся висеть на epoll_wait, 
            // если не придут новые данные
        }

        // accept
        // читаем listenfd возвращенный с событием EPOLLIN в режиме срабатывания по фронту
        event.events = EPOLLIN | EPOLLERR;
        epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event);
        sleep(5); // за это время к нам поключилось >1 клиентов
        // плохой сценарий 
        while(epoll_wait()) {
            newfd = accept(listenfd, ...); // принимаем подключение от первого клиента
            // все сколько бы не поключилось далее клентов 
            // из epoll_wait мы событий от listenfd больше не получим
        }
        // хороший сценарий
        while(epoll_wait()) {
            while((newfd = accept(...)) > 0)
            {
                // делаем что-нибудь полезное
            }
            if(newfd == -1 && errno = EAGAIN) 
            {
                // все хорошо состояние дескриптора было сброшено
                // мы получим уведомление на следующем соединении
            }
        }

    С данным свойством достаточно просто получить голодание (starvation):


    • пакеты приходят в дескриптор
    • читаем пакеты в буфер
    • приходит еще порция пакетов
    • читаем пакеты в буфер
    • приходит еще небольшая порция
    • ...

    Таки образом EAGAIN мы получим не скоро, а можем и вообще не получить.


    Таким образом другие файловые дескрипторы не получают времени для обработки, а мы заняты чтением постоянно приходящих, небольших порций данных.


    thundering nerd herd


    Для того, чтобы перейти к последнему флагу необходимо понять для чего он, собственно, создавался и одна их каких проблем возникла перед разработчиками с эволюцией техники и программного обеспечения.


    Thundering herd problem


    Проблема громоподобного стада

    Представьте огромное количество процессов, ждущих события. Если событие наступает, их всех разбудят и начнётся борьба за ресурсы, хотя требуется только один процесс, который займётся дальнейшей обработкой наступившего события. Остальные процессы снова будут спать.

    ИТ терминология — Василий Алексеенко

    Нас в данном случае интересует проблема распределенных по потокам accept() и read() в связке с epoll.


    accept


    Собственно, с блокирующимся вызовом accept() никаких проблем давно уже нет. Ядро само позаботится, что только один процесс был разблокирован по данному событию, а все входящие соединения сериализуются.


    А вот с epoll такой фокус не пройдет. Если у нас сделан listen() на неблокирующем сокете при установке соединения будут разбужены все epoll_wait() ожидающие событие от данного дескиптора.


    Конечно accept() получится сделать только одному потоку, остальные получат EAGAIN, но это напрасная трата ресурсов.


    Более того EPOLLET нам так же не поможет, поскольку нам неизвестно сколько именно соединений находится в очереди на подсоединение (backlog). Как мы помним при использовании EPOLLET обработка сокета должна продолжаться до возврата с кодом ошибки EAGAIN, поэтому есть шанс, что все accept() будут обработаны одним потоком, и остальным работы не достанется.


    И это опять приводит нас к ситуации, когда соседний поток был разбужен зря.


    Так же мы можем получить starvation другого рода — у нас будет загружен только один поток, а остальные не получат соединений для обработки.


    EPOLLONESHOT


    До версии 4.5 единственным корректным способом обработки распределенного по потокам epoll на неблокирующий listen() дескриптор с послежующим вызовом accept(), было задание флага EPOLLONESHOT, что опять приводило нас к тому, что accept() обрабатывался одновременно только в одном потоке.


    В кратце — в случае применения EPOLLONESHOT событие ассоциированное с конкретным дескриптором сработает только один раз, после чего необходимо заново взвести флаги с помощью epoll_ctl().


    EPOLLEXCLUSIVE


    Здесь нам на помощь приходит EPOLLEXCLUSIVE и level-triggered.


    EPOLLEXCLUSIVE разблокирует один ожидающий epoll_wait() за раз на одно событие.


    Схема достаточно простая (на самом деле нет):


    • У нас N потоков, ожидающих событие на подключение
    • К нам подсоединяется первый клиент
    • Поток 0 разброкируется и начнет обработку, остальные потоки останутся заблокированными
    • К нам подсоединяется второй клиент, если поток 0 все еще занят обработкой, то разблокируется поток 1
    • Продолжаем далее пока не исчерпан пул потоков (никто не ожидает события на epoll_wait())
    • К нам подсоединяется очередной клиент
    • И его обработку получит первый поток, который вызовет epoll_wait()
    • Обработку второго клиента получит следующий поток, который вызовет epoll_wait()

    Таким образом все обслуживание равномерно распределено по потокам.


    $ ./epollexclusive --help  
        -i, --ip=ADDR specify ip address  
        -p, --port=PORT specify port  
        -n, --threads=NUM specify number of threads to use # количество потоков сервера - клиенты n*8
        -t, --thunder not adding EPOLLEXCLUSIVE # с этим флагом воспроизведется thunder herd
        -h, --help prints this message
    $ sudo  taskset -c 0-7 ./epollexclusive -i 10.56.75.201 -p 40000 -n 8 2>&1

    код примера: epollexclusive.c (будет работать только с версией ядра от 4.5)


    Получаем pre-fork модель на epoll. Такая схема хорошо применима для TCP поключений с малым временем жизни (short life-time TCP connections).


    read


    А вот с read() в случае с byte-streaming, EPOLLEXCLUSIVE, как и EPOLLET нам не помогут.


    По понятным причим без EPOLLEXCLUSIVE мы level-triggered использовать не можем, совсем. С EPOLLEXCLUSIVE все не лучше, так как мы может получиться посылку, размазанную по потокам, к тому же с неизвестным порядком пришедших байт.


    C EPOLLET ситуация такая же.


    И здесь выходом будет EPOLLONESHOT с реинициализацией по завершению работы. Так, как только один поток будет работать с данным файловым дескриптором и буфером:


    • дескриптор добавлен в epoll с флагами EPOLLONESHOT | EPOLLET
    • ждем на epoll_wait()
    • читаем из сокета в буфер пока read() не вернет EAGAIN
    • пере инициализируем с флагами EPOLLONESHOT | EPOLLET

    struct epoll_event


    typedef  union  epoll_data {
        void *ptr;
        int  fd;
        uint32_t u32;
        uint64_t u64;
    } epoll_data_t;
    
    struct  epoll_event {
        uint32_t events; /* Epoll  events */
        epoll_data_t  data; /* User  data  variable */
    };

    Данный пункт, пожалуй, единственное в статье моё личное ИМХО. Возможность использовать указатель или число является полезным. Например с использованием указателя при использовании epoll позволяет делать трюк наподобие этого :


    #define  container_of(ptr, type, member) ({ \
        const  typeof( ((type *)0)->member ) *__mptr = (ptr); \
        (type  *)( (char *)__mptr - offsetof(type,member) );})
    
    struct  epoll_client {
        /** some  usefull  associated  data...*/
        struct  epoll_event  event;
    };
    
    struct  epoll_client* to_epoll_client(struct  epoll_event* event)
    {
        return  container_of(event, struct  epoll_client, event);
    }
    
    struct  epoll_client  ec;
    
    ...
    epoll_ctl(efd, EPOLL_CTL_ADD, fd, &ec.e);
    ...
    
    epoll_wait (efd, events, 1, -1);
    struct  epoll_client* ec_ = to_epoll_client(events[0].data.ptr);

    Я думаю все знают откуда пришел данный прием.


    Заключение


    Я надеюсь, что нам удалось приоткрыть тему 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?


    Список литературы


    1. Select is fundamentally broken — Marek
    2. Epoll is fundamentally broken 1/2 — Marek
    3. Epoll is fundamentally broken 2/2 — Marek
    4. The C10K problem — Dan Kegel
    5. Poll vs Epoll, once again — Jacques Mattheij
    6. epoll — I/O event notification facility — The Mann
    7. The method to epoll’s madness — Cindy Sridharan

    Benchmarks


    1. https://www.kernel.org/doc/ols/2004/ols2004v1-pages-215-226.pdf
    2. http://lse.sourceforge.net/epoll/index.html
    3. https://mvitolin.wordpress.com/2015/12/05/endurox-testing-epollexclusive-flag/

    Эволюция epoll


    1. https://lwn.net/Articles/13918/
    2. https://lwn.net/Articles/520012/
    3. https://lwn.net/Articles/520198/
    4. https://lwn.net/Articles/542629/
    5. https://lwn.net/Articles/633422/
    6. https://lwn.net/Articles/637435/

    Постскриптум


    Большое спасибо Сергею (dlinyj) и Петру Овченкову за ценные дискусии, замечания и помощь!

    Поделиться публикацией

    Похожие публикации

    Комментарии 52

      +1
      Шикарная статья. Очень хорошо раскрыто всё, и пример просто золотой. И спасибо за спасибо :). Был рад помочь!
        –2
        пример просто золотой

        Классные примеры с неопределенным поведением. Напомнило мне недавнюю статью про книгу о С, где автор с радостью делится UB со своими читателями, но он сам не понимает, что твороит.

          0
          Думаю будет очень полезно разобрать вам данный пример, чтобы вы показали читателям его недостаток, для лучшего понимания.
            +4

            Да, конечно. Проблема в данном участке кода: (type *)0)->member. Макрос container_of был скопирован из include/linux/kernel.h, то есть из ядра линукса. Дело в том, что в ядре не очень любят стандарт языка С и позволяют себе всякие вольности. Сам Линус, мягко говоря, не очень любит некоторые положения стандарта, и код ядра содержит множество преднамеренных UB.


            Как же это работает? Дело в том, что ядро собирают gcc с определенными флагами, существует договоренность закрывать глаза на некоторые UB.


            Проблема ли это? Несомненно. UB — это всегда проблемы, которые приводят к уязвимостям. И если в ядре линукса конкретный макрос работает из-за определенного компилятора и флагов сборки, то у читателя данной статьи всё сломается. Или не сломается. Будет зависеть от фаз луны, взмаха крыльев бабочки на другом материке и от положения звезд.


            Что дозволено Юпитеру, не дозволено быку.

              +3

              Да нет, там все не так печально, там же написано typeof( ((type *)0)->member ). Выражение ((type *)0)->member нужно только для определения типа, само по себе оно никогда не исполняется — а потому и не может приводить к UB.

                +1

                typeof — это нестандартное расширение GCC, которое собранное другим компилятором может делать все что угодно.
                Поэтому соглашусь с @humburg — такой код не стоит использовать в своих проектах.

                  +1
                  Верно.

                  А ядро у нас собирается исключтельно с помощью gcc =).

                  И я использую исключительно gcc, но статья о epoll!
                0
                Расшифруйте темноте, что такое UB?
        +1
        Мне кажется, что в вашей статье не хватает следующих моментов.
        1. API epoll — thread safe
        2. Мултиплексинг epoll: Edge (взведённый флаг EPOLLET), предназначен в первую очередь для создания дерева epoll, где дескриптор edge-евого epoll слушается верним epoll, а вот симулировать поведение poll можно и в level-triggered с помощью EPOLLONESHOT.

        На практике, epoll многократно более выгоден в следующем кейсе:
        1. Вы можете с помощью epoll_ctl с EPOLL_CTL_ADD, добавить на прослушку только однажды. Следите за руками. Например добавим 10к сокетов на прослушку.
        2. При этом вы не обязаны передавать и самостоятельно формировать массив сокетов для каждого epoll_wait, вместо этого вы подсовываете в epoll_wait массив длинной в 1000 элементов. И если они все 1000 одновременно активны, или даже 2000 активны, вы получите порцию не превышающую событий для 1000 дескрипторов за раз.
        3. Обработав эту порцию и с использованием EPOLLONESHOT, вы в следующую итерацию опять получите не более 1000 событий.
        4. При этом между пунтками 2 и 3 там где вы обрабатываете события, вы просто добавляете epoll_ctl с опцией EPOLL_CTL_MOD для обработанного сокета и с необходиыми флагами (EPOLLIN/EPOLLOUT).
        5. Если код однопоточный, следующей итерацией опять вызываем epoll_wait. Если многопоточный, то возможно epoll_wait уже вызван и уже выдал новую порцию событий, на которые вы реагируете в соседних нитях пула.
          0
          Я вас не до конца понял — «API epoll — thread safe» это в каком смысле?

          >> а вот симулировать поведение poll можно и в level-triggered с помощью EPOLLONESHOT.
          Наверное вы имели ввиду в edge-triggered + EPOLLONESHOT + epoll_mod. Поведение epoll в level-triggered изначально проектировалось с совместимостью с поведением select/poll.

          >> При этом вы не обязаны передавать и самостоятельно формировать массив сокетов для каждого epoll_wait, вместо этого вы подсовываете в epoll_wait массив длинной в 1000 элементов. И если они все 1000 одновременно активны, или даже 2000 активны, вы получите порцию не превышающую событий для 1000 дескрипторов за раз.
          >> 3. Обработав эту порцию и с использованием EPOLLONESHOT, вы в следующую итерацию опять получите не более 1000 событий.

          Вообще говоря вы получите не больше чем maxevents, указанных при вызове в epoll_wait (но об этом написано в man), я не понял при чем тут именно EPOLLONESHOT? Или вы имеете ввиду, что мы получих 1000 других событий если они требуют обработки?

            0
            >> Я вас не до конца понял — «API epoll — thread safe» это в каком смысле?

            В том смысле что вызовы epoll_ctl и epoll_wait одновременно не ведут к data race.

            >> Наверное вы имели ввиду в edge-triggered + EPOLLONESHOT + epoll_mod. Поведение epoll в level-triggered изначально проектировалось с совместимостью с поведением select/poll.

            Нет, я имел в виду именно вашу схему (цитата):
            Level triggered

            дескриптор добавлен в epoll с флагом EPOLLIN
            epoll_wait() блокируется на ожидании события
            пишем в файловый дескриптор 19 байт
            epoll_wait() разблокируется с событием EPOLLIN
            мы ничего не делаем с пришедшими данными
            epoll_wait() опять разблокируется с событием EPOLLIN


            >> Вообще говоря вы получите не больше чем maxevents,

            Я с API спорить не буду (и как бы нигде ему не противоречил), я конкретный кейс привёл, извиняюсь, не чётко пояснил, в чём преимущество над poll: отсутствие менеджмента памяти для массивов с дескрипторами и вообще отсутствие итерирования по всем элементам этого массива для определения факта события. При возврате из epoll_wait вы всегда точно знаете сколько событий в подсунутом массиве (меньше равно maxevents, да). Также отсутствие в собственном коде контроля за временем жизни fd в этом массиве (тоже кстати с линейным временем штука).
              0
              >> Нет, я имел в виду именно вашу схему (цитата).
              Это момент понятен. Да — можно.
              Хотя я в данном примере просто показывал разницу в поведении edge и level в принципе — не только касательно epoll.

              >> Также отсутствие в собственном коде контроля за временем жизни fd в этом массиве (тоже кстати с линейным временем штука).

              Вот кстати этот момент я в статье не стал затрагивать, поточе что там не все так просто. Да действительно согласно документации при вызове close дескриптор автоматически удалится из отслеживаемых с другой стороны, при специфичных случаях: fork, dup, etc… его необходимо удалять вручную ДО! закрытия.
            0
            Может пример у вас завался для лучшего понимания?
              0
              С примером не очень просто. И не на чистом C. Класс wrapper над epoll. Воркер использующий этот класс (см метод execute).

              Ну и после завершения чтения ePoll::mod_in() используется. При этом и ePoll::mod_in() и epoll_wait и ePoll::del(), вызываются параллельно и потокобезопасны.
                0

                В объеме копируемых данных kernel <-> user, между poll и epoll разница есть, даже если все декрипторы — "активные".
                Ведь при каждом вызове epoll_wait в ядро не копируется массив всех дескрипторов, а при каждом вызове poll копируется.

                  0
                  Да!
                    0
                    Дело в том, что данный момент на таких объемах уже не играет существенной роли (посмотрите 5, 1 из вписка литературы) и это надо понимать.

                    Была инересная статья (канула в лета к сожаление) Zed Shaw: «poll, epoll, science, and superpoll» with R, где с цифрами в руках утверждалось, что poll лучше чем epoll.
                      0
                      > Была инересная статья (канула в лета к сожаление) Zed Shaw: «poll, epoll, science, and superpoll» with R

                      На собственном опыте убедилйся, что двойной менеджмент памяти, и копирование полного массива в ядро и обратно массива для poll 100 раз в секунду, плюс линейная обработка массива с поиском событий, это замечательный кейс для рефакторинга и оптимизации.

                        0
                        Бесспорно.
                        0

                        https://jacquesmattheij.com/poll-vs-epoll-once-again/ — там и исходники канули в лету ( Поэтому, затрудняюсь прокомментировать цифры из статьи.


                        А при 10к дескрипторов, у poll накладные расходы: как минимум копирование 80КБ из user в kernel на каждый вызов. Если честно, с трудом верится, что это не играет существенной роли.


                        Вообще, статья полезная, но было бы очень круто, если бы вы приложили бенчмарки с исходниками и графики подтверждающие тезисы.

                          0
                          >> А при 10к дескрипторов, у poll накладные расходы: как минимум копирование 80КБ из user в kernel на каждый вызов. Если честно, с трудом верится, что это не играет существенной роли.

                          Всего один syscall =).

                          >> Вообще, статья полезная, но было бы очень круто, если бы вы приложили бенчмарки с исходниками и графики подтверждающие тезисы.

                          Наверное было бы, но это синтетические тесты, я просто знаю что цифр сильно отличных от допустим www.kernel.org/doc/ols/2004/ols2004v1-pages-215-226.pdf, я не получу. А там как раз сравнение poll/select первым идет.
                            0

                            Возможно не внимательно прочитал, но в статье не тестировался кейс "10000 активных коннектов", а только кейсы "N-активных коннектов" vs "N-активных коннектов+10000 айдл".
                            Сходу, информации о том, чему равно N — не нашел.


                            А если N было не очень велико, то естественно, заметной разницы между poll и epoll нет.

                              0
                              Самые первые три графика.
                                0

                                Так сколько активных коннектов то было на тестах из первых трех графиках? Не нашел инфы об этом в статье.

                                  0
                                  Все коннекты активные!

                                  Figure 3: µserver performance on one byte workload with no idle connections


                                  Так, что вначале все более ли менее ровно.
                                    0

                                    define (все) :)?


                                    судя по


                                    This is followed by a comparison of the performance of the event dispatch mechanisms when the server is pre-loaded with 10,000 idle connections.


                                    В первых тестах не было 10,000 активных коннектов.

                                      0
                                      In this section we first compare the throughput achieved when using level-triggered epoll with that observed when using select and poll under both the one-byte and SPECweb99-like workloads with no idle connections.

                                      :-D
                        0
                        Разобрался, что вы имеете ввиду и честно говоря не нашёл каких-либо противоречий или расхождений с тем, что я рассказал.
                          0
                          Для тех, кто изучал epoll (материала достаточно много в том числе и научных статей) ответ очевиден — он лучше тогда и только тогда, когда число "ожидающих события" соединений существенно превышает число "готовых к обработке".

                          Вот эта формулировка, на мой взгляд, не корректна.

                            0
                            Вы правы.

                            никакого выигрыша не будет никакого существенного выигрыша не будет
                    0

                    Недавно наткнулся на то, что epoll в режиме level triggered может возвращать что в сокет можно писать, а send при этом — возвращать EAGAIN. Это может происходить из-за того, что в системе начала заканчиваться память под tcp-буферы (sysctl net.ipv4.tcp_mem), но ещё не закончилась. При этом приложение обычно повторяет epoll_wait и ситуация повторяется. Это приводит к 100% загрузке CPU.


                    При этом send теоретически может возвращать ENOBUFS или ENOMEM, но возвращает почему-то именно EAGAIN.

                      0
                      Вы очевидно, же говорите об обработке неблокирующих сокетов, и EAGAIN в данном случае как и EWOULDBLOCK одно и тоже. Только EWOULDBLOCK более говорящее описание того-же самого кейса.
                        0

                        Я больше про то, что epoll может сказать про сокет "в него можно писать и он не заблокируется", а при попытке писать получить EAGAIN (что то же самое, что EWOULDBLOCK). При этом мне не известны приложения, которые как-то обрабатывают эту ситуацию, не впадая в бесконечный цикл epoll_wait->send->epoll_wait->… со 100% загрузкой CPU

                          0
                          Да согласен. С неблокирующими сокетами граблей много можно поймать. Техник митигации для данного кейса я представить не могу. Но тут либо само всё рассoсётся, — когда ресурсы освободятся, либо сокеты по таймауту начнут отваливаться (если без KEEP ALIVE), либо клиентам надоест ждать ответа, либо админ сервер рестартанёт…
                      0
                      #define  container_of(ptr, type, member) ({ \
                      const  typeof( ((type *)0)->member ) *__mptr = (ptr); \
                      (type  *)( (char *)__mptr - offsetof(type,member) );})


                      Я думаю все знают откуда пришел данный прием.

                      Дайте угадаю, этот прием пришел из статьи про undefined behavior от PVS-Studio, где человеческим языком говорится, что так делать не стоит.

                        –4
                        Нет я имел ввиду вот это lwn.net/Articles/336255

                        По поводу PVS ничего сказать не могу я не интересуюсь программированием под Windows и проприетарными продуктами.
                          0

                          Я даже растерялся. При чем тут undefined behavior и проприетарщина? Вы никогда не слышали про UB, не читали цикл статей What Every C Programmer Should Know About Undefined Behavior?

                            –1
                            А теперь уже растерялся я — а причем тут llvm и ядро linux?
                              +2
                              (type *)0)->member


                              Вот это товарища смутило.
                                –1
                                Посмотрел — даже автор там пишет — цитирую:

                                Естественно я решил изучить данную тему подробнее. Но, если честно, в результате я только ещё больше запутался. Поэтому я не дам вам точный ответ, можно так писать или нет. Я только предоставлю некоторые ссылки и поделюсь своим мнением.


                                Но в общем и целом, я не думаю, что это место где стоит обсуждать код ядра линукс. Кто хочет пускай пишет на kernel mail lists, а потом напишет нам, что ему ответили.
                        • НЛО прилетело и опубликовало эту надпись здесь
                          0

                          Возвращаясь к прошлому холивару на пустом месте (https://habr.com/company/infopulse/blog/415259/#comment_18815187) могу сказать следующее:


                          1. EPOLLET — это никакое не преимущество epoll над select/poll, а необходимость, обусловленная архитектурой epoll. При использовании select/poll аналогичную функциональность можно без труда получить самостоятельно (просто не передавая не до конца обработанные события на вход).


                          2. EPOLLEXCLUSIVE и EPOLLONESHOT — это и правда фичи epoll, которые значительно упрощают многопоточную работу с сокетами.


                            0
                            >> необходимость, обусловленная архитектурой epoll
                            Вот тут поподробнее пожалуйста, что вы имеете ввиду.

                            >> При использовании select/poll аналогичную функциональность можно без труда получить самостоятельно (просто не передавая не до конца обработанные события на вход).

                            Разница межде level и edge находиться в ядре. Если c level ядро осуществляет обход каждого дескриптора при вызове epoll_wait, чтобы выснить готов ли дескриптор к обработке. То в edge он пропускает проверку и сразу отправляет вызвавший процесс спать.

                            Внешние проявления можно получить безусловно. Но кроме epoll edge умеет только AIO, который кстати вообще обычно никем не рассматривается, даже в обзорных статьях.
                              0
                              Ну разумеется poll нельзя научить работать абсолютно так же как epoll, особенно с той же асимптотикой. Но нельзя же говорить что epoll лучше poll просто потому что он epoll!

                              Фича EPOLLET решает ровно одну задачу — дает возможность отложить реакцию на событие и продолжить ждать остальные события. Но poll умеет так же без специальных флагов.
                                0
                                >> Ну разумеется poll нельзя научить работать абсолютно так же как epoll, особенно с той же асимптотикой. Но нельзя же говорить что epoll лучше poll просто потому что он epoll!

                                Так… А с чего вы решили, что я в статье утверждаю обратное? Я вроде специально акцент сделал на изначальное предназначение epoll и с какой целью он создавался, а потом уже перешел к дополнительным плюшкам.

                                Здесь надо акцентировать на том, что epoll по-крайней мере не хуже poll, а в некоторых случаях лучше.

                                >> Фича EPOLLET решает ровно одну задачу — дает возможность отложить реакцию на событие и продолжить ждать остальные события. Но poll умеет так же без специальных флагов.

                                Именно поэтому я указал на отличие в обработке в ядре, чтобы было еще одно отличие. Собственно из-за это его называют O(1) I/O multiplexer…

                                Но если уж быть дотошным, то есть отличия и в правилах merge'a для событий разных типов.
                                  0

                                  Вы не в статье утверждали обратное, а в том комментарии:


                                  из статьи никак не следует, что epoll лучше, основное отличие не упомянуто (edge-triggered EPOLLET)
                                    0
                                    Принимается.

                                    Тем более, что EPOLLET был добавлен позже, а не одновременно с epoll.
                            0
                            del
                              0
                              В статью стоит добавить информацию про epoll в сравнении с kqueue. Плюсы и минусы. Epoll часто сравнивают с kqueue и обычно выводы не в пользу epoll.

                              people.eecs.berkeley.edu/~sangjin/2012/12/21/epoll-vs-kqueue.html
                              idea.popcount.org/2017-02-20-epoll-is-fundamentally-broken-12
                              news.ycombinator.com/item?id=3028687

                              In terms of performance, the epoll design has a weakness; it does not support multiple updates on the interest set in a single system call. When you have 100 file descriptors to update their status in the interest set, you have to make 100 epoll_ctl() calls.


                              Another issue, which is more important in my opinion, is the limited scope of epoll. As it was designed to improve the performance of select()/epoll(), but for nothing more, epoll only works with file descriptors. What is wrong with this?

                              It is often quoted that “In Unix, everything is a file”. It is mostly true, but not always. For example, timers are not files. Signals are not files. Semaphores are not files. Processes are not files. (In Linux,) Network devices are not files. There are many things that are not files in UNIX-like operating systems. You cannot use select()/poll()/epoll for event multiplexing of those “things”. Typical network servers manage various types of resources, in addition to sockets. You would probably want monitor them with a single, unified interface, but you cannot. To work around this problem, Linux supports many supplementary system calls, such as signalfd(), eventfd(), and timerfd_create(), which transforms non-file resources to file descriptors, so that you can multiplex them with epoll(). But this does not look quite elegant… do you really want a dedicated system call for every type of resource?
                              In kqueue, the versatile struct kevent structure supports various non-file events. For example, your application can get a notification when a child process exits (with filter = EVFILT_PROC, ident = pid, and fflags = NOTE_EXIT). Even if some resources or event types are not supported by the current kernel version, those are extended in a future kernel version, without any change in the API.
                                0
                                Нужно больше материала…

                                Если серьезно, я не возьмусь сравнивать, от слова совсем. Это две различные операционные системы, причем я ничего не знаю о FreeBSD.

                                Про everything is a file (but not really) — это правда, если начинаешь использовать epoll всегда и везде, как замену select, то точно заметишь. Можно попробовать перефразировать «everything is a file descriptor» — но это тоже не будет правдой.

                                Для автора сравнения [1] kqueue статьи важен epoll_ctl — а для меня никогда не было bottleneck, поэтому не берусь судить.

                                Так что мы можем сравнить только удобство пользования в каком то смысле, но пускай это делают те кто интересуется кросс-платформенной разработкой.
                                +1
                                Пример с container_of непонятен (либо он неправильный, либо я не так прочитал).

                                events[0].data.ptr это ведь произвольный указатель, который может задать пользователь. Зачем указывать на epoll_event и заморачиваться с container_of, если можно сразу положить туда указатель на epoll_client.

                                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                Самое читаемое