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

В этой статье мы рассмотрим:

  • select()
  • poll()
  • epoll()
  • libevent

Использование select()


Старый, проверенный годами работяга select() создавался ещё в те времена, когда «сокеты» назывались "сокетами Беркли". Данный метод не вошел в самую первую спецификацию тех самих сокетов Беркли, поскольку в те времена вообще ещё не существовало концепции неблокирующего ввода-вывода. Но где-то в 80-ых годах она появилась, а вместе с ней и select(). С тех пор в его интерфейсе ничего существенно не менялось.

Для использования select() разработчику необходимо инициализировать и заполнить несколько структур fd_set дескрипторами и событиями, которые необходимо мониторить, а затем уже вызвать select(). Типичный код выглядит примерно вот так:

fd_set fd_in, fd_out;
struct timeval tv;
 
// обнуляем структуры
FD_ZERO( &fd_in );
FD_ZERO( &fd_out );
 
// Будем мониторить события о входящих данных для sock1
FD_SET( sock1, &fd_in );
 
// Будем мониторить события об исходящих данных для sock2
FD_SET( sock2, &fd_out );
 
// Определим сокет с максимальным числовым значением (select требует это значение)
int largest_sock = sock1 > sock2 ? sock1 : sock2;
 
// Будем ждать до 10 секунд
tv.tv_sec = 10;
tv.tv_usec = 0;
 
// Вызываем select
int ret = select( largest_sock + 1, &fd_in, &fd_out, NULL, &tv );
 
// Проверяем успешность вызова
if ( ret == -1 )
    // ошибка
else if ( ret == 0 )
    // таймаут, событий не произошло
else
{
    if ( FD_ISSET( sock1, &fd_in ) )
        // входящее событие на sock1
 
    if ( FD_ISSET( sock2, &fd_out ) )
        // исходящее событие на sock2
}

Когда проектировался select() никто, вероятно, не ожидал, что в будущем у нас появится необходимость писать многопоточные приложения, обслуживающие тысячи соединений. У select() есть сразу несколько существенных недостатков, делающих его плохо пригодным для работы в такого рода системах. Основными являются следующие:

  • select модифицирует передаваемые ему структуры fd_sets, так что ни одну из них нельзя переиспользовать. Даже если вам не нужно ничего менять (например, получив порцию данных, вы хотите получить ещё) структуры fd_sets придётся переинициализировать. Ну или копировать из заранее сохранённого бэкапа с помощью FD_COPY. И это придётся делать снова и снова, перед каждым вызовом select.
  • Для выяснения того, какой именно дескриптор сгенерировал событие, вам придётся вручную опросить их все с помощью FD_ISSET. Когда вы мониторите 2000 дескрипторов, а событие произошло лишь для одного из них (который по закону подлости будет последним в списке) — вы потратите уйму процессорных ресурсов впустую.
  • Я только что упомянул 2000 дескрипторов? Это я погорячился. select не поддерживает так много. Ну, по крайней мере на обычном линуксе, с обычным ядром. Максимальное количество одновременно наблюдаемых дескрипторов ограниченно константой FD_SETSIZE, которая в линуксе жестко равна 1024. Некоторые операционные системы позволяют реализовать хак с переопределением значения FD_SETSIZE перед включением заголовочного файла sys/select.h, но этот хак не является частью какого-то общего стандарта. Тот же Linux проигнорирует его.
  • Вы не можете работать с дескрипторами из наблюдаемого набора из другого потока. Представьте себе поток, выполняющий вышеуказанный код. Вот он запустился и ждёт событий в своём select(). Теперь представьте, что у вас есть ещё один поток, мониторящий общую нагрузку на систему, и вот он решил, что данные от сокета sock1 не приходят уже слишком давно и пора бы разорвать соединение. Поскольку данный сокет может быть переиспользован для обслуживания новых клиентов, хорошо бы его корректно закрыть. Но ведь первый поток наблюдает в том числе и за этим дескриптором прямо сейчас. Что же будет, если мы его всё-таки закроем? О, у документации есть ответ на этот вопрос и он вам не понравится: «Если дескриптор, наблюдаемый при помощи select(), будет закрыт другим потоком, вы получите неопределённое поведение».
  • Та же проблема появляется и при попытке отправить какие-то данные через sock1. Мы ничего не отправим, пока select не закончит свою работу.
  • Выбор событий, которые мы можем мониторить, достаточно ограничен. Например, для определения того, что удалённый сокет был закрыт, вам следует, во-первых, мониторить события прихода данных по нему, а во-вторых, сделать попытку чтения этих данных (read вернёт 0 для закрытого сокета). Это ещё можно назвать приемлемым при чтении данных из сокета (прочитали 0 — сокет закрыт), но что, если наша текущая задача в данный момент — отправка данных этому сокету и никакое чтение данных из него нам сейчас не нужно?
  • select накладывает на вас излишнее бремя вычисления «наибольшего дескриптора» и передачу его отдельным параметром

