Русский
Русский
English
Статистика
Реклама

Epoll

Перевод Реализация epoll, часть 1

20.10.2020 16:06:30 | Автор: admin
Сегодня мы публикуем перевод первой статьи из серии материалов, посвящённых реализации epoll в ядре Linux 3.16.1*. Автор исходит из предположения о том, что читатели знакомы с API и с использованием epoll. Он уделяет основное внимание реализации подсистемы epoll в ядре Linux, а не особенностям её применения. Если вы не знаете о том, как пользоваться epoll автор рекомендует сначала почитать документацию. Это значительно облегчит понимание деталей реализации этого механизма.



* Linux 3.16.1 достаточно старое ядро, но информация работы с epoll актуальна и сегодня (прим. переводчика).

Что такое epoll?


Epoll это несколько системных вызовов, предоставляемых ядром Linux и предназначенных для эффективной организации мультиплексирования ввода-вывода. Благодаря тому, как спроектирована виртуальная файловая система (VFS, Virtual File System) Linux, с любым опрашиваемым файлом, или, точнее, с файлом, реализующим файловую операцию poll(), можно работать, используя epoll. Среди таких файлов можно отметить файлы сокетов, которые в наши дни вызывают наибольший интерес разработчиков.

Старые методы работы


Традиционно программисты использовали для мультиплексирования ввода-вывода select или poll. Но и то и другое было спроектировано уже очень давно, во времена, когда сетевым службам приходилось работать лишь с тысячами одновременных клиентских подключений. Select и poll очень похожи. На самом деле, и тот и другой механизмы реализованы в одном и том же файле в репозитории ядра Linux (fs/select.c). Их, работа, кроме того, организована весьма просто. Приложение генерирует массив файловых дескрипторов (file descriptor, fd), которые его интересуют. Затем приложение выполняет системный вызов, обращаясь к ядру. Ядро копирует массив из пользовательского пространства и обходит дескрипторы, проверяя, с использованием файловой операции poll(), наличие новых событий. После этого select просто генерирует битовый массив и копирует его обратно в пользовательское пространство. А poll напрямую работает со структурой pollfd прямо в пользовательском пространстве, не копируя её.

Проблема


Можно заметить, что реализация select и poll указывает на то, что эти механизмы плохо приспособлены к обработке больших количеств файловых дескрипторов. Временная сложность алгоритмов, лежащих в основе обоих этих механизмов, это O(n). С этим не было никаких проблем до тех пор, пока число тех, кто пользуется интернетом, было сравнительно небольшим. В наши дни вполне обычной является ситуация, когда серверам приходится поддерживать 100000 одновременных подключений. Хотя и можно обрабатывать такие количества подключений, пользуясь select и poll, весьма вероятно то, что ценное процессорное время будет растрачиваться на выполнение опросов огромного количества файловых дескрипторов. Это сильно подействует на масштабируемость и доступность сервера. Данную проблему можно решить с помощью механизма, который способен уведомлять нас о событиях дескрипторов, делая это более интеллектуально. Именно такой вот интеллектуальный механизм уведомлений и реализован в epoll.

Обзор


Прежде чем мы углубимся в исходный код ядра Linux, давайте разберёмся с тем, как, в общих чертах, работает epoll.

Главное отличие epoll от традиционных механизмов мультиплексирования ввода-вывода заключается в следующем. Приложение, вместо того чтобы каждый раз создавать и передавать ядру огромный массив, просто берёт экземпляр epoll и регистрирует в нём файловые дескрипторы. А epoll, вместо того чтобы опрашивать все файловые дескрипторы из массива, осуществляет мониторинг зарегистрированных дескрипторов и докладывает приложению о событиях тогда, когда приложение обращается за такими сведениями. Этот процесс очень эффективен в тех случаях, когда сравнительно невелико соотношение файловых дескрипторов, в которых возникли события, к общему числу отслеживаемых файловых дескрипторов. Так как временная сложность алгоритмов, лежащих в основе механизмов epoll, представлена константой, эти механизмы очень легко справляются с обработкой нескольких сотен тысяч открытых TCP-соединений.

Экземпляр epoll


