Системные вызовы 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
.
В этом случае, можно попробовать выполнить операцию ввода или вывода с файловым дескриптором позже, а тем временем обработать другие файловые дескрипторы.
Если основное назначение программы - это обработка данных из файловых дескрипторов, то неблокирующий ввод-вывод может приводить к 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 Triggered v.s. Level Triggered Interrupts (на англ.).
По умолчанию события регистрируются в режиме отслеживания по значению, то есть, по факту наличия флага готовности операций ввода и вывода, если в буфере есть готовые данные или место для записи.
Регистрация обработки событий в режим Eddge-Triggered возможна с указанием флага EPOLLET
. В этом случае повышается скорость реакции на событие за счет того, что событие регистрируется в тот момент, когда оно только начинает быть готовым, например в буфер ввода начали поступать данные. Недостатком этого подхода является то, что регистрируется только факт изменения состояния, и если, например, не прочитать данные из буфера полностью, то в следующий раз событие готовности к чтению не будет обнаружено, т.к. оно уже произошло.
Помимо ввода-вывода, в системе Linux могут быть использованы другие файловые дескрипторы специального назначения, которые также как и обычные, можно регистрировать в наблюдение через epoll
.
Пара виртуальных сокетов создается с помощью системного вызова socketpair
, который, по аналогии с pipe
, заполняет массив из двух целочисленных значений. Эти файловые дескрипторы могут быть использованы для взаимодействия родственных процессов, от отличаются от неименованных каналов тем, что:
- Являются двунаправленными
- Их можно настраивать через
setsockopts
, как обычные сокеты - Обрабатывается отдельная операция "завершения соединения", которая, в случае в
epoll
регистрируется как событиеEPOLLIN
с 0 количеством байт.
// Пример:
int pair[2];
socketpair(AF_UNIX, // в Linux поддерживается только UNIX,
SOCK_STREAM, // еще можно SOCK_DGRAM
0, // автоматический выбор протокола
pair // массив из 2-х int, куда будут записаны дескрипторы
);
Системные вызовы signalfd
, timerfd_create
, и eventfd
реализованы только в Linux, и создают специальные файловые дескрипторы, из которых можно читать события:
- поступления определенных сигналов (
signalfd
); - срабатывание таймера (
timerfd_create
); - уведомления, пересылаемые разными потоками (
read
иwrite
черезeventfd
).