EPOLL(7)

ИМЯ
epoll - средство уведомления о событии ввода-вывода

ОБЗОР
#include <sys/epoll.h>

ОПИСАНИЕ
Программный интерфейс epoll выполняется схожую с poll(2) задачу: следит за несколькими файловыми дескрипторами и ждёт, когда станет возможен ввод-вывод с одним из них. Программный интерфейс epoll можно использовать либо в режиме edge-triggered, либо в level-triggered и применять для слежения за достаточно большим количеством файловых дескрипторов. Для создания и управления экземпляром epoll служат следующие системные вызовы:

*   Вызов epoll_create(2) создаёт экземпляр epoll и возвращает файловый дескриптор, указывающий на этот экземпляр (более новый epoll_create1(2) расширяет возможности epoll_create(2)).

*   Затем с помощью epoll_ctl(2) регистрируются интересующие файловые дескрипторы. Список зарегистрированных в экземпляре epoll файловых дескрипторов называется набором epoll.

*   Вызов epoll_wait(2) ждёт наступления событий ввода-вывода, блокируя вызывающую нить, если событий уже не произошло.

  Режимы level-triggered и edge-triggered
Существует два режима выдачи событий epoll: edge-triggered (ET) и level-triggered (LT). Разницу между ними можно описать так. Предположим, что реализован следующий сценарий событий:

1.  Файловый дескриптор, представляющий читающую сторону канала (rfd), регистрируется в экземпляре epoll.

2.  Пишущая сторона канала записывает 2КБ данных на записываемой стороне канала.

3.  Вызов epoll_wait(2) завершается и возвращает rfd как готовый файловый дескриптор.

4.  Читающая сторона канала считывает 1КБ данных из rfd.

5.  Вызов epoll_wait(2) завершается.

Если файловый дескриптор 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):

    i   неблокирующие файловые дескрипторы; и

    ii  ожидание события только после того, как read(2) или write(2) возвратят EAGAIN.

Напротив, при использовании интерфейса level-triggered (по умолчанию, если не указан EPOLLET) epoll проще и быстрее poll(2), и может быть использован везде, где используется последний, так как имеет ту же семантику. Так как даже с edge-triggered epoll при получении нескольких порций данных могут генерироваться множественные события, вызывающий может задать флаг EPOLLONESHOT, который указывает epoll отключить связанный файловый дескриптор после приёма события с помощью epoll_wait(2). Если указан флаг EPOLLONESHOT, то вызывающий должен переустановить файловый дескриптор с помощью epoll_ctl(2) с флагом EPOLL_CTL_MOD.

  Взаимодействие с autosleep
Если система в режиме autosleep посредством /sys/power/autosleep и происходит событие, которое пробуждает устройство, то драйвер устройства держит устройство проснувшимся только, пока событие ставится в очередь. Чтобы устройство не заснуло пока не обработает событие, необходимо использовать флаг epoll(7) EPOLLWAKEUP. Флаг EPOLLWAKEUP задаётся в поле events для struct epoll_event; система будет оставаться разбуженной с момента когда событие поступает в очередь, пока не закончится работа вызова epoll_wait(2), возвращающий событие, и до последующего вызова epoll_wait(2). Если событие должно держать систему разбуженной дольше, то нужно применить отдельный wake_lock перед вторым вызовом epoll_wait(2).

  Интерфейсы /proc
Для ограничения потребления epoll памяти ядра, можно использовать следующие интерфейсы:

/proc/sys/fs/epoll/max_user_watches (начиная с Linux 2.6.28)
    Задаёт ограничение на общее количество файловых дескрипторов, которые пользователь может зарегистрировать во всех экземплярах epoll в системе. Ограничение привязывается к реальному идентификатору пользователя. Каждый зарезервированный файловый дескриптор занимает, приблизительно, 90 байт в 32-битном ядре, и, приблизительно, 160 байт в 64-битном ядре. В настоящее время, значение по умолчанию для max_user_watches равно 1/25 (4%) доступной памяти ядра (low memory), поделённое на значение размера дескриптора в байтах.

  Примеры использования