Экземпляр epoll это сердце подсистемы epoll. В Linux экземпляр epoll можно запросить, выполнив команду epoll_create(2) или команду epoll_create1(2). Обе команды возвращают файловый дескриптор. Причина того, что в качестве ссылки на экземпляр epoll используется файловый дескриптор, заключается в том, что это позволяет опрашивать экземпляр epoll. Благодаря такому подходу можно использовать продвинутые схемы работы с epoll, в ходе реализации которых, например, экземпляры epoll можно мониторить с использованием epoll, select или даже poll. Но самая важная часть экземпляра epoll это внутренняя структура данных struct eventpoll, объявленная в коде ядра, в 180 строке файла fs/eventpoll.c. Эта структура данных отвечает за поддержку всех тех механизмов, которые нужны epoll для правильной работы. Код, который выделяет память под struct eventpoll и возвращает файловый дескриптор, можно найти в файле fs/eventpoll.c, в строке 1767. Вот фрагмент этого файла:

/** Создание внутренней структуры данных ("struct eventpoll").*/error = ep_alloc(&ep);

Команда ep_alloc() просто выделяет из кучи ядра память, объём которой достаточен для хранения struct eventpoll, и инициализирует эту память.

После этого epoll_create() пытается получить у процесса неиспользуемый файловый дескриптор:

/** Создание всего что нужно для настройки файла eventpoll. То есть -* файловой структуры и свободного файлового дескриптора.*/fd = get_unused_fd_flags(O_RDWR | (flags & O_CLOEXEC));

Если epoll_create() удалось получить файловый дескриптор, то будет сделана попытка получить у системы анонимный inode. Обратите внимание на то, что epoll_create() сохраняет указатель на ранее выделенную память под struct eventpoll в поле файла private_data. Так как любые системные вызовы, работающие с экземпляром epoll, обращаются к нему с использованием номера файлового дескриптора экземпляра, это крайне упрощает и делает весьма эффективным повторное получение struct eventpoll, выполняемое позже:

file = anon_inode_getfile("[eventpoll]", &eventpoll_fops, ep,O_RDWR | (flags & O_CLOEXEC));

После этого epoll_create связывает анонимный inode с файловым дескриптором и возвращает файловый дескриптор вызывающему процессу:

fd_install(fd, file);return fd;

Как экземпляр epoll запоминает файловые дескрипторы?


Экземпляр epoll, по очевидным причинам, должен как-то запоминать файловые дескрипторы, наблюдение за которыми ему поручили. Для этого применяется структура данных, которая часто используется в ядре Linux. Это красно-чёрное дерево (КЧ-дерево, Red-black tree, RB-Tree), в котором хранятся файловые дескрипторы, за которыми наблюдает конкретный экземпляр epoll. Корень дерева представлен членом rbr структуры eventpoll, он инициализируется в функции ep_alloc().

В красно-чёрном дереве для каждого файлового дескриптора, за которым наблюдает экземпляр epoll, создаётся соответствующий элемент struct epitem. В struct epitem находятся некоторые важные структуры данных, которые будут использоваться epoll при наблюдении за жизненным циклом соответствующего файлового дескриптора.

Когда для добавления файлового дескриптора в экземпляр epoll используется команда epoll_ctl(2), ядро сначала использует ep_find() в попытке найти структуру epitem, соответствующую этому файлу (строка 973 файла fs/eventpoll.c).

Так как красно-чёрное дерево это двоичное дерево поиска, то оказывается, что, прежде чем сохранять в нём элементы epitem, им нужно назначать ключи, содержащие данные, которые можно использовать в операциях сравнения. В случае с epoll ключи элементов, хранящихся в КЧ-дереве, представлены структурами epoll_filefd, хранящимися в epitem. Структуры epoll_filefd устроены очень просто, код их объявления можно найти в файле fs/eventpoll.c, в строке 106. Вот этот код с моими комментариями:

