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