Pull to refresh

Comments 53

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

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

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

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


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


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


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

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

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

Верно.

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

И я использую исключительно gcc, но статья о epoll!
Расшифруйте темноте, что такое UB?
Мне кажется, что в вашей статье не хватает следующих моментов.
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 уже вызван и уже выдал новую порцию событий, на которые вы реагируете в соседних нитях пула.
Я вас не до конца понял — «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 других событий если они требуют обработки?

>> Я вас не до конца понял — «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 в этом массиве (тоже кстати с линейным временем штука).
>> Нет, я имел в виду именно вашу схему (цитата).
Это момент понятен. Да — можно.
Хотя я в данном примере просто показывал разницу в поведении edge и level в принципе — не только касательно epoll.

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

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

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

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

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

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

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

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


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


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

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

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

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

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

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


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

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

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

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

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


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

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 активных коннектов.

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
Разобрался, что вы имеете ввиду и честно говоря не нашёл каких-либо противоречий или расхождений с тем, что я рассказал.
Для тех, кто изучал epoll (материала достаточно много в том числе и научных статей) ответ очевиден — он лучше тогда и только тогда, когда число "ожидающих события" соединений существенно превышает число "готовых к обработке".

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

Вы правы.

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

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


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

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

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

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


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

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

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

По поводу PVS ничего сказать не могу я не интересуюсь программированием под Windows и проприетарными продуктами.
А теперь уже растерялся я — а причем тут llvm и ядро linux?
(type *)0)->member


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

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


Но в общем и целом, я не думаю, что это место где стоит обсуждать код ядра линукс. Кто хочет пускай пишет на kernel mail lists, а потом напишет нам, что ему ответили.
UFO just landed and posted this here

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


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


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


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

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

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

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

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

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

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

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

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

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

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


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

Тем более, что EPOLLET был добавлен позже, а не одновременно с epoll.
В статью стоит добавить информацию про 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.
Нужно больше материала…

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

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

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

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

events[0].data.ptr это ведь произвольный указатель, который может задать пользователь. Зачем указывать на epoll_event и заморачиваться с container_of, если можно сразу положить туда указатель на epoll_client.
Only those users with full accounts are able to leave comments. Log in, please.