Комментарии 66
Для poll() точно нужно руками обнулять revents?
Ссылка https://linux.die.net/man/2/poll об этом явно не говорит, но намекает, что в случае отрицательного fd и прочего, системный вызов туда сам напишет 0
А учитывая, что "только *BSD" не где-то там у гуру, а, внезапно, под капотом мака — то и весьма актуально.
Много чего. Там вообще смесь bsd и порой проприетарщины.
Весь мэйнстрим направлен на то, чтобы кодить под гуй на новомодных языках под новомодные фреймворки (причём эта самая мода имеет свойство очень быстро меняться).
Слой "тихой гавани" posix есть, но иногда внезапно натыкаешься на что-то нереализованное или реализованное криво.
По поводу kqueue есть годный пост от Игоря Сысоева; он старый, но до сих пор весьма актуален. http://www.opennet.ru/base/dev/kqueue_vs_epoll.txt.html
Сперва кажется, что это перевод. Например, фраза "с помощью вызова calling epoll_create".
Ещё можно было упомянуть как ограничение select() — ограничение на максимальный номер сокета. Когда соединений немного, но при этом сокеты имеют номера >1000. И тут он, внезапно, совершенно бесполезен, покуда FD_SET — битовое поле статического размера.
А под windows внезапно можно любые номера. Но при этом самих сокетов так же немного (точно не помню; 64 кажется).
А ещё можно упомянуть хорошее практическое применение select() с пустыми сетами как кросс-платформенного и относительно точного таймера.
poll внезапно хорош, когда нужно подождать фиксированное время один какой-то сокет (независимо от номера). Да, можно и epoll (угу, создавать и инициализировать целую ядерную структуру ради того, чтоб подождать, не прилетело ли в мой сокет что-то за последующие пару мс — ага, можно. Но зачем?). Можно их все сложить в один epoll и опрашивать централизованно — но это тоже слишком всё усложняет. А вот вызвать для него poll с массивом из единственного элемента, внезапно и систему особо не нагружает пинг-понгом из юзерспейса в ядро (помним же, после фиксов meltdown это внезапно стало БОЛЕЕ дорогой операцией!), и работает прямо на месте без всяких вспомогательных механизмов.
epoll с одной стороны да, подразумевает больше системных вызовов. Но конкретные числа лучше для конкретных приложений определять бенчем на типовой нагрузке. Особенно, где на сокет нагрузка всего в один-два пакета (подконнектился, опросил, выплюнул ответ, отключился). Особенно учитывая, что для неблокирующих сокетов можно вообще просто попробовать написать/прочитать без всякого опроса.
Ну и libevent — да, круто. Но всё ж сперва бы таки остальные фундаментальные "кирпичики" рассмотреть? kqueue, вон, весьма рулит! В отличие от epoll события можно добавлять сразу целым пакетом. И добавлять, одновременно опрашивая их и уже имеющиеся в сете — единственным системным вызовом. И запись/чтение — это два разных события (значит, не нужно ничего менять по ходу протокола; нужно просто отключить ненужные направления).
Или iocp под виндой. Там же совсем другой концепт! Он не ждёт "чего-нибудь на сокете", а лишь завершения конкретных ранее вызванных асинхронных операций записи/чтения. И при этом ждать может сразу в несколько потоков (без всяких особых приготовлений к этому). И обработку конкретных сокетов "привязывает" к конкретным ядрам, чтоб данные между ними не дрейфовали туда-сюда.
А libevent — да, всего лишь прокси поверх всего этого. Можно было даже в заголовок не выносить; по ней можно либо отдельную статью (покуда существующие грешат тем, что упираются в её возможности по работе с http, как-то опуская базовые возможности поллера)
// будем выбирать из очереди событий по 20 событий за раз struct epoll_event pevents[ 20 ]; // Ждём 10 секунд int ready = epoll_wait( pollingfd, pevents, 20, 10000 );
Вот и поди догадайся без комментария что это не 10 секунд, а 10000 миллисекунд. И что за 20 перед этим тоже не понятно — те же двадцать событий или какие-то другие 20.
// будем выбирать из очереди событий по 20 событий за раз
const unsigned EVENT_COUNT = 20; // количество обрабатываемых событий
const int TIME_WAIT_MS = 10000; // время ожидания
struct epoll_event pevents[EVENT_COUNT];
int ready = epoll_wait( pollingfd, pevents, EVENT_COUNT, TIME_WAIT_MS );
Понятнее же
Тем что он хуже. Внезапно :
- В рамках linux select и poll, используют один и тот же syscall poll, точнее даже select это обертка для poll сделанная для совместимости. Почему-то этот факт не упомянут.
- Нет никакой информации о pselect и ppoll, наверное они таки для чего-то нужны.
- с epoll совсем плохо:
- из статьи никак не следует, что epoll лучше, основное отличие не упомянуто (edge-triggered EPOLLET)
- не упомянут EPOLLONESHOT
- нет информации о thundering herd problem и флага EPOLLEXCLUSIVE
В общем лучше прочитать официальную страницу man'a http://man7.org/linux/man-pages/man7/epoll.7.html
В рамках linux select и poll, используют один и тот же syscall poll, точнее даже select это обертка для poll сделанная для совместимости
с чего бы это?
github.com/bminor/glibc/blob/release/2.27/master/sysdeps/unix/sysv/linux/select.c#L37
не вижу ни одного вхождения poll
fs/select.c#L450
Но glibc тут как раз ни при чем:
man7.org/linux/man-pages/man2/syscalls.2.html
Тем не менее syscall select присутствует.
SYSCALL_DEFINE5(select, int, n, fd_set __user *, inp, fd_set __user *, outp,
fd_set __user *, exp, struct timeval __user *, tvp)
{
return kern_select(n, inp, outp, exp, tvp);
}
https://github.com/torvalds/linux/blob/813835028e9ae1f18cd11bb0ec591d0f0577d96a/fs/select.c#L720
Так понятнее ?
В событийном цикле в современных системах — обычно нет, сигналы проще слушать — и это очень часто делают — через signalfd, подключенный к тому же epoll-директору.
Вот до появления signalfd надо было, да, крутиться вокруг атомарной установки маски сигналов.
> из статьи никак не следует, что epoll лучше
Основное отличие как раз упомянуто — что там, где select или poll на каждый вызов производят работу по подключению и отключению наблюдений за объектами, в epoll это постоянно установлено.
Edge triggered — вкусное свойство, но основным я бы его никак не назвал. Не все циклы событий работают так, что он им подходит. Даже EPOLLONESHOT как-то полезнее — он лучше укладывается, например, на логику Boost.ASIO — где после чтения/записи нужно явно «пнуть» канал, чтобы вызвало следующую операцию, но не предполагается при этом тут же пытаться догонять хвосты, пока не откажет по EAGAIN.
EPOLLEXCLUSIVE — сценарии, где он нужен, как-то сильно специфичен — на ум приходит только подход Apache, где у него пул процессов одновременно садится в accept(), и одному достаётся соединение. Но насколько это частое использование? С нитями вместо процессов внутренняя передача предельно дешёвая и эти странные методы просто не нужны…
Edge triggered — вкусное свойство, но основным я бы его никак не назвал.
Оно не «вкусное», EPOLLET + EPOLLONESHOT основное для ядра версии до 4.5. Иначе на многопоточном приложении будут проблемы со starvation и thundering herd.
EPOLLEXCLUSIVE — сценарии, где он нужен, как-то сильно специфичен
А вот это неправда, он как раз лечит выше перечисленное.
Так что не разобрать эти вещи если мы говорим о
проектировании высокопроизводительных сетевых приложения с неблокирующими сокетами
(заметьте не я это сказал)
непростительно.
«На многопоточном приложении», в котором несколько тредов постоянно дерутся за работу с одним и тем же сокетом? Или вы чего-то не договариваете, или это очень сомнительный дизайн.
> А вот это неправда, он как раз лечит выше перечисленное.
Как раз при такой драке за сокеты, и не нужен, если её нет.
Мы говорим о «высоко производительном сетевом приложении» или так поиграться?
Если так просто поиграться то я конечно ерунду написал, это никому не надо. Проще просто взять poll/select и поделить дескрипторы между потоками. А accept в одном потоке.
В догонку lwn.net/Articles/542629
Мы говорим о высокопроизводительном сетевом приложении. Именно поэтому я считаю, что варианты типа «сделаем, чтобы любой тред выхватывал готовность» как минимум требуют обоснования конкретной областью применения.
И, пожалуйста, поменьше неконструктивных дискуссионных приёмов.
> В догонку lwn.net/Articles/542629
То есть речь таки только о приёме нового соединения, как и сказал. Понятно, но это никак не все случаи «высокопроизводительных сетевых приложений».
Если просто по их количеству для корректного вызова, то нет такого преимущества по сравнению как минимум с poll().
Если по выдерживаемой нагрузке для типового приложения — да, именно так. Источники экономии общеизвестны. Есть, конечно, и неоднозначности тут (где-то была статья, что если одновременно более ~40% дескрипторов готовы, то poll выгоднее), но в целом картина смотрит именно в эту сторону.
И это уже универсальные факторы, а не зависящие от специфической модели применённого event engine, характера нагрузки (короткие соединения или долгоживущие) и так далее.
Вполне возможно, будет следующая статья, раскрывающая уже эти особенности.
Только в том что можно добавить больше fd?
В сопроводительном письме к патчу с epoll очень подробно все описано про это.
Я спрашивал про фундаментальные проблемы select/poll в multithreading, и как они решены в epoll.
И почему кстати обязательно сокетов это не только к сокетам относиться.
Никто epoll'ом пользоваться не заставляет. Но тогда из такой статьи следует — мало fd пользуемся poll/select — много fd пользуемся epoll и все. Добавим к этому, что epoll
Ваши собственные комментарии:
Так для малого числа сокетов никакой многопоточности и не нужно. По крайней мере, в сетевой части.
Как смысл его использования может пропасть когда он — единственное нормальное решение при большом количестве fd?
А теперь вопрос — при большом количестве fd — может быть таки пригодилась бы многопоточность?
epoll именно этим и отличается, что позволяет реализовать многопоточный accept, чтение дескрипторов более простым способом и с более лучшей балансировкой.
И как раз в этом состоит роль его особенностей в виде EPOLLET, EPOLLONESHOT, EPOLLEXCLUSIVE (и не только кстати). Что я собственно и пытался донести.
И много усилий было сделано после написание данной статьи (2014 год если кто не заметил).
Данная статья epoll просто не раскрывает.
И более того на момент появления он обладал теми же недостатками, что и poll/select.
Во-вторых, а чтение/запись, а sigfd — я должен писать разный код для разных потоков?
И, честно говоря, теперь уже я вас не понимаю, если вам это не нужно и вы не сталкивались с необходимостью — это не ведь не значит, что не нужно никому?
А значит, какой бы замечательной ни была фича которая позволяет делать многопоточный accept — основной она точно не является.
Фича не в многопоточном accept, для которого еще кстати нужно кое-что сделать.
Фича в простой баллансировке обработки событий между потоками.
Вы поняли из данной статьи, как это можно сделать с помощью epoll?
Хотя бы многопоточный accept сможете сделать на основе данной статьи?
Вы можете хотя бы в общих словах описать задачи, где это идёт на пользу?
Я до сих пор сталкивался только с такими, где контекст слишком тяжёлый, чтобы его можно свободно гонять между нитями (подразумевается, на разных ядрах) на каждую мелкую порцию данных, и ваш подход там пошёл бы только во вред.
И вот именно это показывает специфику его задач. Я ни разу ещё не сталкивался с задачей, где бы контекст работы был настолько маленьким, а оперативность реакции — настолько важной, чтобы оправдать затраты на перемещение данных между ядрами на каждый мельчайший чих. Минимальный контекст, который трогается задачей на один такой вызов, это несколько килобайт, обычно выше. Пока эти данные будут ползти между ядрами по шине синхронизации… позеленеть можно, но не дождаться.
(Интересно послушать, что же это за задачи у него.)
Поэтому я подобные подходы обычно рассматриваю в рамках какого-то диспетчера, который может перекидывать задачи между нитями, но пока хватает производительности — сохранять за той же нитью, ну и не допускать спора нескольких нитей за одну задачу.
1. Я поблагодарил автора, сказал что мне понравилась статья.
2. Немного описал чем я занимаюсь (это для пункта 3).
3. С учетом пункта 2, я сказал что конкретно мне понравилось в статье и почему.
Скорее это вы не поняли моего комментария. Всё что сказано в статье, более лаконично и точно изложено в любой книге. В том числе и ограничение на 1024 дескриптора
Там значительно короче и подробнее описано. Можно использовать, как справочник.
Вы бы ещё глубже капнули бы. Хабр в первую очередь развлекательный ресурс, а подобные статьи просто его замусоривают. Не неся конкретной пользы.
А все таки лучше раскошелиться например на эту книгу, вложения окупятся.
man7.org/tlpi
Брать только на английском.
можно еще почитать/написать про IOCP и Registered I/O в Windows (последнее в Win8+/2012+)
Если речь про posix signal() — то, емнип, он не потокобезопасен. То есть предназначен максимум, чтобы просемафорить программе
fcntl(fd, F_SETSIG, sig)
для дескриптора
и последующем получении информации об I/O событиях через sigwaitinfo()/sigtimedwait()
Здесь как раз отсутствуют проблемы poll()/select() выраженные в квадратичной сложности от количества сокетов. А realtime сигналы избавлены от проблем обычных сигналов.
В свое время (10 лет назад) реализовывал на этом сервер. Все работало без проблем.
Но вот сама ситуация, когда тебя прерывают в произвольном месте кода, чтобы в очень ограниченном по возможностям контексте что-то сделать — просто неудобна. Самое практичное, что может сделать обработчик сигнала в долговременно работающей программе — поставить пару флагов типа sig_atomic_t и выйти. Даже longjmp() уже сомнителен, ибо нет гарантии, что подружится с, например, RAII и исключениями С++. Тогда зачем, если через signalfd можно просто узнать приход сигнала в основном цикле?
И зачем это делать, если через epoll c компанией можно узнать о готовности объекта по дескриптору — напрямую, а не через промежуточный механизм посылки сигнала?
Вторая причина более экзотична и имеет отношению к тому факту, что select может (теоретически) работать с таймаутами порядка одной наносекунды (если позволит аппаратная часть), в то время как и poll и epoll поддерживают лишь миллисекундную точность
неправда же, микросекунды (10^-6) и миллисекунды (10^-3) соотвественно.
Максимальное количество одновременно наблюдаемых дескрипторов ограниченно константой FD_SETSIZE, которая в линуксе жестко равна 1024
на самом деле ограничение более жёсткое: нельзя использовать select с файловыми дескрипторами, у которых номер больше FD_SETSIZE (благо, ядро повторно использует номера fd, так что обычно это не является проблемой)
Он спроектирован намного лучше и не страдает от большинства недостатков метода select.
по большому счёту он решает только проблему отслеживания кучи файловых дескрипторов, но решает её неудачно (объём передаваемых данных из/в userspace только вырос, линейный обход списка дескрипторов вынуждено присутствует и в ядре, и в userspace).
честно скажу — я использую select, и не стыжусь этого. мне он банально привычнее, при этом я не вижу существенных преимуществ poll.
Но всё же в мире есть системы реального времени, имеющие такие таймеры.
Вот была у меня проблема в QNX 6.3: «если в функции select указать в структуре времени ожидания нули (вообще не ждать), то минут через 50 загрузка процессора возрастёт до 100%. Но если задать, скажем, 1 мкс, то всё работает как часы. Конечно, время ожидания на select в худшем случае будет с пару-тройку системных тиков, но тик у меня 0.1 мс, так что приемлимо. „
Эту бы статью да лет 20 назад. И конечно poll вместо select очень помог.
Великолепно.
select / poll / epoll: практическая разница