EPOLL(7) | Руководство программиста Linux | EPOLL(7) |
epoll - средство уведомления о событии ввода-вывода
#include <sys/epoll.h>
Программный интерфейс epoll выполняется схожую с poll(2) задачу: следит за несколькими файловыми дескрипторами и ждёт, когда станет возможен ввод-вывод для одного из них. Программный интерфейс epoll можно использовать либо в режиме edge-triggered, либо в level-triggered и применять для слежения за достаточно большим количеством файловых дескрипторов.
Центральным элементом программного интерфейса epoll является экземпляр epoll — структура данных ядра, которая с точки зрения пользовательского пространства может рассматриваться как контейнер с двумя списками:
Для создания и управления экземпляром epoll служат следующие системные вызовы:
Существует два режима выдачи событий epoll: edge-triggered (ET) и level-triggered (LT). Разницу между ними можно описать так. Предположим, что реализован следующий сценарий событий:
Если файловый дескриптор rfd добавлен к экземпляру epoll с указанным флагом EPOLLET (edge-triggered), то вызов epoll_wait(2) на шаге 5, вероятно, повиснет, несмотря на имеющие данные в буфере ввода; в это же время удалённая сторона может ожидать подтверждения приёма уже отправленных данных. Причиной этого является то, что в режиме edge-triggered события доставляются только когда происходит изменение состояния отслеживаемого файлового дескриптора. Поэтому в шаге 5 вызывающий может бесконечно ждать появления данных, хотя они уже есть в буфере ввода. В приведённом выше примере событие для rfd будет сгенерировано из-за операции записи, сделанной в шаге 2, и это событие будет обработано в шаге 3. Так как операция в шаге 4, не прочитала все данные из буфера, вызов epoll_wait(2) в шаге 5 может заблокироваться навсегда.
Приложение, которое применяет флаг EPOLLET, должно использовать неблокирующие файловые дескрипторы, чтобы избежать приостановки задания, обрабатывающего множество файловых дескрипторов, из-за блокировок чтения или записи. Предлагаемый способ использования epoll с интерфейсом Edge Triggered (EPOLLET):
Напротив, при использовании интерфейса level-triggered (по умолчанию, если не указан EPOLLET) epoll проще и быстрее poll(2), и может быть использован везде, где используется последний, так как имеет ту же семантику.
Так как даже с edge-triggered epoll при получении нескольких порций данных могут генерироваться множественные события, вызывающий может задать флаг EPOLLONESHOT, который указывает epoll отключить связанный файловый дескриптор после приёма события с помощью epoll_wait(2). Если указан флаг EPOLLONESHOT, то вызывающий должен переустановить файловый дескриптор с помощью epoll_ctl(2) с флагом EPOLL_CTL_MOD.
Если несколько нитей (или процессов, если дочерние процессы унаследовали файловый дескриптор epoll при fork(2)) блокируются в ожидании epoll_wait(2) одного и того же файлового дескриптора и файловый дескриптор в списке interest, помеченный для уведомления edge-triggered (EPOLLET), становится готовым, то только одна из нитей (или процессов) пробуждается из epoll_wait(2). Такая полезная оптимизация в некоторых случаях помогает избежать «лавины» пробуждений.
Если система в режиме autosleep посредством /sys/power/autosleep и происходит событие, которое пробуждает устройство, то драйвер устройства держит устройство проснувшимся только, пока событие ставится в очередь. Чтобы устройство не заснуло пока не обработает событие, необходимо использовать флаг epoll_ctl(2) EPOLLWAKEUP.
Флаг EPOLLWAKEUP задаётся в поле events для struct epoll_event; система будет оставаться разбуженной с момента когда событие поступает в очередь, пока не закончится работа вызова epoll_wait(2), возвращающий событие, и до последующего вызова epoll_wait(2). Если событие должно держать систему разбуженной дольше, то нужно применить отдельный wake_lock перед вторым вызовом epoll_wait(2).
Для ограничения потребления epoll памяти ядра, можно использовать следующие интерфейсы:
При применении epoll с интерфейсом level-triggered он имеет ту же семантику что и poll(2), а при edge-triggered требует больших проверок для избежания зависаний приложения в событийном цикле. В этом примере, слушающим является неблокирующий сокет, для которого был вызван listen(2). Функция do_use_fd() использует новый готовый файловый дескриптор до тех пор, пока не возвратится EAGAIN от read(2) или write(2). Приложение на основе машины состояний должно после получения EAGAIN записать своё текущее состояние так, чтобы последующий вызов do_use_fd() продолжил выполнять read(2) или write(2) с места остановки.
#define MAX_EVENTS 10 struct epoll_event ev, events[MAX_EVENTS]; int listen_sock, conn_sock, nfds, epollfd; /* Код для настройки слушающего сокета, 'listen_sock', (socket(), bind(), listen()) не показаны */ epollfd = epoll_create1(0); if (epollfd == -1) { perror("epoll_create1"); exit(EXIT_FAILURE); } ev.events = EPOLLIN; ev.data.fd = listen_sock; if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) { perror("epoll_ctl: listen_sock"); exit(EXIT_FAILURE); } for (;;) { nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1); if (nfds == -1) { perror("epoll_wait"); exit(EXIT_FAILURE); } for (n = 0; n < nfds; ++n) { if (events[n].data.fd == listen_sock) { conn_sock = accept(listen_sock, (struct sockaddr *) &addr, &addrlen); if (conn_sock == -1) { perror("accept"); exit(EXIT_FAILURE); } setnonblocking(conn_sock); ev.events = EPOLLIN | EPOLLET; ev.data.fd = conn_sock; if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock, &ev) == -1) { perror("epoll_ctl: conn_sock"); exit(EXIT_FAILURE); } } else { do_use_fd(events[n].data.fd); } } }
При использовании интерфейса edge-triggered для большей производительности можно однократно добавить файловый дескриптор внутрь интерфейса epoll (EPOLL_CTL_ADD), указав (EPOLLIN|EPOLLOUT). Это позволит вам избежать постоянного переключения между EPOLLIN и EPOLLOUT, вызывающими epoll_ctl(2) c EPOLL_CTL_MOD.
Если существует большое пространство ввода/вывода, то возможно, что пока вы его читаете, другие файлы не будут обрабатываться и возникнет недостаток данных (этого, обычно, не происходит с epoll).
Решением будет поддержка списка готовности и маркировка файлового дескриптора как готового в связанной с ним структуре данных, тем самым позволяя приложению запоминать какие файлы требуют обработки, но всё ещё не обработанных среди уже готовых файлов. Это также поддерживает игнорирование последующих событий готовности файловых дескрипторов, получаемых вами.
Если вы используете кэш событий или храните все файловые дескрипторы, возвращённые от epoll_wait(2), то убедитесь, что вы обеспечили способ его динамического закрытия (например, вызванное обработкой предыдущего события). Предположим, что вы получили 100 событий от epoll_wait(2), и что в событии №47 некоторое условие определяет, что событие №13 должно быть закрыто. Если вы удалите структуру и выполните close(2) файлового дескриптора для события №13, то кэш событий всё ещё может сообщать о том, что есть ожидаемые события для этого файлового дескриптора, что приводит к путнице.
Одним из решений будет вызов, во время обработки события №47, epoll_ctl(EPOLL_CTL_DEL) для удаления файлового дескриптора 13 и вызов close(2), а затем маркировка связанной с ним структуры данных как удалённой и связки его со списком очистки. Если при пакетной обработке найдется другое событие для файлового дескриптора 13, то обнаружится, что файловый дескриптор уже был удалён и конфликтов не будет.
Программный интерфейс epoll был добавлен в ядро Linux версии 2.5.44. Поддержка в glibc доступна с версии 2.3.2.
Программный интерфейс epoll есть только в Linux. В некоторых других системах есть подобные механизмы, например, в FreeBSD есть kqueue, а в Solaris — /dev/poll.
Набор файловых дескрипторов, которые отслеживаются через файловый дескриптор epoll, можно найти в записи для файлового дескриптора epoll в каталоге процесса /proc/[pid]/fdinfo. Подробности смотрите в proc(5).
Вызов The kcmp(2) с операцией KCMP_EPOLL_TFD можно использовать для проверки, что файловый дескриптор присутствует в экземпляре epoll.
epoll_create(2), epoll_create1(2), epoll_ctl(2), epoll_wait(2), poll(2), select(2)
2019-03-06 | Linux |