[Перевод] Реализация epoll, часть 4
Это — последний материал из серии четырёх статей (часть 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 функция устанавливает соответствующие флаги и выходит из цикла ожидания. В последнем случае функция просто снова переходит в режим ожидания.
После того, как эта часть работы сделана, 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 за то, что они, выкладывая результаты своей работы в общий доступ, делятся своими знаниями со всеми, кто в них нуждается.
Как вы относитесь к опенсорсному программному обеспечению?