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

ссылка на оригинал статьи https://habr.com/ru/company/ruvds/blog/526802/


Добавить комментарий