Реализация epoll, часть 3

в 13:15, , рубрики: epoll, linux, ruvds_перевод, Блог компании RUVDS.com, Программирование, разработка, Разработка под Linux, системное программирование, системные вызовы

В предыдущих двух материалах (часть 1, часть 2) этой серии речь шла об общих вопросах работы epoll, и о том, как epoll получает уведомления о новых событиях от файловых дескрипторов, за которыми наблюдает. Здесь я расскажу о том, как epoll хранит уведомления о событиях, и о том, как эти уведомления получают приложения, работающие в пользовательском режиме.

Реализация epoll, часть 3 - 1

Функция 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 при использовании механизмов срабатывания по фронту и срабатывания по уровню.

Часто ли вам приходится, разбираясь с какой-нибудь проблемой, добираться до исходного кода используемых вами опенсорсных инструментов?

Реализация epoll, часть 3 - 2

Реализация epoll, часть 3 - 3

Автор: programmerguru

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js