Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
В предыдущих двух материалах (часть 1, часть 2) этой серии речь шла об общих вопросах работы
Как уже было сказано, функция
Сначала
После этого
Потом функция проверяет возникшее событие на предмет того, является ли оно именно тем событием, наблюдение за которым пользователь поручил
Теперь
Так как мы удерживаем блокировку структуры
После этого
Далее, функция
После этого функция
В
Расскажу о том, как
Объявление функции
Легко заметить то, что данная функция использует различные подходы к работе в зависимости от того, блокирующим или неблокирующим должен быть вызов
Прежде чем в
После этого, когда процесс начинает выполняться (вне зависимости от того, что инициировало его выполнение: тайм-аут, сигнал, новое полученное событие),
Функция
Если функция не обнаружила событий, и если не истёк тайм-аут, что может произойти в том случае, если функция была активирована преждевременно, она просто возвращается в
Неблокирующая версия функции (
На этом мы завершаем третью часть цикла материалов о реализации
Часто ли вам приходится, разбираясь с какой-нибудь проблемой, добираться до исходного кода используемых вами опенсорсных инструментов?
epoll
, и о том, как epoll
получает уведомления о новых событиях от файловых дескрипторов, за которыми наблюдает. Здесь я расскажу о том, как epoll
хранит уведомления о событиях, и о том, как эти уведомления получают приложения, работающие в пользовательском режиме.Функция ep_poll_callback()
Как уже было сказано, функция
ep_insert()
прикрепляет текущий экземпляр epoll
к очереди ожидания файлового дескриптора, за которым осуществляется наблюдение, и регистрирует ep_poll_callback()
в качестве функции возобновления работы процесса в соответствующей очереди. Как выглядит ep_poll_callback()
? Узнать об этом можно, заглянув в строку 1002 файла fs/eventpoll.c
(тут приведён лишь самый важный для нас код):static int ep_poll_callback(wait_queue_t *wait, unsigned mode, int sync, void *key)
{
int pwake = 0;
unsigned long flags;
struct epitem *epi = ep_item_from_wait(wait);
struct eventpoll *ep = epi->ep;
Сначала
ep_poll_callback()
пытается получить структуру epitem
, связанную с файловым дескриптором, который вызвал ep_poll_callback()
с использованием ep_item_from_wait()
. Вспомните о том, что раньше мы называли структуру eppoll_entry
«связующим звеном», поэтому получение реального epitem
выполняется путём выполнения простых операций с указателями:static inline struct epitem *ep_item_from_wait(wait_queue_t *p)
{
return container_of(p, struct eppoll_entry, wait)->base;
}
После этого
ep_poll_callback()
блокирует структуру eventpoll
:spin_lock_irqsave(&ep->lock, flags);
Потом функция проверяет возникшее событие на предмет того, является ли оно именно тем событием, наблюдение за которым пользователь поручил
epoll
. Помните о том, что функция ep_insert()
регистрирует коллбэк с маской событий, установленной в ~0U
. У этого есть две причины. Первая — пользователь может часто менять состав отслеживаемых событий через epoll_ctl()
, а перерегистрация коллбэка не особенно эффективна. Второе — не все файловые системы обращают внимание на маску события, поэтому использование масок — это не слишком надёжно.if (key && !((unsigned long) key & epi->event.events))
goto out_unlock;
Теперь
ep_poll_callback()
проверяет, пытается ли экземпляр epoll
передать сведения о событии в пользовательское пространство (с помощью epoll_wait()
или epoll_pwait()
). Если это так, то ep_poll_callback()
прикрепляет текущую структуру epitem
к односвязному списку, голова которого хранится в текущей структуре eventpoll
:if (unlikely(ep->ovflist != EP_UNACTIVE_PTR)) {
if (epi->next == EP_UNACTIVE_PTR) {
epi->next = ep->ovflist;
ep->ovflist = epi;
if (epi->ws) {
__pm_stay_awake(ep->ws);
}
}
goto out_unlock;
}
Так как мы удерживаем блокировку структуры
eventpoll
, то при выполнении этого кода, даже в SMP-окружении, не может возникнуть состояние гонок.После этого
ep_poll_callback()
проверяет, находится ли уже текущая структура epitem
в очереди готовых файловых дескрипторов. Это может произойти в том случае, если у программы пользователя не было возможности вызвать epoll_wait()
. Если такой возможности и правда не было, то ep_poll_callback()
добавит текущую структуру epitem
в очередь готовых файловых дескрипторов, которая представлена членом rdllist
структуры eventpoll
.if (!ep_is_linked(&epi->rdllink)) {
list_add_tail(&epi->rdllink, &ep->rdllist);
ep_pm_stay_awake_rcu(epi);
}
Далее, функция
ep_poll_callback()
вызывает процессы, ожидающие в очередях wq
и poll_wait
. Очередь wq
используется самой реализацией epoll
в том случае, когда пользователь запрашивает информацию о событиях с применением epoll_wait()
, но время ожидания пока не истекло. А poll_wait
используется epoll-реализацией операции poll()
файловой системы. Помните о том, что за событиями файловых дескрипторов epoll
тоже можно наблюдать!if (waitqueue_active(&ep->wq))
wake_up_locked(&ep->wq);
if (waitqueue_active(&ep->poll_wait))
pwake++;
После этого функция
ep_poll_callback()
освобождает блокировку, которую она захватила ранее, и активирует poll_wait
, очередь ожидания poll()
. Обратите внимание на то, что мы не можем активировать очередь ожидания poll_wait
во время удержания блокировки, так как существует возможность добавления файлового дескриптора epoll
в его собственный список файловых дескрипторов, за которыми осуществляется наблюдение. Если сначала не освободить блокировку — это может привести к ситуации взаимной блокировки.Член rdllink структуры eventpoll
В
epoll
используется очень простой способ хранения готовых файловых дескрипторов. Но, на всякий случай, я о нём расскажу. Речь идёт о члене rdllink
структуры eventpoll
, который является головой двусвязного списка. Узлы этого списка — это самостоятельные структуры epitem
, у которых имеются произошедшие события.Функции epoll_wait() и ep_poll()
Расскажу о том, как
epoll
передаёт список файловых дескрипторов при вызове epoll_wait()
программой пользователя. Функция epoll_wait()
(файл fs/eventpoll.c
, строка 1963) устроена очень просто. Она выполняет проверку на наличие ошибок, получает структуру eventpoll
из поля private_data
файлового дескриптора и вызывает ep_poll()
для решения задачи по копированию событий в пользовательское пространство. В оставшейся части этого материала я уделю основное внимание именно ep_poll()
.Объявление функции
ep_poll()
можно найти в строке 1588 файла fs/eventpoll.c
. Вот фрагменты кода этой функции:static int ep_poll(struct eventpoll *ep, struct epoll_event __user *events, int maxevents, long timeout)
{
int res = 0, eavail, timed_out = 0;
unsigned long flags;
long slack = 0;
wait_queue_t wait;
ktime_t expires, *to = NULL;
if (timeout > 0) {
struct timespec end_time = ep_set_mstimeout(timeout);
slack = select_estimate_accuracy(&end_time);
to = &expires;
*to = timespec_to_ktime(end_time);
} else if (timeout == 0) {
timed_out = 1;
spin_lock_irqsave(&ep->lock, flags);
goto check_events;
}
Легко заметить то, что данная функция использует различные подходы к работе в зависимости от того, блокирующим или неблокирующим должен быть вызов
epoll_wait()
. Если вызов является блокирующим (timeout > 0
), то функция вычисляет end_time
на основе предоставленного ей значения timeout
. Если вызов должен быть неблокирующим (timeout == 0
), то функция переходит прямо к блоку кода, соответствующего метке check_events:
, о котором мы поговорим ниже.Блокирующая версия
fetch_events:
spin_lock_irqsave(&ep->lock, flags);
if (!ep_events_available(ep)) {
init_waitqueue_entry(&wait, current);
__add_wait_queue_exclusive(&ep->wq, &wait);
for (;;) {
set_current_state(TASK_INTERRUPTIBLE);
if (ep_events_available(ep) || timed_out)
break;
if (signal_pending(current)) {
res = -EINTR;
break;
}
spin_unlock_irqrestore(&ep->lock, flags);
if (!schedule_hrtimeout_range(to, slack, HRTIMER_MODE_ABS))
timed_out = 1; /* resumed from sleep */
spin_lock_irqsave(&ep->lock, flags);
}
__remove_wait_queue(&ep->wq, &wait);
set_current_state(TASK_RUNNING);
}
Прежде чем в
fetch_events:
начнут выполняться какие-то действия, нужно захватить блокировку текущей структуры eventpoll
. А иначе, если мы вызовем для проверки наличия новых событий ep_events_available(ep)
, у нас будут неприятности. Если новых событий нет, то функция добавит текущий процесс в очередь ожидания ep
, о которой мы говорили выше. Затем функция установит состояние текущей задачи как TASK_INTERRUPTIBLE
, освободит блокировку и сообщит планировщику о необходимости перепланировки, но при этом и установит таймер ядра для перепланировки текущего процесса по истечению заданного промежутка времени или в том случае, если он получит какой-нибудь сигнал.После этого, когда процесс начинает выполняться (вне зависимости от того, что инициировало его выполнение: тайм-аут, сигнал, новое полученное событие),
ep_poll()
опять захватывает блокировку eventpoll
, убирает себя из очереди ожидания wq
, возвращает состояние задачи в значение TASK_RUNNING
и проверяет, получила ли она что-нибудь интересное. Это делается в блоке check_events:
.Блок check_events:
Функция
ep_poll()
, всё ещё удерживая блокировку, проверяет, имеются ли некие события, о которых нужно сообщить. После этого она освобождает блокировку:check_events:
eavail = ep_events_available(ep);
spin_unlock_irqrestore(&ep->lock, flags);
Если функция не обнаружила событий, и если не истёк тайм-аут, что может произойти в том случае, если функция была активирована преждевременно, она просто возвращается в
fetch_events:
и продолжает ждать. В противном случае функция возвращает значение res
:if (!res && eavail && !(res = ep_send_events(ep, events, maxevents)) && !timed_out)
goto fetch_events;
return res;
Неблокирующая версия
Неблокирующая версия функции (
timeout == 0
) очень проста. При её использовании сразу осуществляется переход к метке check_events:
. Если событий на момент вызова функции не было, она не ждёт поступления новых событий.Итоги
На этом мы завершаем третью часть цикла материалов о реализации
epoll
. В следующей части мы поговорим о том, как события копируются в пространство пользователя, и о том, как приходится поступать этой реализации epoll
при использовании механизмов срабатывания по фронту и срабатывания по уровню.Часто ли вам приходится, разбираясь с какой-нибудь проблемой, добираться до исходного кода используемых вами опенсорсных инструментов?