Конечно, всё вышесказанное не является какой-то новостью. Разработчики операционных систем давно осознали данные проблемы и многие из них были учтены при проектировании метода poll. В этом месте вы можете спросить, а зачем мы вообще сейчас изучаем древнюю историю и есть ли сегодня какие-то причины использовать древний select? Да, такие причины есть и их целых две. Не факт, что они когда-то вам пригодятся, но почему бы о них не узнать.

Первая причина — портируемость. select() с нами уже миллион лет. В какие бы дебри программно-аппаратных платформ вас не занесло, если там есть сеть — там будет и select. Там может не быть никаких других методов, но select будет практически гарантированно. И не думайте, что я сейчас впадаю в старческий маразм и вспоминаю что-то типа перфокарт и ENIAC, нет. Более современного метода poll нет, например, в Windows XP. А вот select есть.

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

Описанный выше случай, вероятно, единственный в котором у вас и правда нет выбора, что использовать (подходит лишь select). Однако, если вы пишете обычное приложение для работы на обычном железе, и вы будете оперировать адекватным количеством сокетов (десятками, сотнями — и не больше), то разница в производительности poll и select будет не заметна, так что выбор будет основываться на других факторах.

Опрос с помощью poll()


poll — это более новый метод опроса сокетов, созданный после того, как люди начали пытаться писать большие и высоконагруженные сетевые сервисы. Он спроектирован намного лучше и не страдает от большинства недостатков метода select. В большинстве случаев при написании современных приложений вы будете выбирать между использованием poll и epoll/libevent.

Для использования poll разработчику нужно инициализировать члены структуры pollfd наблюдаемыми дескрипторами и событиями, а затем вызвать poll().
Типичный код выглядит вот так:

// два события
struct pollfd fds[2];
 
// от sock1 мы будем ожидать входящих данных
fds[0].fd = sock1;
fds[0].events = POLLIN;
 
// а от sock2 - исходящих
fds[1].fd = sock2;
fds[1].events = POLLOUT;
 
// ждём до 10 секунд
int ret = poll( &fds, 2, 10000 );
// проверяем успешность вызова
if ( ret == -1 )
    // ошибка
else if ( ret == 0 )
    // таймаут, событий не произошло
else
{
    // обнаружили событие, обнулим revents чтобы можно было переиспользовать структуру
    if ( pfd[0].revents & POLLIN )
        pfd[0].revents = 0;
        // обработка входных данных от sock1

    if ( pfd[1].revents & POLLOUT )
        pfd[1].revents = 0;
        // обработка исходящих данных от sock2
}

Poll был создан для решения проблем метода select, давайте посмотрим, как у него это получилось:

  • Нет никакого лимита количества наблюдаемых дескрипторов, можно мониторить более 1024 штук
  • Не модифицируется структура pollfd, что даёт возможность её переиспользования между вызовами poll() — нужно лишь обнулить поле revents.
  • Наблюдаемые события лучше структурированы. Например, можно определить отключение удалённого клиента без необходимости чтения данных из сокета.

О недостатках метода poll мы уже говорили выше: его нет на некоторых платформах, вроде Windows XP. Начиная с Vista он существует, но называется WSAPoll. Прототип тот же, так что для платформенно-независимого кода можно написать переопределение, вроде:

#if defined (WIN32)
static inline int poll( struct pollfd *pfd, int nfds, int timeout) { return WSAPoll ( pfd, nfds, timeout ); }
#endif

Ну и точность таймаутов в 1 мс, которой будет недостаточно очень редко. Однако, у poll есть и другие недостатки:

  • Как и при использовании select, невозможно определить какие именно дескрипторы сгенерировали события без полного прохода по всем наблюдаемым структурам и проверки в них поля revents. Что ещё хуже, так же это реализовано и в ядре ОС.
  • Как и при использовании select, нет возможности динамически менять наблюдаемый набор событий

Однако, всё вышеперечисленное можно считать относительно несущественным для большинства клиентских приложений. Исключение составляют, наверное, лишь p2p протоколы, где каждый из клиентов может быть связан с тысячами других. Эти проблемы могут игнорироваться даже большинством серверных приложений. Таким образом poll должен быть вашим предпочтением по-умолчанию перед select, если только вас не ограничивает одна из двух вышеуказанных причин.

Забегая наперёд, скажу, что poll является более предпочтительным даже по сравнению с более современным epoll (рассматривается ниже) в следующих случаях:

  • Вы хотите писать кросплатформенный код (epoll есть только в Linux)
  • Вам не нужно мониторить более 1000 сокетов (epoll не даст вам ничего существенного в этом случае)
  • Вам нужно мониторить более 1000 сокетов, но время соединения с каждым из них очень невелико (в этих случаях производительность poll и epoll будет очень близка — выигрыш от ожидания меньшего количества событий в epoll будет перечёркнут накладными расходами на их добавление/удаление)
  • Ваше приложение не спроектировано таким образом, чтобы менять события из одного потока, пока другой ожидает их (или вам этого не требуется)

Polling with epoll()


epoll — это новейший и лучший метод ожидания событий в Linux (и только в Linux). Ну, не то чтобы прям «новейший» — он в ядре с 2002 года. От poll и select он отличается тем, что предоставляет API для добавления/удаления/модификации списка наблюдаемых дескрипторов и событий.