struct epoll_filefd {struct file *file; // указатель на структуру целевого файла, соответствующий fdint fd; // номер дескриптора целевого файла} __packed;

Функция, которая выполняет сравнение значений, носит имя ep_cmp_ffd() (файл fs/eventpoll.c, строка 326):

/* Сравнение ключей красно-чёрного дерева */static inline int ep_cmp_ffd(struct epoll_filefd *p1,struct epoll_filefd *p2){return (p1->file > p2->file ? +1:(p1->file < p2->file ? -1 : p1->fd - p2->fd));}

Сначала функция ep_cmp_ffd() сравнивает с имеющимися данными адрес памяти из struct file. Большей считается структура с большим адресом.

Если адреса памяти совпадают (что возможно, так как несколько файловых дескрипторов могут ссылаться на один и тот же элемент struct file, например, благодаря dup()), то ep_cmp_ffd() просто сочтёт, что файл с большим файловым дескриптором больше. Такой подход гарантирует то, что функция ep_cmp_ffd() способна сравнить любые два файловых дескриптора, которые не эквивалентны друг другу. Кроме того, если два файловых дескриптора идентичны, ep_cmp_ffd() вернёт 0.

После того, как объявлена функция сравнения, поиск файловых дескрипторов в красно-чёрном дереве не отличается от поиска узла в любом двоичном дереве поиска.

Предположим, мы пытаемся добавить в экземпляр epoll файловый дескриптор. После того, как успешно отработает функция ep_find(), epoll_ctl() узнает о том, что ep_find() ничего не нашла. В противном случае работа ep_find() будет завершена с errno, установленным в EEXIST.

После этого epoll_ctl() вызовет ep_insert() для добавления в КЧ-дерево нового файлового дескриптора, а так же для настройки некоторых структур данных и коллбэков, необходимых для получения уведомлений о событиях. Подробности о ep_insert() читайте в следующей статье.



Подробнее..

Перевод Реализация epoll, часть 2

05.11.2020 16:06:36 | Автор: admin
Публикуя перевод первой статьи из цикла материалов о реализации epoll, мы провели опрос, посвящённый целесообразности перевода продолжения цикла. Более 90% участников опроса высказались за перевод остальных статей. Поэтому сегодня мы публикуем перевод второго материала из этого цикла.



Функция ep_insert()


Функция ep_insert() это одна из самых важных функций в реализации epoll. Понимание того, как она работает, чрезвычайно важно для того чтобы разобраться в том, как именно epoll получает сведения о новых событиях от файлов, за которыми наблюдает.

Объявление ep_insert() можно найти в строке 1267 файла fs/eventpoll.c. Рассмотрим некоторые фрагменты кода этой функции:

user_watches = atomic_long_read(&ep->user->epoll_watches);if (unlikely(user_watches >= max_user_watches))  return -ENOSPC;

В этом фрагменте кода функция ep_insert() сначала проверяет, не превышает ли общее количество файлов, за которым наблюдает текущий пользователь, значения, заданного в /proc/sys/fs/epoll/max_user_watches. Если user_watches >= max_user_watches, то функция немедленно прекращает работу с errno, установленным в ENOSPC.

После этого ep_insert() выделяет память, пользуясь механизмом управления памятью slab ядра Linux:

if (!(epi = kmem_cache_alloc(epi_cache, GFP_KERNEL)))  return -ENOMEM;

Если функции удалось выделить объём памяти, достаточный для struct epitem, будет выполнен следующий процесс инициализации:

/* Инициализация ... */INIT_LIST_HEAD(&epi->rdllink);INIT_LIST_HEAD(&epi->fllink);INIT_LIST_HEAD(&epi->pwqlist);epi->ep = ep;ep_set_ffd(&epi->ffd, tfile, fd);epi->event = *event;epi->nwait = 0;epi->next = EP_UNACTIVE_PTR;

После этого ep_insert() попытается зарегистрировать коллбэк в файловом дескрипторе. Но прежде чем мы сможем об этом поговорить, нам нужно познакомиться с некоторыми важными структурами данных.

Структура poll_table это важная сущность, используемая реализацией poll() VFS. (Понимаю, что в этом можно запутаться, но тут мне хотелось бы объяснить, что функция poll(), которую я тут упомянул, представляет собой реализацию файловой операции poll(), а не системный вызов poll()). Она объявлена в include/linux/poll.h:

typedef struct poll_table_struct {  poll_queue_proc _qproc;  unsigned long _key;} poll_table;

Сущность poll_queue_proc представляет тип функции-коллбэка, который выглядит так:

typedef void (*poll_queue_proc)(struct file *, wait_queue_head_t *, struct poll_table_struct *);

Член _key таблицы poll_table, на самом деле, является не тем, чем он может показаться на первый взгляд. А именно, несмотря на имя, наводящее на мысль о некоем ключе, в _key, на самом деле, хранятся маски интересующих нас событий. В реализации epoll _key устанавливается в ~0 (дополнение до 0). Это значит, что epoll стремится получать сведения о событиях любых видов. В этом есть смысл, так как приложения пользовательского пространства могут в любое время менять маску событий с помощью epoll_ctl(), принимая все события от VFS, а затем фильтруя их в реализации epoll, что упрощает работу.

Для того чтобы облегчить восстановление в poll_queue_proc исходной структуры epitem, epoll использует простую структуру, называемую ep_pqueue, которая служит обёрткой для poll_table с указателем на соответствующую структуру epitem (файл fs/eventpoll.c, строка 243):

/* Структура-обёртка, используемая в очереди */struct ep_pqueue {  poll_table pt;  struct epitem *epi;};

Затем ep_insert() инициализирует struct ep_pqueue. Следующий код сначала записывает в член epi структуры ep_pqueue указатель на структуру epitem, соответствующую файлу, который мы пытаемся добавить, а затем записывает ep_ptable_queue_proc() в член _qproc структуры ep_pqueue, а в _key записывает ~0.

/* Инициализация таблицы с использованием коллбэка */epq.epi = epi;init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);

