Комментарии 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 — такой код не стоит использовать в своих проектах.
Неопределённое поведение
Undefined behavior
Если кратко — это запрещенная ситуация, о которой компилятору разрешается не думать.
1. API epoll — thread safe
2. Мултиплексинг epoll: Edge (взведённый флаг EPOLLET), предназначен в первую очередь для создания дерева epoll, где дескриптор edge-евого epoll слушается верним epoll, а вот симулировать поведение poll можно и в level-triggered с помощью EPOLLONESHOT.
На практике, epoll многократно более выгоден в следующем кейсе:
1. Вы можете с помощью epoll_ctl с EPOLL_CTL_ADD, добавить на прослушку только однажды.
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 уже вызван и уже выдал новую порцию событий, на которые вы реагируете в соседних нитях пула.
>> а вот симулировать поведение 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 других событий если они требуют обработки?
В том смысле что вызовы 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… его необходимо удалять вручную ДО! закрытия.
Ну и после завершения чтения ePoll::mod_in() используется. При этом и ePoll::mod_in() и epoll_wait и ePoll::del(), вызываются параллельно и потокобезопасны.
В объеме копируемых данных kernel <-> user, между poll и epoll разница есть, даже если все декрипторы — "активные".
Ведь при каждом вызове epoll_wait в ядро не копируется массив всех дескрипторов, а при каждом вызове poll копируется.
Была инересная статья (канула в лета к сожаление) Zed Shaw: «poll, epoll, science, and superpoll» with R, где с цифрами в руках утверждалось, что poll лучше чем epoll.
На собственном опыте убедилйся, что двойной менеджмент памяти, и копирование полного массива в ядро и обратно массива для poll 100 раз в секунду, плюс линейная обработка массива с поиском событий, это замечательный кейс для рефакторинга и оптимизации.
https://jacquesmattheij.com/poll-vs-epoll-once-again/ — там и исходники канули в лету ( Поэтому, затрудняюсь прокомментировать цифры из статьи.
А при 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 активных коннектов.
Для тех, кто изучал epoll (материала достаточно много в том числе и научных статей) ответ очевиден — он лучше тогда и только тогда, когда число "ожидающих события" соединений существенно превышает число "готовых к обработке".
Вот эта формулировка, на мой взгляд, не корректна.
Недавно наткнулся на то, что epoll в режиме level triggered может возвращать что в сокет можно писать, а send при этом — возвращать EAGAIN. Это может происходить из-за того, что в системе начала заканчиваться память под tcp-буферы (sysctl net.ipv4.tcp_mem), но ещё не закончилась. При этом приложение обычно повторяет epoll_wait и ситуация повторяется. Это приводит к 100% загрузке CPU.
При этом send теоретически может возвращать ENOBUFS или ENOMEM, но возвращает почему-то именно EAGAIN.
Я больше про то, что epoll может сказать про сокет "в него можно писать и он не заблокируется", а при попытке писать получить EAGAIN (что то же самое, что EWOULDBLOCK). При этом мне не известны приложения, которые как-то обрабатывают эту ситуацию, не впадая в бесконечный цикл epoll_wait->send->epoll_wait->… со 100% загрузкой CPU
#define container_of(ptr, type, member) ({ \ const typeof( ((type *)0)->member ) *__mptr = (ptr); \ (type *)( (char *)__mptr - offsetof(type,member) );})
Я думаю все знают откуда пришел данный прием.
Дайте угадаю, этот прием пришел из статьи про undefined behavior от PVS-Studio, где человеческим языком говорится, что так делать не стоит.
По поводу PVS ничего сказать не могу я не интересуюсь программированием под Windows и проприетарными продуктами.
Я даже растерялся. При чем тут undefined behavior и проприетарщина? Вы никогда не слышали про UB, не читали цикл статей What Every C Programmer Should Know About Undefined Behavior?
(type *)0)->member
Вот это товарища смутило.
Естественно я решил изучить данную тему подробнее. Но, если честно, в результате я только ещё больше запутался. Поэтому я не дам вам точный ответ, можно так писать или нет. Я только предоставлю некоторые ссылки и поделюсь своим мнением.
Но в общем и целом, я не думаю, что это место где стоит обсуждать код ядра линукс. Кто хочет пускай пишет на kernel mail lists, а потом напишет нам, что ему ответили.
Возвращаясь к прошлому холивару на пустом месте (https://habr.com/company/infopulse/blog/415259/#comment_18815187) могу сказать следующее:
EPOLLET — это никакое не преимущество epoll над select/poll, а необходимость, обусловленная архитектурой epoll. При использовании select/poll аналогичную функциональность можно без труда получить самостоятельно (просто не передавая не до конца обработанные события на вход).
EPOLLEXCLUSIVE и EPOLLONESHOT — это и правда фичи epoll, которые значительно упрощают многопоточную работу с сокетами.
Вот тут поподробнее пожалуйста, что вы имеете ввиду.
>> При использовании select/poll аналогичную функциональность можно без труда получить самостоятельно (просто не передавая не до конца обработанные события на вход).
Разница межде level и edge находиться в ядре. Если c level ядро осуществляет обход каждого дескриптора при вызове epoll_wait, чтобы выснить готов ли дескриптор к обработке. То в edge он пропускает проверку и сразу отправляет вызвавший процесс спать.
Внешние проявления можно получить безусловно. Но кроме epoll edge умеет только AIO, который кстати вообще обычно никем не рассматривается, даже в обзорных статьях.
Фича EPOLLET решает ровно одну задачу — дает возможность отложить реакцию на событие и продолжить ждать остальные события. Но poll умеет так же без специальных флагов.
Так… А с чего вы решили, что я в статье утверждаю обратное? Я вроде специально акцент сделал на изначальное предназначение epoll и с какой целью он создавался, а потом уже перешел к дополнительным плюшкам.
Здесь надо акцентировать на том, что epoll по-крайней мере не хуже poll, а в некоторых случаях лучше.
>> Фича EPOLLET решает ровно одну задачу — дает возможность отложить реакцию на событие и продолжить ждать остальные события. Но poll умеет так же без специальных флагов.
Именно поэтому я указал на отличие в обработке в ядре, чтобы было еще одно отличие. Собственно из-за это его называют O(1) I/O multiplexer…
Но если уж быть дотошным, то есть отличия и в правилах merge'a для событий разных типов.
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, поэтому не берусь судить.
Так что мы можем сравнить только удобство пользования в каком то смысле, но пускай это делают те кто интересуется кросс-платформенной разработкой.
events[0].data.ptr это ведь произвольный указатель, который может задать пользователь. Зачем указывать на epoll_event и заморачиваться с container_of, если можно сразу положить туда указатель на epoll_client.
Вся правда о linux epoll