При применении 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_pwait");
        exit(EXIT_FAILURE);
    }
    for (n = 0; n < nfds; ++n) {
        if (events[n].data.fd == listen_sock) {
            conn_sock = accept(listen_sock,
                            (struct sockaddr *) &local, &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.

  Вопросы и ответы

Q0  Как различать зарегистрированные файловые дескрипторы в наборе epoll?

A0  Уникальной комбинацией является номер файлового дескриптора и описание открытого файла (так называемый «описатель открытого файла» — внутреннее представление открытого файла в ядре).

Q1  Что случится, если зарегистрировать один файловый дескриптор в экземпляре epoll дважды?

A1  Вероятно, вы получите EEXIST. Однако возможно добавить дубликат дескриптора (dup(2), dup2(2), fcntl(2) F_DUPFD) в тот же экземпляр epoll. Это может быть полезно для фильтрующих событий, если дубликаты файловых дескрипторов регистрируются с разными масками events.

Q2  Могут ли два экземпляра epoll ожидать один файловый дескриптор? Если да, то сообщаются ли события в оба файловых дескриптора epoll?

A2  Да, и события будут доходить в оба. Однако, чтобы сделать это правильно, нужна внимательность к деталям.

Q3  Могут ли операции poll/epoll/select применяться к самому файловому дескриптору epoll?

A3  Да. Если файловый дескриптор epoll имеет ожидающие события, то он будет помечен как доступный для чтения.

Q4  Что случится, если попытаться поместить файловый дескриптор epoll в свой собственный набор файловых дескрипторов?

A4  Вызов epoll_ctl(2) завершится ошибкой (EINVAL). Однако вы можете добавлять файловый дескриптор epoll внутрь другого набора файлового дескриптора epoll.

Q5  Можно ли отправить файловый дескриптор epoll через доменный сокет UNIX другому процессу?

A5  Да, но это не имеет смысла, так как принимающий процесс не имеет копий файловых дескрипторов в наборе epoll.

Q6  Приводит ли закрытие файлового дескриптора к его автоматическому удалению из всех наборов epoll?

A6  Да, но учтите следующее. Файловый дескриптор — это ссылка на описание открытого файла (смотрите open(2)). Чем бы не была сделана копия дескриптора — dup(2), dup2(2), fcntl(2) F_DUPFD или fork(2) — новый файловый дескриптор ссылается на одно и же созданное когда-то описание открытого файла. Описание открытого файла продолжает существовать до тех пор, пока все ссылающиеся на него файловые дескрипторы не будут закрыты. Файловый дескриптор удаляется из набора epoll только после того, как все файловые дескрипторы, ссылающиеся на нижележащее описание открытого файла, будут закрыты (или раньше, если дескриптор явно удаляется с помощью epoll_ctl(2) EPOLL_CTL_DEL). Это означает, что даже после того, как файловый дескриптор — часть набора epoll — был закрыт, события по этому файловому дескриптору могут продолжать приходить, если другие файловые дескрипторы ссылаются на то же нижележащее описание файла, оставшегося открытым.

Q7  Если между вызовами epoll_wait(2) придёт более одного события, то они будут объединены или о них будет сообщено по отдельности?

A7  Они будут объединены.

Q8  Влияет ли операция над файловым дескриптором на уже собранные, но пока ещё не сообщенные события?

A8  Вы можете выполнить две операции на существующем файловом дескрипторе. Удаление в этом случае бессмысленно. Изменение приведёт к повторному чтению доступного ввода/вывода.

Q9  Должен ли я читать/записывать файловый дескриптор до пор пока, не получу EAGAIN при использовании флага EPOLLET (поведение edge-triggered)?

A9  Получение события от epoll_wait(2) должно сообщить вам, что файловый дескриптор готов для запрошенной операции ввода/вывода. Вы должны предполагать, что он готов до тех пор, пока вы не получите следующий EAGAIN от (неблокирующего) чтения/записи. Когда и как вы будете использовать файловый дескриптор — полностью зависит от вас.

    Для пакетных/метко ориентированных файлов (например, датаграмных сокетов, терминал в каноническом режиме) единственным способом обнаружить конец чтения/записи пространства ввода-вывода — это продолжать чтение/записи до получения EAGAIN.

    Для потокоориентированных файлов (например, каналы, FIFO, потоковые сокеты) условие, при которых чтение/запись пространства ввода/вывода закончилось, может быть определено проверкой количества считанных/записанных данных из/в целевого файлового дескриптора. Например, если вы вызвали read(2) для чтения определённого количества данных и read(2) вернул меньшее количество байтов, то можно быть уверенным, что пространство чтения ввода/вывода этого файлового дескриптора закончилось. То же самое справедливо для записи посредством write(2) (не используйте последнее, если вы не можете гарантировать, что отслеживаемый файловый дескриптор всегда ссылается на потокоориентированный файл).

  Возможные ловушки и способы их обхода

o Информационный голод (edge-triggered)

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

Решением будет поддержка списка готовности и маркировка файлового дескриптора как готового в связанной с ним структуре данных, тем самым позволяя приложению запоминать какие файлы требуют обработки, но всё ещё не обработанных среди уже готовых файлов. Это также поддерживает игнорирование последующих событий готовности файловых дескрипторов, получаемых вами.

o Если использовать кэш событий…

Если вы используете кэш событий или храните все файловые дескрипторы, возвращённые от 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_create(2), epoll_create1(2), epoll_ctl(2), epoll_wait(2)