Это — последний материал из серии четырёх статей (часть 1, часть 2, часть 3), посвящённой реализации
Эта статья написана позже остальных. Когда я начинал работу над первым материалом, самой свежей стабильной версией ядра Linux была 3.16.1. А во время написания данной статьи это уже версия 4.1. Именно на коде этой версии ядра и основана данная статья. Код, правда, изменился не особенно сильно, поэтому читатели предыдущих статей могут не беспокоиться о том, что что-то в реализации
В предыдущих материалах я потратил довольно много времени на объяснение того, как работает система обработки событий в ядре. Но, как известно, ядру надо передать сведения о событиях программе, работающей в пользовательском пространстве для того чтобы программа могла бы воспользоваться этими сведениями. Это, в основном, делается с помощью системного вызова epoll_wait(2).
Код этой функции можно найти в строке 1961 файла
Функция
Если же пользователь задал значение
Затем функция переходит в режим ожидания, вызывая
В сценариях 1, 2 и 3 функция устанавливает соответствующие флаги и выходит из цикла ожидания. В последнем случае функция просто снова переходит в режим ожидания.
После того, как эта часть работы сделана,
В этом блоке сначала проверяется наличие событий, а затем выполняется следующий вызов, где и происходит самое интересное.
Функция
Функция
Зачем авторы
После этого
Тут, правда, стоит упомянуть об одной детали. Функция
После проверки маски события
Теперь мы наконец можем обсудить разницу между срабатыванием по фронту (Edge Triggering, ET) и срабатыванием по уровню (Level Triggering, LT) с точки зрения особенностей их реализации.
Это очень просто! Функция
После того, как
Когда функция
На этом я завершаю четвёртую и последнюю статью из цикла, посвящённого реализации
Как вы относитесь к опенсорсному программному обеспечению?
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 за то, что они, выкладывая результаты своей работы в общий доступ, делятся своими знаниями со всеми, кто в них нуждается.Как вы относитесь к опенсорсному программному обеспечению?