Skip to content

Latest commit

 

History

History
138 lines (96 loc) · 13.6 KB

README.md

File metadata and controls

138 lines (96 loc) · 13.6 KB

Мультиплексирование ввода-вывода

Неблокирующий ввод-вывод

Системные вызовы read и write блокируют выполнение текущего потока выполнения в случае отсутствия данных или места в буферах ввода или вывода.

В случае, когда процесс работает одновременно с несколькими файловыми дескрипторами, такое поведение может стать узким местом в производительности.

Для того, чтобы избежать простоя на операциях ввода-вывода, предусмотрен аттрибут файлового дескриптора O_NONBLOCK, который можно установить как при открытии файла, так и для уже открытого файлового дескриптора с помощью системного вызова fcntl:

int fd = ....;                   // какой-то файловый дескриптор
int flags = fcntl(fd, F_GETFL);  // получить предыдущие флаги открытия/создания
flags |= O_NONBLOCK;             // добавить флаг неблокируемости
fcntl(fd, F_SETFL, flags);       // установить новые флаги

Попытка чтения из файлового дескриптора, если в буфере нет данных, или записи в файловый дескриптор, если в буфере нет места, приведет к ошибке. То есть, системный вызов read или write завершит работу со значением -1, и при этом в переменной errno будет зафиксировано значение EAGAIN.

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

Обработка событий в Linux

Если основное назначение программы - это обработка данных из файловых дескрипторов, то неблокирующий ввод-вывод может приводить к 100% загрузке процессора даже в том случае, когда с процессом не происходит никакого взаимодействия. Для того, чтобы это избежать, необходимо ставить процесс в состояние ожидания до тех пор, пока не возникнет какое-либо событие, связанное с одним из файловых дескрипторов, требующее реакции.

Такой механизм является системно-зависимым (вне стандарта POSIX); в системе Linux он реализуется через механизм epoll(7), в системах FreeBSD и MacOS - через системный вызов kqueue. Эти механизмы реализованы идентично, и различаются только в API.

Очередь ядра

Очередь ядра в системе Linux создается с помощью системного вызова epoll_create или epoll_create1. Результатом является файловый дескриптор (который должен быть закрыт, когда очередь станет не нужна), который связан с некоторым специальным объектом - очередью ядра, в которую складываются события по некоторому фильтру.

Событие описывается структурой epoll_event:

#include <sys/epoll.h>

typedef union {
    void*       ptr;
    int         fd;
    uint32_t    u32;
    uint64_t    u64;
} epoll_data_t;

struct epoll_event {
    uint32_t        events; // маска событий
    epoll_data_t    data;   // произвольное поле, заполненное при регистрации
};

Маска событий - это набор из различных флагов:

  • EPOLLIN - готовность к чтению;
  • EPOLLOUT - готовность к записи;
  • EPOLLHUP - соединение разорвано;
  • EPOLLERR - ошибка ввода-вывода.

Ожидание событий

Перевести процесс в ожидание поступления событий можно с помощью системного вызова epoll_wait или epoll_pwait.

struct epoll_event events[MaxEventsToRead];
int N = epoll_wait(
                   int epoll_fd,  // дескриптор, созданный epoll_create
                   struct epoll_event *events, // куда прочитать события
                   int MaxEventsToRead, // размер массива для чтения
                   int timeout // таймаут в миллисекундах, или -1
                  );

Системный вызов epoll_wait ожидает появление хотя бы одного события из зарегистрированных на наблюдение, блокируя выполнение текущего потока. Поскольку за время простоя процесса или потока может появиться несколько событий, то значение переменной N после выполнения epoll_wait может быть больше 1, - это количество событий, которые были прочитаны в массив events.

Если был указан параметр timeout в значение, отличное от -1, либо во время ожидания поступил сигнал, то значение N может стать равным -1, в errno будет записано значение EINTR.

Системный вызов epoll_pwait дополнительно принимает ещё один аргумент - маску сигналов, которые нужно блокировать на время ожидания.

После выполнения epoll_wait, в массив events будет записано N структур epoll_event, которые содержат описание того, что произошло с наблюдаемыми файловыми дескрипторами. При этом, если с одним и тем же файловым дескриптором произошло несколько событий, то они группируются в одно событие.

Регистрация файловых событий для отслеживания изменений

Для того, чтобы события, связанные с определенными файловыми дескрипторами, отслеживались и попадали в очередь ядра, их необходимо явно зарегистрировать с помощью epoll_ctl:

epoll_ctl(
          int epoll_fd, // дескриптор, созданный epoll_create
          int op, // одна из операций:
                  //  - EPOLL_CTL_ADD - добавить дескриптор в наблюдение
                  //  - EPOLL_CTL_MOD - модицифировать параметры наблюдения
                  //  - EPOLL_CTL_DEL - убрать дескриптор из наблюдения
          int fd, // дескриптор, события над которым нас интересуют
          struct epoll_event *event // структура, в которой описаны:
                                    //  - маска интересуемых событий events
                                    //  - произвольные данные, которые
                                    //    as-is попадают в структуру, читаемую
                                    //    epoll_wait
         );

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

Если файловый дескриптор был закрыт, то он автоматически удаляется из наблюдаемых файловых дескприпторов.

Отслеживание по изменению фронта (Edge-Triggerd) v.s. отслеживание по состоянию (Level-Triggered)

Описание физической аналогии приведено в статье Edge Triggered v.s. Level Triggered Interrupts (на англ.).

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

Регистрация обработки событий в режим Eddge-Triggered возможна с указанием флага EPOLLET. В этом случае повышается скорость реакции на событие за счет того, что событие регистрируется в тот момент, когда оно только начинает быть готовым, например в буфер ввода начали поступать данные. Недостатком этого подхода является то, что регистрируется только факт изменения состояния, и если, например, не прочитать данные из буфера полностью, то в следующий раз событие готовности к чтению не будет обнаружено, т.к. оно уже произошло.

Событийно-ориентированное программирование

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

Пара виртуальных сокетов

Пара виртуальных сокетов создается с помощью системного вызова socketpair, который, по аналогии с pipe, заполняет массив из двух целочисленных значений. Эти файловые дескрипторы могут быть использованы для взаимодействия родственных процессов, от отличаются от неименованных каналов тем, что:

  1. Являются двунаправленными
  2. Их можно настраивать через setsockopts, как обычные сокеты
  3. Обрабатывается отдельная операция "завершения соединения", которая, в случае в epoll регистрируется как событие EPOLLIN с 0 количеством байт.
// Пример:
int pair[2];
socketpair(AF_UNIX,     // в Linux поддерживается только UNIX,
           SOCK_STREAM, // еще можно SOCK_DGRAM
           0,           // автоматический выбор протокола
           pair         // массив из 2-х int, куда будут записаны дескрипторы
          );

SignalFD, TimerFD, EventFD

Системные вызовы signalfd, timerfd_create, и eventfd реализованы только в Linux, и создают специальные файловые дескрипторы, из которых можно читать события:

  • поступления определенных сигналов (signalfd);
  • срабатывание таймера (timerfd_create);
  • уведомления, пересылаемые разными потоками (read и write через eventfd).