Использование epoll требует чуть более тщательных приготовлений. Разработчик должен:

  • Создать дескриптор epoll с помощью вызова epoll_create
  • Инициализировать структуру epoll_event нужными событиями и указателями на контексты соединений. «Контекст» здесь может быть чем-угодно, epoll просто передаёт это значение в возвращаемых событиях
  • Вызвать epoll_ctl( … EPOLL_CTL_ADD ) для добавления дескриптора в список наблюдаемых
  • Вызвать epoll_wait() для ожидания событий (мы указываем сколько именно событий хотим получить за раз, например, 20). В отличии от предыдущих методов — мы получим эти события отдельно, а не в свойствах входных структур. Если мы наблюдаем 200 дескрипторов и 5 из них получили новые данные — epoll_wait вернёт лишь 5 событий. Если произойдёт 50 событий — нам вернут первые 20, а остальные 30 будут ждать следующего вызова, они не потеряются
  • Обработать полученные события. Это будет относительно быстрая обработка, ведь мы не просматриваем те дескрипторы, где ничего не произошло

Типичный код выглядит вот так:

// Создаём дескриптор epoll. Нам нужен лишь один на всё приложение, он будет мониторить все сокеты
// Аргумент функции игнорируется (раньше это было не так, но сейчас так), так что напишите здесь своё любимое число
int pollingfd = epoll_create( 0xCAFE ); 

if ( pollingfd < 0 )
 // ошибка

// Инициализируем структуру epoll_event
struct epoll_event ev = { 0 };

// Ассоциируйте соединение с наблюдаемым событием. Вы можете ассоциировать всё, что угодно
// epoll никак не использует эту информацию. Можно, например, сохранить указатель на объект класса соединения
ev.data.ptr = pConnection1;

// Наблюдаем события прихода данных, по одному за раз
ev.events = EPOLLIN | EPOLLONESHOT;

// Добавляем дескриптор в список наблюдаемых. Это можно сделать даже из другого потока
// пока первый ожидает в вызове epoll_wait - всё сработает правильно
if ( epoll_ctl( epollfd, EPOLL_CTL_ADD, pConnection1->getSocket(), &ev ) != 0 )
    // report error

// будем выбирать из очереди событий по 20 событий за раз
struct epoll_event pevents[ 20 ];

// Ждём 10 секунд
int ready = epoll_wait( pollingfd, pevents, 20, 10000 );

// Проверяем успешность вызова
if ( ret == -1 )
    // ошибка
else if ( ret == 0 )
    // таймаут, событий не произошло
else
{
    // просматриваем полученный список событий
    for ( int i = 0; i < ret; i++ )
    {
        if ( pevents[i].events & EPOLLIN )
        {
            // получаем ранее ассоциированный с событием указатель на соединение, обрабатываем его
            Connection * c = (Connection*) pevents[i].data.ptr;
            c->handleReadEvent();
         }
    }
}

Давайте начнём с недостатков epoll — они очевидны из кода. Данный метод сложнее использовать, нужно написать больше кода, он делает больше системных вызовов.

Достоинства тоже налицо:

  • epoll возвращает список только тех дескрипторов, для которых реально произошли наблюдаемые события. Не нужно просматривать тысячи структур в поисках той, возможно, одной, где сработало ожидаемое событие.
  • Вы можете ассоциировать некоторый значимый контекст с каждым наблюдаемым событием. В примере выше мы использовали для этого указатель на объект класса соединения — это сэкономило нам ещё один потенциальный поиск по массиву соединений.
  • Вы можете добавлять или удалять сокеты из списка в любое время. Вы можете даже модифицировать наблюдаемые события. Всё будет работать корректно, это официально поддерживается и задокументировано.
  • Можно завести сразу несколько потоков, ожидающих события из одной и той же очереди с помощью epoll_wait. Нечто, что никоим образом не получится сделать с select/poll.

Но нужно также помнить и о том, что epoll — это не «во всём улучшенный poll». У него есть и недостатки по сравнению с poll:

  • Изменение флагов событий (например, переключение с READ на WRITE) требует лишнего системного вызова epoll_ctl, в то время как для poll вы просто меняете битовую маску (полностью в пользовательском режиме). Переключение 5000 сокетов с чтения на запись потребует для epoll 5000 системных вызовов и переключений контекста, в то время как для poll это будет тривиальная битовая операция в цикле.
  • Для каждого нового соединения вам придётся вызвать accept() и epoll_ctl() — это два системных вызова. В случае использования poll вызов будет лишь один. При очень коротком времени жизни соединения это может иметь значение.
  • epoll есть только в Linux. В других ОС есть схожие механизмы, но всё же не полностью идентичные. Вам не удастся написать код с epoll так, чтобы он собрался и заработал, например, на FreeBSD.
  • Писать высоконагруженный параллельный код — тяжело. Многим приложениям не нужен столь фундаментальный подход, поскольку их уровень нагрузки легко обрабатывается и более простыми методами.

