Это — последний материал из серии четырёх статей (часть 1, часть 2, часть 3), посвящённой реализации epoll. Тут речь пойдёт о том, как epoll передаёт события из пространства ядра в пользовательское пространство, и о том, как реализованы режимы срабатывания по фронту и по уровню.



Эта статья написана позже остальных. Когда я начинал работу над первым материалом, самой свежей стабильной версией ядра Linux была 3.16.1. А во время написания данной статьи это уже версия 4.1. Именно на коде этой версии ядра и основана данная статья. Код, правда, изменился не особенно сильно, поэтому читатели предыдущих статей могут не беспокоиться о том, что что-то в реализации epoll очень сильно изменилось.

Взаимодействие с пользовательским пространством


В предыдущих материалах я потратил довольно много времени на объяснение того, как работает система обработки событий в ядре. Но, как известно, ядру надо передать сведения о событиях программе, работающей в пользовательском пространстве для того чтобы программа могла бы воспользоваться этими сведениями. Это, в основном, делается с помощью системного вызова epoll_wait(2).

Код этой функции можно найти в строке 1961 файла fs/eventpoll.c. Сама эта функция очень проста. После вполне обычных проверок она просто получает указатель на eventpoll из файлового дескриптора и выполняет вызов следующей функции:

error = ep_poll(ep, events, maxevents, timeout);

Функция ep_poll()


Функция ep_poll() объявлена в строке 1585 того же файла. Она начинается с проверки того, задал ли пользователь значение timeout. Если так и было, то функция инициализирует очередь ожидания и устанавливает тайм-аут в значение, заданное пользователем. Если пользователь не хочет ждать, то есть, timeout = 0, то функция сразу же переходит к блоку кода с меткой check_events:, ответственному за копирование события.

Если же пользователь задал значение timeout, и событий, о которых ему можно сообщить, нет (их наличие определяют с помощью вызова ep_events_available(ep)), функция ep_poll() добавляет сама себя в очередь ожидания ep->wq (вспомните то, о чём мы говорили в третьем материале этой серии). Там мы упоминали о том, что ep_poll_callback() в процессе работы активирует любые процессы, ожидающие в очереди ep->wq.

Затем функция переходит в режим ожидания, вызывая schedule_hrtimeout_range(). Вот в каких обстоятельствах «спящий» процесс может «проснуться»:

  1. Истекло время тайм-аута.
  2. Процесс получил сигнал.
  3. Возникло новое событие.
  4. Ничего не произошло, а планировщик просто решил активировать процесс.

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

После того, как эта часть работы сделана, ep_poll() продолжает выполнять код блока check_events:.

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

ep_send_events(ep, events, maxevents)

Функция ep_send_events() объявлена в строке 1546. Она, после вызова, вызывает функцию ep_scan_ready_list(), передавая, в качестве коллбэка, ep_send_events_proc(). Функция ep_scan_ready_list() проходится в цикле по списку готовых файловых дескрипторов и вызывает ep_send_events_proc() для каждого найденного ей готового события. Ниже станет понятно, что механизм, предусматривающий применение коллбэка, нужен для обеспечения безопасности и многократного использования кода.

Функция ep_send_events() сначала помещает данные из списка готовых файловых дескрипторов структуры eventpool в свою локальную переменную. Затем она устанавливает поле ovflist структуры eventpool в NULL (а его значением по умолчанию является EP_UNACTIVE_PTR).

Зачем авторы epoll используют ovflist? Это сделано ради обеспечения высокой эффективности работы epoll! Можно заметить, что после того, как список готовых файловых дескрипторов был взят из структуры eventpool, ep_scan_ready_list() устанавливает ovflist в значение NULL. Это приводит к тому, что ep_poll_callback() не попытается присоединить событие, которое передаётся в пользовательское пространство, обратно к ep->rdllist, что может привести к большим проблемам. Благодаря использованию ovflist функции ep_scan_ready_list() не нужно удерживать блокировку ep->lock при копировании событий в пользовательское пространство. В результате улучшается общая производительность решения.

После этого ep_send_events_proc() обойдёт имеющийся у неё список готовых файловых дескрипторов и снова вызовет их методы poll() для того чтобы удостовериться в том, что событие действительно произошло. Зачем epoll снова проверяет здесь события? Делается это для того чтобы убедиться в том, что событие (или события), зарегистрированное пользователем, всё ещё доступно. Поразмыслите над ситуацией, когда файловый дескриптор был добавлен в список готовых файловых дескрипторов по событию EPOLLOUT в тот момент, когда пользовательская программа выполняет запись в этот дескриптор. После того, как программа завершит запись, файловый дескриптор уже может быть недоступным для записи. Epoll нужно правильно обрабатывать подобные ситуации. В противном случае пользователь получит EPOLLOUT в тот момент, когда операция записи будет заблокирована.

Тут, правда, стоит упомянуть об одной детали. Функция ep_send_events_proc() прилагает все усилия для того чтобы обеспечить получение программами из пространства пользователя точных уведомлений о событиях. При этом возможно, хотя и маловероятно, то, что доступный набор событий изменится после того, как ep_send_events_proc() вызовет poll(). В этом случае программа из пользовательского пространства может получить уведомление о событии, которого больше не существует. Именно поэтому правильным считается всегда использовать неблокирующие сокеты при применении epoll. Благодаря этому ваше приложение не будет неожиданно заблокировано.

После проверки маски события ep_send_events_proc() просто копирует структуру события в буфер, предоставленный программой пользовательского пространства.

Срабатывание по фронту и срабатывание по уровню


Теперь мы наконец можем обсудить разницу между срабатыванием по фронту (Edge Triggering, ET) и срабатыванием по уровню (Level Triggering, LT) с точки зрения особенностей их реализации.

else if (!(epi->event.events & EPOLLET)) {
    list_add_tail(&epi->rdllink, &ep->rdllist);
}

Это очень просто! Функция ep_send_events_proc() добавляет событие обратно в список готовых файловых дескрипторов. В результате при следующем вызове ep_poll() тот же файловый дескриптор будет снова проверен. Так как ep_send_events_proc() всегда вызывает для файла poll() перед возвратом его приложению пользовательского пространства, это немного увеличивает нагрузку на систему (в сравнении с ET) если файловый дескриптор больше не доступен. Но смысл этого всего заключается в том, чтобы, как сказано выше, не сообщать о событиях, которые больше недоступны.

После того, как ep_send_events_proc() завершит копирование событий, функция возвращает количество скопированных ей событий, держа в курсе происходящего приложение пользовательского пространства.

Когда функция ep_send_events_proc() завершила работу, функции ep_scan_ready_list() нужно немного прибраться. Сначала она возвращает в список готовых файловых дескрипторов события, которые остались необработанными функцией ep_send_events_proc(). Такое может произойти в том случае, если количество доступных событий превысит размеры буфера, предоставленного программой пользователя. Кроме того, ep_send_events_proc() быстро прикрепляет все события из ovflist, если таковые имеются, обратно к списку готовых файловых дескрипторов. Далее, в ovflist опять записывается EP_UNACTIVE_PTR. В результате новые события будут прикрепляться к главному списку ожидания (rdllist). Функция завершает работу, активируя любые другие «спящие» процессы в том случае, если имеются ещё какие-то доступные события.

Итоги


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

Как вы относитесь к опенсорсному программному обеспечению?