предисловие
«Сетевое программирование UNIX» не упоминает epoll, я не знаю почему, следующее содержание резюмируется в соответствии с руководством по Linux.
Введение в API
epoll — это механизм, предусмотренный в Linux для реализации мультиплексирования ввода-вывода. Подобно опросу, epoll может отслеживать несколько дескрипторов одновременно; epoll добавляет концепции граничного и горизонтального срабатывания и имеет преимущества при работе с большим количеством дескрипторов.
Основной концепцией epoll API является экземпляр epoll, представляющий собой структуру данных в ядре.С точки зрения пользователя его можно просто рассматривать как содержащий два списка:
- список интересов (или набор epoll): набор дескрипторов интересов, зарегистрированных пользователем
- готовый список: набор готовых дескрипторов.Когда IO будет готов, ядро автоматически добавит готовые дескрипторы в готовый список
API epoll состоит из трех системных вызовов:
epoll_create
int epoll_create(int size);
int epoll_create1(int flags);
epoll_create
Создайте экземпляр epoll, функция вернет дескриптор, указывающий на экземпляр epoll, вы должны вызвать close, чтобы закрыть экземпляр epoll после использования. Параметр размера аналогичен емкости карты и определяет количество дескрипторов, поддерживаемых экземпляром epoll.
epoll_create1
иepoll_create
Аналогично, но параметр становится флагом, а размер игнорируется. Здесь есть необязательная опция для флагов:EPOLL_CLOEXEC
,EPOLL_CLOEXEC
означает набор на созданном дескриптореFD_CLOEXEC
логотип.
epoll_ctl
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
/* Valid opcodes ( "op" parameter ) to issue to epoll_ctl(). */
#define EPOLL_CTL_ADD 1 /* Add a file decriptor to the interface. */
#define EPOLL_CTL_DEL 2 /* Remove a file decriptor from the interface. */
#define EPOLL_CTL_MOD 3 /* Change file decriptor epoll_event structure. */
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
};
struct epoll_event
{
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
}
epoll_ctl
Зарегистрируйте дескриптор и интересующее событие в экземпляре epoll.Эта функция эквивалентна добавлению дескриптора в список интересов экземпляра epoll. Возвращает 0, если работа функции выполнена успешно, в противном случае возвращает -1 и устанавливает errno.
epoll_wait
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
epoll_wait
Это заблокирует ожидание событий ввода-вывода, что можно понимать как получение дескриптора из списка готовых. Функция возвращает количество готовых описаний и сохраняет готовые дескрипторы в параметре events.Тайм-аут в миллисекундах можно установить через тайм-аут, а -1 означает никогда не тайм-аут.
Срабатывание по фронту и по горизонтали
Существует много вводных сведений о запуске по фронту и по горизонтали, так что вот перевод содержания руководства пользователя.
epoll предоставляет два механизма срабатывания: по фронту (ET) и по уровню (LT).Разницу между ними можно проиллюстрировать следующими примерами:
- Предположим, у нас уже есть дескриптор
rfd
, мы прочитаем из него вывод канала, мы зарегистрируем его в экземпляре epoll, и интересующие нас события станут доступны для чтения - Конец записи канала записывает 2 КБ данных в канал.
- процесс называется
epoll_wait
, тогдаrfd
будет помещен в список готовых, а затем успешно возвращен - Конец чтения канала считывает 1 КБ данных из канала.
- Обработка вызовов снова
epoll_wait
еслиrfd
Используется при регистрации в экземпляре epollEPOLLET
вариант, то шаг 5 выше вызываетepoll_wait
Блокировка может произойти, хотя в буфере чтения в это время еще есть читаемые данные, в то же время другой конец пайпа может ждать соответствующего ответа, так что он застрял в бесконечном ожидании друг друга. Причина этого явления в том, что ЭТ происходит только вИзменения дескрипторасобытие будет возвращено. В приведенном выше примере шаг 2 генерирует событие, а шаг 3 использует это событие. Поскольку на шаге 4 не были прочитаны все данные, шаг 5 может застрять в блокировке на неопределенный срок.
И использование, запускаемое по краю, предложенное руководством Linux, выглядит следующим образом:
- Использование с неблокирующими дескрипторами
- пока каждый раз
read
илиwrite
возвращениеEAGAIN
ждите следующего события
В отличие от запуска по краю, когда используется параметр запуска по горизонтали, epoll представляет собой обновленную версию опроса, которая может просто заменить опрос.
В целом, разница между ET и LT заключается в том, что условия для срабатывания событий различны: LT больше соответствует мышлению программирования (срабатывает, если выполняются условия), а условия срабатывания ET более жесткие (срабатывает только при наличии события). изменения) Требования пользователя также выше, и теоретическая эффективность выше. Стоит отметить, что селектор java nio будет использовать разные реализации в зависимости от разных операционных систем: в linux 2.6 и более поздних версиях используется epoll, использующий горизонтальный запуск, и дополнительные, предусмотренные в netty.EpollEventLoop
Используется запуск по фронту.
При прослушивании событий дескриптора несколько событий могут происходить последовательно для одного и того же дескриптора, что является настройкой, выбираемой пользователем.EPOLLONESHOT
Возможность уведомить epoll, чтобы отключить последующие события. если установленоEPOLLONESHOT
Опция, пользователю необходимо повторно зарегистрировать событие после обработки события. Этот параметр более полезен в параллельных средах.
Когда несколько процессов или потоков одновременно прослушивают дескриптор экземпляра epoll, используйтеEPOLLET
Эта опция может гарантировать, что только один процесс или поток будет уведомлен о каждом событии, избегая таких проблем, как «шокированное стадо».
Ограничение прослушивания epoll
/proc/sys/fs/epoll/max_user_watches
Конфигурация ограничивает общее количество дескрипторов, которые один и тот же пользователь может прослушивать во всех экземплярах epoll.
Пример использования запуска по фронту
Поскольку использование запуска по горизонтали и опроса не сильно отличается, здесь приведен только пример запуска по фронту:
#define MAX_EVENTS 10
struct epoll_event ev, events[MAX_EVENTS];
int listen_sock, conn_sock, nfds, epollfd;
/* 此处省略调用listen_sock调用socket、bind和listen的过程 */
//创建epoll实例,程序最后应该调用close关闭epollfd
epollfd = epoll_create1(0);
if (epollfd == -1)
{
perror("epoll_create1");
exit(EXIT_FAILURE);
}
ev.events = EPOLLIN; //感兴趣的事件为读事件
ev.data.fd = listen_sock; //注册fd为监听套接字
//注册event
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1)
{
perror("epoll_ctl: listen_sock");
exit(EXIT_FAILURE);
}
for (;;)
{
//等待描述符就绪,参数-1表示不超时
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)
{
//监听套接字就绪,调用accept建立连接
conn_sock = accept(listen_sock,
(struct sockaddr *)&addr, &addrlen);
if (conn_sock == -1)
{
perror("accept");
exit(EXIT_FAILURE);
}
//设置新连接为非阻塞模式(ET下必须设置非阻塞)
setnonblocking(conn_sock);
//感兴趣的事件为读事件,同时设置为边缘触发
ev.events = EPOLLIN | EPOLLET;
//注册fd为新建立的连接描述符
ev.data.fd = conn_sock;
//注册event
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock,
&ev) == -1)
{
perror("epoll_ctl: conn_sock");
exit(EXIT_FAILURE);
}
} else {//新建立的连接就绪
//do_use_fd应该对fd进行read或者write直到EAGAIN,然后记录当前的read或write进度,等到下次就绪后再继续
do_use_fd(events[n].data.fd);
}
}
}
В режиме с запуском по фронту, если вы хотите не действовать немедленно при поступлении события, а дождаться готовности других условий перед выполнением чтения или записи, вы можете зарегистрироваться в то же времяEPOLLIN|EPOLLOUT
Мероприятие для повышения производительности, не повторяется вызовepoll_ctl
пройти черезEPOLL_CTL_MOD
существуетEPOLLIN和EPOLLOUT
Переключение вперед и назад, что невозможно, если вы находитесь в горизонтальном режиме, потому что интересующее событие будет продолжать происходить после того, как событие будет готово, что приведет к ненужному потреблению.
Почему epoll быстрее опроса
Во введении epoll упоминается, что epoll быстрее, чем опрос.Согласно другим блогам в Интернете, резюмируются следующие причины:
- При ожидании готовности дескриптора нет необходимости каждый раз передавать набор дескрипторов в ядро, достаточно зарегистрировать дескриптор в экземпляре epoll, а экземпляр epoll внутренне поддерживает все наборы дескрипторов.
- Красно-черное дерево и область кэша ядра используются внутри экземпляра epoll для поддержки набора дескрипторов, что повышает эффективность операций регистрации и удаления набора дескрипторов.
- epoll внутренне поддерживает список готовности с помощью механизма обратного вызова. Когда дескриптор готов, поместите его в список готовности.При вызове epoll_wait вам нужно только оценить, пуст ли список готовности.Если он не пуст, скопируйте список готовности в пользовательское пространство и очистите список готовности, в противном случае , он заснет.
- Нет необходимости повторно проходить все дескрипторы, когда есть готовые дескрипторы, epoll напрямую вернет готовый набор дескрипторов.
Кстати, вот реализация LT,epoll_wait
Перед возвратом к готовому дескриптору будет проверен тип триггера дескриптора.Если это горизонтальный триггер и на дескрипторе есть необработанные данные, он будет добавлен в только что опустевший список готовых, чтобы следующий вызовepoll_wait
Готовый список по-прежнему будет иметь дескриптор. Это также является фактической причиной разницы в производительности LT и ET.