Таким образом, использовать epoll следует только тогда, когда выполняется всё нижесказанное:

  • Ваше приложение использует пул потоков для обработки сетевых соединений. Выигрыш от epoll в однопоточном приложении будет ничтожен, не стоит и заморачиваться на реализацию.
  • Вы ожидаете относительно большого числа соединений (от 1000 и выше). На небольшом количестве наблюдаемых сокетов epoll не даст прироста производительности, а если сокетов буквально несколько штук — может даже замедлить.
  • Ваши соединения живут относительно долго. В ситуации, когда новое соединение передаёт буквально несколько байт данных и тут же закрывается — poll будет работать быстрее, ведь на обработку ему нужно будет делать меньше системных вызовов.
  • Вы намерены запускать ваш код на Linux и только на Linux.

Если один или несколько пунктов не выполняются — рассмотрите использование poll или libevent.

libevent


libevent — это библиотека, которая «оборачивает» методы опроса, перечисленные в данной статье (а также некоторые другие) в унифицированный API. Преимущество здесь в том, что, однажды написав код, вы можете собрать и запустить его на разных операционных системах. Тем не менее, важно понимать, что libevent — это всего лишь обёртка, внутри которой работают всё те же вышеперечисленные методы, со всеми их преимуществами и недостатками. libevent не заставит select слушать более 1024 сокетов, а epoll — модифицировать список событий без дополнительного системного вызова. Так что знать лежащие в основе технологии по-прежнему важно.

Необходимость поддерживать разные методы опроса приводит к усложнению API библиотеки libevent. Но всё же его использование проще, чем вручную писать два разных движка выборки событий для, например, Linux и FreeBSD (используя epoll и kqueue).

Рассмотреть возможность использования libevent стоит при сочетании двух событий:

  • Вы рассмотрели методы select и poll и они вам точно не подошли
  • Вам нужно поддерживать несколько ОС