После этого ep_insert() вызовет ep_item_poll(epi, &epq.pt);, что приведёт к вызову реализации poll(), связанной с файлом.

Давайте рассмотрим пример, в котором используется реализация poll() TCP-стека Linux, и разберёмся с тем, что именно эта реализация делает с poll_table.

Функция tcp_poll() это реализация poll() для TCP-сокетов. Её код можно найти в файле net/ipv4/tcp.c, в строке 436. Вот фрагмент этого кода:

unsigned int tcp_poll(struct file *file, struct socket *sock, poll_table *wait){  unsigned int mask;  struct sock *sk = sock->sk;  const struct tcp_sock *tp = tcp_sk(sk);  sock_rps_record_flow(sk);  sock_poll_wait(file, sk_sleep(sk), wait);  // код опущен}

Функция tcp_poll() вызывает sock_poll_wait(), передавая, в качестве второго аргумента, sk_sleep(sk), а в качестве третьего wait (это ранее переданная функции tcp_poll() таблица poll_table).

А что представляет собой sk_sleep()? Как оказывается, это всего лишь геттер, предназначенный для доступа к очереди ожидания событий для конкретной структуры sock (файл include/net/sock.h, строка 1685):

static inline wait_queue_head_t *sk_sleep(struct sock *sk){  BUILD_BUG_ON(offsetof(struct socket_wq, wait) != 0);  return &rcu_dereference_raw(sk->sk_wq)->wait;}

Что же sock_poll_wait() собирается делать с очередью ожидания событий? Оказывается, что эта функция выполнит некую простую проверку и потом вызовет poll_wait() с передачей тех же самых параметров. Затем функция poll_wait() вызовет заданный нами коллбэк и передаст ему очередь ожидания событий (файл include/linux/poll.h, строка 42):

static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p){  if (p && p->_qproc && wait_address)    p->_qproc(filp, wait_address, p);}

В случае с epoll сущность _qproc будет представлять собой функцию ep_ptable_queue_proc(), объявленную в файле fs/eventpoll.c, в строке 1091.

/** Это - коллбэк, используемый для включения нашей очереди ожидания в* состав списков процессов целевого файла, работу которых надо возобновить.*/static void ep_ptable_queue_proc(struct file *file, wait_queue_head_t *whead,       poll_table *pt){  struct epitem *epi = ep_item_from_epqueue(pt);  struct eppoll_entry *pwq;  if (epi->nwait >= 0 && (pwq = kmem_cache_alloc(pwq_cache, GFP_KERNEL))) {    init_waitqueue_func_entry(&pwq->wait, ep_poll_callback);    pwq->whead = whead;    pwq->base = epi;    add_wait_queue(whead, &pwq->wait);    list_add_tail(&pwq->llink, &epi->pwqlist);    epi->nwait++;  } else {    /* Нам нужно сообщить о возникновении ошибки */    epi->nwait = -1;  }}

Сначала ep_ptable_queue_proc() пытается восстановить структуру epitem, которая соответствует файлу из очереди ожидания, с которым мы работаем. Так как epoll использует структуру-обёртку ep_pqueue, восстановление epitem из указателя poll_table представлено простой операцией с указателями.

После этого ep_ptable_queue_proc() просто выделяет столько памяти, сколько нужно для struct eppoll_entry. Эта структура работает как связующее звено между очередью ожидания файла, за которым ведётся наблюдение, и соответствующей структурой epitem для этого файла. Для epoll чрезвычайно важно знать о том, где находится голова очереди ожидания для файла, за которым ведётся наблюдение. В противном случае epoll не сможет позже отменить регистрацию в очереди ожидания. Структура eppoll_entry, кроме того, включает в себя очередь ожидания (pwq->wait) с функцией возобновления работы процесса, представленной ep_poll_callback(). Возможно, pwq->wait это самая важная часть во всей реализации epoll, так как эту сущность используют для решения следующих задач:

  1. Мониторинг событий, происходящих с конкретным файлом, за которым осуществляется наблюдение.
  2. Возобновление работы других процессов в том случае, если возникает такая необходимость.

