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 за то, что они, выкладывая результаты своей работы в общий
доступ, делятся своими знаниями со всеми, кто в них нуждается.Как вы относитесь к опенсорсному программному обеспечению?