Комментарии (65)


  1. Dima_Sharihin
    26.06.2018 09:30

    Для poll() точно нужно руками обнулять revents?


    Ссылка https://linux.die.net/man/2/poll об этом явно не говорит, но намекает, что в случае отрицательного fd и прочего, системный вызов туда сам напишет 0


  1. untilx
    26.06.2018 09:54

    Не плохо бы всё-таки рассказать про упомянутый kqueue. По-моему, технология стоящая, хоть и только для *BSD.


    1. klirichek
      26.06.2018 10:22

      А учитывая, что "только *BSD" не где-то там у гуру, а, внезапно, под капотом мака — то и весьма актуально.


      1. untilx
        26.06.2018 10:25

        Согласен. Хотя, чего только у мака под капотом нету?


        1. klirichek
          26.06.2018 12:11

          Много чего. Там вообще смесь bsd и порой проприетарщины.
          Весь мэйнстрим направлен на то, чтобы кодить под гуй на новомодных языках под новомодные фреймворки (причём эта самая мода имеет свойство очень быстро меняться).
          Слой "тихой гавани" posix есть, но иногда внезапно натыкаешься на что-то нереализованное или реализованное криво.


          По поводу kqueue есть годный пост от Игоря Сысоева; он старый, но до сих пор весьма актуален. http://www.opennet.ru/base/dev/kqueue_vs_epoll.txt.html


          1. untilx
            26.06.2018 15:23

            Ага, пост уже видел сразу после первого комментария.


  1. klirichek
    26.06.2018 10:21

    Сперва кажется, что это перевод. Например, фраза "с помощью вызова calling epoll_create".


    Ещё можно было упомянуть как ограничение select() — ограничение на максимальный номер сокета. Когда соединений немного, но при этом сокеты имеют номера >1000. И тут он, внезапно, совершенно бесполезен, покуда FD_SET — битовое поле статического размера.
    А под windows внезапно можно любые номера. Но при этом самих сокетов так же немного (точно не помню; 64 кажется).
    А ещё можно упомянуть хорошее практическое применение select() с пустыми сетами как кросс-платформенного и относительно точного таймера.


    poll внезапно хорош, когда нужно подождать фиксированное время один какой-то сокет (независимо от номера). Да, можно и epoll (угу, создавать и инициализировать целую ядерную структуру ради того, чтоб подождать, не прилетело ли в мой сокет что-то за последующие пару мс — ага, можно. Но зачем?). Можно их все сложить в один epoll и опрашивать централизованно — но это тоже слишком всё усложняет. А вот вызвать для него poll с массивом из единственного элемента, внезапно и систему особо не нагружает пинг-понгом из юзерспейса в ядро (помним же, после фиксов meltdown это внезапно стало БОЛЕЕ дорогой операцией!), и работает прямо на месте без всяких вспомогательных механизмов.


    epoll с одной стороны да, подразумевает больше системных вызовов. Но конкретные числа лучше для конкретных приложений определять бенчем на типовой нагрузке. Особенно, где на сокет нагрузка всего в один-два пакета (подконнектился, опросил, выплюнул ответ, отключился). Особенно учитывая, что для неблокирующих сокетов можно вообще просто попробовать написать/прочитать без всякого опроса.


    Ну и libevent — да, круто. Но всё ж сперва бы таки остальные фундаментальные "кирпичики" рассмотреть? kqueue, вон, весьма рулит! В отличие от epoll события можно добавлять сразу целым пакетом. И добавлять, одновременно опрашивая их и уже имеющиеся в сете — единственным системным вызовом. И запись/чтение — это два разных события (значит, не нужно ничего менять по ходу протокола; нужно просто отключить ненужные направления).
    Или iocp под виндой. Там же совсем другой концепт! Он не ждёт "чего-нибудь на сокете", а лишь завершения конкретных ранее вызванных асинхронных операций записи/чтения. И при этом ждать может сразу в несколько потоков (без всяких особых приготовлений к этому). И обработку конкретных сокетов "привязывает" к конкретным ядрам, чтоб данные между ними не дрейфовали туда-сюда.
    А libevent — да, всего лишь прокси поверх всего этого. Можно было даже в заголовок не выносить; по ней можно либо отдельную статью (покуда существующие грешат тем, что упираются в её возможности по работе с http, как-то опуская базовые возможности поллера)


    1. tangro Автор
      26.06.2018 11:00

      Сперва кажется, что это перевод.

      Наверное, потому, что так и есть :) Исправлено.


    1. hdfan2
      26.06.2018 14:38

      А под windows внезапно можно любые номера. Но при этом самих сокетов так же немного (точно не помню; 64 кажется).

      Там как раз работает переопределение FD_SETSIZE.


    1. tangro Автор
      27.06.2018 12:35

      Или iocp под виндой

      А как же без него!


  1. domix32
    26.06.2018 11:22
    -1

    // будем выбирать из очереди событий по 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 );

    Понятнее же


  1. dlinyj
    26.06.2018 11:24
    +1

    В чём отличие этого поста, от любой книжки программирования под линукс?


    1. maquefel
      26.06.2018 12:03
      +1

      Тем что он хуже. Внезапно :


      1. В рамках linux select и poll, используют один и тот же syscall poll, точнее даже select это обертка для poll сделанная для совместимости. Почему-то этот факт не упомянут.
      2. Нет никакой информации о pselect и ppoll, наверное они таки для чего-то нужны.
      3. с epoll совсем плохо:
        • из статьи никак не следует, что epoll лучше, основное отличие не упомянуто (edge-triggered EPOLLET)
        • не упомянут EPOLLONESHOT
        • нет информации о thundering herd problem и флага EPOLLEXCLUSIVE

      В общем лучше прочитать официальную страницу man'a http://man7.org/linux/man-pages/man7/epoll.7.html


      1. dlinyj
        26.06.2018 12:08

        Из вашего комментария узнал больше, чем из поста


        1. shushu
          27.06.2018 07:19
          +1

          По-моему — это дивиз Хабра.
          Как правило в коментариях найдешь больше информации чем статье :-)


      1. edo1h
        27.06.2018 01:18

        В рамках linux select и poll, используют один и тот же syscall poll, точнее даже select это обертка для poll сделанная для совместимости


        с чего бы это?
        github.com/bminor/glibc/blob/release/2.27/master/sysdeps/unix/sysv/linux/select.c#L37
        не вижу ни одного вхождения poll


        1. splav_asv
          27.06.2018 09:39

          Syscall'ы действительно разные, но используется один внутренний механизм — poll.
          fs/select.c#L450


          1. edo1h
            27.06.2018 15:42

            правильнее сказать «внутри ядра используется общий механизм», что, в общем-то, неудивительно


            1. maquefel
              27.06.2018 16:10

              Не «так будет правильнее сказать», а собственно вы правы.

              Но glibc тут как раз ни при чем:
              man7.org/linux/man-pages/man2/syscalls.2.html

              Тем не менее syscall select присутствует.


              1. edo1h
                27.06.2018 16:30

                glibc при чём в том плане, что 99% инсталляций linux на компьютере используют именно её — соответсвенно от реализации select в glibc зависит то, какой syscall будет дёргаться.


                1. maquefel
                  27.06.2018 16:41

                  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


                  Так понятнее ?


      1. netch80
        27.06.2018 16:56

        > Нет никакой информации о pselect и ppoll, наверное они таки для чего-то нужны.

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

        > из статьи никак не следует, что epoll лучше

        Основное отличие как раз упомянуто — что там, где select или poll на каждый вызов производят работу по подключению и отключению наблюдений за объектами, в epoll это постоянно установлено.

        Edge triggered — вкусное свойство, но основным я бы его никак не назвал. Не все циклы событий работают так, что он им подходит. Даже EPOLLONESHOT как-то полезнее — он лучше укладывается, например, на логику Boost.ASIO — где после чтения/записи нужно явно «пнуть» канал, чтобы вызвало следующую операцию, но не предполагается при этом тут же пытаться догонять хвосты, пока не откажет по EAGAIN.

        EPOLLEXCLUSIVE — сценарии, где он нужен, как-то сильно специфичен — на ум приходит только подход Apache, где у него пул процессов одновременно садится в accept(), и одному достаётся соединение. Но насколько это частое использование? С нитями вместо процессов внутренняя передача предельно дешёвая и эти странные методы просто не нужны…


        1. maquefel
          27.06.2018 17:12

          Edge triggered — вкусное свойство, но основным я бы его никак не назвал.

          Оно не «вкусное», EPOLLET + EPOLLONESHOT основное для ядра версии до 4.5. Иначе на многопоточном приложении будут проблемы со starvation и thundering herd.

          EPOLLEXCLUSIVE — сценарии, где он нужен, как-то сильно специфичен


          А вот это неправда, он как раз лечит выше перечисленное.

          Так что не разобрать эти вещи если мы говорим о

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

          (заметьте не я это сказал)

          непростительно.


          1. netch80
            27.06.2018 17:23

            > Иначе на многопоточном приложении будут проблемы со starvation и thundering herd.

            «На многопоточном приложении», в котором несколько тредов постоянно дерутся за работу с одним и тем же сокетом? Или вы чего-то не договариваете, или это очень сомнительный дизайн.

            > А вот это неправда, он как раз лечит выше перечисленное.

            Как раз при такой драке за сокеты, и не нужен, если её нет.


            1. maquefel
              27.06.2018 17:28

              «Вы либо крест снимите или трусы оденьте».

              Мы говорим о «высоко производительном сетевом приложении» или так поиграться?

              Если так просто поиграться то я конечно ерунду написал, это никому не надо. Проще просто взять poll/select и поделить дескрипторы между потоками. А accept в одном потоке.

              В догонку lwn.net/Articles/542629


              1. netch80
                27.06.2018 17:36

                > Мы говорим о «высоко производительном сетевом приложении» или так поиграться?

                Мы говорим о высокопроизводительном сетевом приложении. Именно поэтому я считаю, что варианты типа «сделаем, чтобы любой тред выхватывал готовность» как минимум требуют обоснования конкретной областью применения.
                И, пожалуйста, поменьше неконструктивных дискуссионных приёмов.

                > В догонку lwn.net/Articles/542629

                То есть речь таки только о приёме нового соединения, как и сказал. Понятно, но это никак не все случаи «высокопроизводительных сетевых приложений».


                1. maquefel
                  27.06.2018 17:41

                  Тогда позвольте вопрос, а в чем собственно преимущество epoll по вашему мнению тогда? Только в том что можно добавить больше fd?


                  1. netch80
                    27.06.2018 17:55

                    «Больше fd» в каком смысле?
                    Если просто по их количеству для корректного вызова, то нет такого преимущества по сравнению как минимум с poll().
                    Если по выдерживаемой нагрузке для типового приложения — да, именно так. Источники экономии общеизвестны. Есть, конечно, и неоднозначности тут (где-то была статья, что если одновременно более ~40% дескрипторов готовы, то poll выгоднее), но в целом картина смотрит именно в эту сторону.
                    И это уже универсальные факторы, а не зависящие от специфической модели применённого event engine, характера нагрузки (короткие соединения или долгоживущие) и так далее.
                    Вполне возможно, будет следующая статья, раскрывающая уже эти особенности.


                    1. maquefel
                      27.06.2018 18:03
                      -1

                      Вы не ответили на вопрос о преимуществах epoll по вашему мнению.


                      1. netch80
                        27.06.2018 18:05
                        +1

                        Ответил, и подробно.


                  1. mayorovp
                    27.06.2018 20:45
                    -1

                    Прежде всего в том, что (амортизированные) накладные расходы не растут с ростом числа сокетов, в отличии от select или poll.


                    1. maquefel
                      27.06.2018 20:50

                      Про это я как раз написал,

                      Только в том что можно добавить больше fd?


                      В сопроводительном письме к патчу с epoll очень подробно все описано про это.
                      Я спрашивал про фундаментальные проблемы select/poll в multithreading, и как они решены в epoll.

                      И почему кстати обязательно сокетов это не только к сокетам относиться.


                      1. mayorovp
                        27.06.2018 20:54
                        -1

                        Так для малого числа сокетов никакой многопоточности и не нужно. По крайней мере, в сетевой части.


                        1. maquefel
                          27.06.2018 20:59

                          И мы опять возвращаемся к вопросу «а зачем тогда epoll и чем он лучше?».

                          Никто epoll'ом пользоваться не заставляет. Но тогда из такой статьи следует — мало fd пользуемся poll/select — много fd пользуемся epoll и все. Добавим к этому, что epoll уникален для linux kernel, и весь смысл пользоваться epoll пропадает — разве не так?


                          1. mayorovp
                            27.06.2018 21:01
                            -1

                            Я вас не понимаю. Как смысл его использования может пропасть когда он — единственное нормальное решение при большом количестве fd?


                            1. maquefel
                              27.06.2018 21:05

                              Вы просто читаете мои комментарии наполовину.
                              Ваши собственные комментарии:

                              Так для малого числа сокетов никакой многопоточности и не нужно. По крайней мере, в сетевой части.

                              Как смысл его использования может пропасть когда он — единственное нормальное решение при большом количестве fd?


                              А теперь вопрос — при большом количестве fd — может быть таки пригодилась бы многопоточность?


                              1. mayorovp
                                27.06.2018 21:20

                                Разумеется. А что, кто-то утверждает обратное?


                                1. maquefel
                                  27.06.2018 21:28

                                  Вот! Наконец консенсус.

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

                                  И как раз в этом состоит роль его особенностей в виде EPOLLET, EPOLLONESHOT, EPOLLEXCLUSIVE (и не только кстати). Что я собственно и пытался донести.

                                  И много усилий было сделано после написание данной статьи (2014 год если кто не заметил).

                                  Данная статья epoll просто не раскрывает.

                                  И более того на момент появления он обладал теми же недостатками, что и poll/select.


                                  1. mayorovp
                                    27.06.2018 21:29
                                    -1

                                    Но почему многопоточная работа с сокетами — это всегда многопоточный accept?


                                    1. maquefel
                                      27.06.2018 21:33

                                      Во-первых почему нет, почему если я пишу многопоточное приложение у меня accept должен происходить только в одном месте?

                                      Во-вторых, а чтение/запись, а sigfd — я должен писать разный код для разных потоков?

                                      И, честно говоря, теперь уже я вас не понимаю, если вам это не нужно и вы не сталкивались с необходимостью — это не ведь не значит, что не нужно никому?


                                      1. mayorovp
                                        27.06.2018 21:41

                                        Просто без многопоточного accept можно написать высоконагруженное приложение, а с ограничением на количество сокетов — нет.

                                        А значит, какой бы замечательной ни была фича которая позволяет делать многопоточный accept — основной она точно не является.


                                        1. maquefel
                                          27.06.2018 21:46

                                          А вы умеете полностью комментарии читать?

                                          Фича не в многопоточном accept, для которого еще кстати нужно кое-что сделать.

                                          Фича в простой баллансировке обработки событий между потоками.

                                          Вы поняли из данной статьи, как это можно сделать с помощью epoll?
                                          Хотя бы многопоточный accept сможете сделать на основе данной статьи?


                                          1. netch80
                                            28.06.2018 10:33

                                            > Фича в простой баллансировке обработки событий между потоками.

                                            Вы можете хотя бы в общих словах описать задачи, где это идёт на пользу?

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


                                    1. netch80
                                      28.06.2018 10:30

                                      У maquefel@ не только accept. Он подразумевает существенную пользу от того, что будет, например, на поступление чего-то по TCP каналу реагировать из первой попавшейся нити, которой достался этот факт поступления, минимально тревожа остальные, и заложил это в основу всей позиции, которую доказывает тут.

                                      И вот именно это показывает специфику его задач. Я ни разу ещё не сталкивался с задачей, где бы контекст работы был настолько маленьким, а оперативность реакции — настолько важной, чтобы оправдать затраты на перемещение данных между ядрами на каждый мельчайший чих. Минимальный контекст, который трогается задачей на один такой вызов, это несколько килобайт, обычно выше. Пока эти данные будут ползти между ядрами по шине синхронизации… позеленеть можно, но не дождаться.

                                      (Интересно послушать, что же это за задачи у него.)

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


                                      1. maquefel
                                        28.06.2018 11:32

                                        Я предлагаю не разводить здесь простыню (уже очень неудобно читать стало). Я сейчас пишу статью (с вашей подачи кстати) — в чем я вижу отличия и особенности epoll.

                                        Я вас туда приглашу и можем продолжить дисскусию там.


  1. GoodGod
    26.06.2018 11:45

    Спасибо за статью. Я например программист PHP, но иногда приходится залезать в исходники apache, nginx (для понимания почему в логах какая-то ошибка). В целом, конечно, в будущем планирую выучить системное программирование c++ под Linux, но статья для меня отличная — простым и понятным языком разобраны основные (и практически используемые!) технологии работы с сокетами в Linux. Быстро просмотрел и сразу получил информацию (что значит select, почему технологий работы с сокетами несколько, какая самая последняя и т.д.) — все классно.


    1. dlinyj
      26.06.2018 12:12

      Лучше прочитайте любую книжку по системному программированию — больше пользы.


      1. GoodGod
        26.06.2018 12:24
        +1

        Вы не поняли суть комментария:
        1. Я поблагодарил автора, сказал что мне понравилась статья.
        2. Немного описал чем я занимаюсь (это для пункта 3).
        3. С учетом пункта 2, я сказал что конкретно мне понравилось в статье и почему.


        1. dlinyj
          26.06.2018 12:31
          +2

          Скорее это вы не поняли моего комментария. Всё что сказано в статье, более лаконично и точно изложено в любой книге. В том числе и ограничение на 1024 дескриптора


          1. GoodGod
            26.06.2018 12:46

            Давайте приложим в статью вашу бесплатную книгу по системному программированию Linux, где в 9,5 экранов будет более лаконично и точно рассказано про select, poll, epoll и libevent. Я думаю это будет полезно мне.

            tango это кстати идея — брать бесплатные книги и переводить и из них тоже.


            1. dlinyj
              26.06.2018 12:50
              -1

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


              1. GoodGod
                26.06.2018 13:11
                +2

                Статья несет не меньшую пользу чем статья «Программирование для Palm в 2017 году» и давайте не обсуждать вопрос что из этого является более или менее ненужным, а знаете почему мы с вами к этому пришли? Дали бы вы просто ссылку на книжку под моим комментарием — и вопросов бы не возникло.


                1. dlinyj
                  26.06.2018 13:14
                  +1

                  Из русского лучше чем: «Linux. Системное программирование» Роберт Лав, посоветовать не могу. Хотя книжек по теме очень много. Если тема будет актуально, скину список своей библиотеки, так сказать маст хев.

                  Там значительно короче и подробнее описано. Можно использовать, как справочник.

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


            1. maquefel
              26.06.2018 12:55

              Бесплатно есть man. Его вполне достаточно.
              А все таки лучше раскошелиться например на эту книгу, вложения окупятся.

              man7.org/tlpi

              Брать только на английском.


  1. zelyony
    26.06.2018 12:56

    можно еще почитать/написать про IOCP и Registered I/O в Windows (последнее в Win8+/2012+)


    1. hdfan2
      26.06.2018 14:40

      А также про WSAAsyncSelect (своеобразная хрень, посылающая сообщения о завершении тех или иных асинхронных операций прямо в основную очередь сообщений). Доступна с Win2000.


  1. robert_ayrapetyan
    26.06.2018 16:58

    Интересуемшися данной темой рекомендую pod.tst.eu/http://cvs.schmorp.de/libev/ev.pod, автор другой популярной обертки. Там он весьма нелестно проходится по всем реализациям во всех ОС, интересно, исправлено ли хоть что-то с тех пор.


    1. maquefel
      26.06.2018 17:20

      libev не умеет EPOLLET.


  1. bgnx
    26.06.2018 19:07

    В статье не упомянут еще один способ асинхронного io — сигналы и реалтайм сигналы. Кто-нибудь знает чем они хуже epoll?


    1. Dima_Sharihin
      26.06.2018 19:29

      Если речь про posix signal() — то, емнип, он не потокобезопасен. То есть предназначен максимум, чтобы просемафорить программе


      1. avdx
        26.06.2018 20:03

        Речь о realtime сигналах (sig >= SIGRTMIN) путем установки:
        fcntl(fd, F_SETSIG, sig)
        для дескриптора
        и последующем получении информации об I/O событиях через sigwaitinfo()/sigtimedwait()
        Здесь как раз отсутствуют проблемы poll()/select() выраженные в квадратичной сложности от количества сокетов. А realtime сигналы избавлены от проблем обычных сигналов.
        В свое время (10 лет назад) реализовывал на этом сервер. Все работало без проблем.


      1. netch80
        27.06.2018 17:02

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

        Но вот сама ситуация, когда тебя прерывают в произвольном месте кода, чтобы в очень ограниченном по возможностям контексте что-то сделать — просто неудобна. Самое практичное, что может сделать обработчик сигнала в долговременно работающей программе — поставить пару флагов типа sig_atomic_t и выйти. Даже longjmp() уже сомнителен, ибо нет гарантии, что подружится с, например, RAII и исключениями С++. Тогда зачем, если через signalfd можно просто узнать приход сигнала в основном цикле?
        И зачем это делать, если через epoll c компанией можно узнать о готовности объекта по дескриптору — напрямую, а не через промежуточный механизм посылки сигнала?


  1. edo1h
    27.06.2018 01:50
    +2

    Вторая причина более экзотична и имеет отношению к тому факту, что select может (теоретически) работать с таймаутами порядка одной наносекунды (если позволит аппаратная часть), в то время как и poll и epoll поддерживают лишь миллисекундную точность

    неправда же, микросекунды (10^-6) и миллисекунды (10^-3) соотвественно.

    Максимальное количество одновременно наблюдаемых дескрипторов ограниченно константой FD_SETSIZE, которая в линуксе жестко равна 1024

    на самом деле ограничение более жёсткое: нельзя использовать select с файловыми дескрипторами, у которых номер больше FD_SETSIZE (благо, ядро повторно использует номера fd, так что обычно это не является проблемой)

    Он спроектирован намного лучше и не страдает от большинства недостатков метода select.

    по большому счёту он решает только проблему отслеживания кучи файловых дескрипторов, но решает её неудачно (объём передаваемых данных из/в userspace только вырос, линейный обход списка дескрипторов вынуждено присутствует и в ядре, и в userspace).

    честно скажу — я использую select, и не стыжусь этого. мне он банально привычнее, при этом я не вижу существенных преимуществ poll.


  1. da-nie
    27.06.2018 07:16

    Но всё же в мире есть системы реального времени, имеющие такие таймеры.


    Вот была у меня проблема в QNX 6.3: «если в функции select указать в структуре времени ожидания нули (вообще не ждать), то минут через 50 загрузка процессора возрастёт до 100%. Но если задать, скажем, 1 мкс, то всё работает как часы. Конечно, время ожидания на select в худшем случае будет с пару-тройку системных тиков, но тик у меня 0.1 мс, так что приемлимо. „


  1. saipr
    28.06.2018 13:03
    +1

    Эту бы статью да лет 20 назад. И конечно poll вместо select очень помог.