После этого ep_ptable_queue_proc() присоединит pwq->wait к очереди ожидания целевого файла (whead). Функция, кроме того, добавит struct eppoll_entry в связный список из struct epitem (epi->pwqlist) и инкрементирует значение epi->nwait, представляющее собой длину списка epi->pwqlist.

А вот тут у меня возникает один вопрос. Почему epoll нужно использовать связный список для хранения структуры eppoll_entry внутри структуры epitem одного файла? Не нужен ли epitem лишь один элемент eppoll_entry?

Я, правда, не могу точно ответить на этот вопрос. Насколько я могу судить, если только некто не собирается использовать экземпляры epoll в каких-нибудь безумных циклах, список epi->pwqlist будет содержать лишь один элемент struct eppoll_entry, а epi->nwait для большинства файлов, скорее всего, будет равняться 1.

Хорошо тут то, что неясности вокруг epi->pwqlist никак не отражаются на том, о чём я буду говорить ниже. А именно, речь пойдёт о том, как Linux уведомляет экземпляры epoll о событиях, происходящих с файлами, за которыми осуществляется наблюдение.

Помните то, о чём мы говорили в предыдущем разделе? Речь шла о том, что epoll присоединяет wait_queue_t к списку ожидания целевого файла (к wait_queue_head_t). Несмотря на то, что wait_queue_t чаще всего используется как механизм возобновления работы процессов, это, по сути, просто структура, хранящая указатель на функцию, которая будет вызвана тогда, когда Linux решит возобновить работу процессов из очереди wait_queue_t, прикреплённую к wait_queue_head_t. В этой функции epoll может принять решение о том, что делать с сигналом возобновления работы, но у epoll нет необходимости возобновлять работу какого-либо процесса! Как можно будет увидеть позже, обычно при вызове ep_poll_callback() возобновления работы чего-либо не происходит.

Полагаю, ещё стоит обратить внимание на то, что механизм возобновления работы процессов, применяемый в poll(), полностью зависит от реализации. В случае с файлами TCP-сокетов голова очереди ожидания это член sk_wq, сохранённый в структуре sock. Это, кроме того, объясняет необходимость использования коллбэка ep_ptable_queue_proc() для работы с очередью ожидания. Так как в реализациях очереди для разных файлов голова очереди может оказаться в совершенно разных местах, у нас нет способа обнаружить нужное нам значение wait_queue_head_t без использования коллбэка.

Когда именно осуществляется возобновление работы sk_wq в структуре sock? Как оказалось, система сокетов Linux следует тем же OO-принципам проектирования, что и VFS. Структура sock объявляет следующие хуки в строке 2312 файла net/core/sock.c:

void sock_init_data(struct socket *sock, struct sock *sk){  // код опущен...  sk->sk_data_ready  =   sock_def_readable;  sk->sk_write_space =  sock_def_write_space;  // код опущен...}

В sock_def_readable() и sock_def_write_space() осуществляется вызов wake_up_interruptible_sync_poll() для (struct sock)->sk_wq с целью выполнения функции-коллбэка, возобновляющей работу процесса.

Когда же будут вызываться sk->sk_data_ready() и sk->sk_write_space()? Это зависит от реализации. Рассмотрим, в качестве примера, TCP-сокеты. Функция sk->sk_data_ready() будет вызвана во второй половине обработчика прерывания в том случае, когда TCP-подключение завершит процедуру трёхстороннего рукопожатия, или когда для некоего TCP-сокета будет получен буфер. Функция sk->sk_write_space() будет вызвана при изменении состояния буфера с full на available. Если помнить об этом при разборе следующих тем, особенно той, что посвящена срабатыванию по фронту, эти темы будут выглядеть интереснее.

Итоги


На этом мы завершаем второй материал из цикла статей о реализации epoll. В следующий раз поговорим о том, чем именно занимается epoll в коллбэке, зарегистрированном в очереди возобновления выполнения процессов сокета.

Пользовались ли вы epoll?



Подробнее..

Перевод Реализация epoll, часть 4

09.11.2020 20:17:29 | Автор: admin
Это последний материал из серии четырёх статей (часть 1, часть 2, часть 3), посвящённой реализации 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. Возникло новое событие.
  4. Ничего не произошло, а планировщик просто решил активировать процесс.

В сценариях 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 за то, что они, выкладывая результаты своей работы в общий доступ, делятся своими знаниями со всеми, кто в них нуждается.

Как вы относитесь к опенсорсному программному обеспечению?



Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru