Резюме epoll о мультиплексировании ввода-вывода в Linux

Операционная система

предисловие

«Сетевое программирование 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).Разницу между ними можно проиллюстрировать следующими примерами:

  1. Предположим, у нас уже есть дескрипторrfd, мы прочитаем из него вывод канала, мы зарегистрируем его в экземпляре epoll, и интересующие нас события станут доступны для чтения
  2. Конец записи канала записывает 2 КБ данных в канал.
  3. процесс называетсяepoll_wait, тогдаrfdбудет помещен в список готовых, а затем успешно возвращен
  4. Конец чтения канала считывает 1 КБ данных из канала.
  5. Обработка вызовов сноваepoll_wait

еслиrfdИспользуется при регистрации в экземпляре epollEPOLLETвариант, то шаг 5 выше вызываетepoll_waitБлокировка может произойти, хотя в буфере чтения в это время еще есть читаемые данные, в то же время другой конец пайпа может ждать соответствующего ответа, так что он застрял в бесконечном ожидании друг друга. Причина этого явления в том, что ЭТ происходит только вИзменения дескрипторасобытие будет возвращено. В приведенном выше примере шаг 2 генерирует событие, а шаг 3 использует это событие. Поскольку на шаге 4 не были прочитаны все данные, шаг 5 может застрять в блокировке на неопределенный срок.

И использование, запускаемое по краю, предложенное руководством Linux, выглядит следующим образом:

  1. Использование с неблокирующими дескрипторами
  2. пока каждый раз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 быстрее, чем опрос.Согласно другим блогам в Интернете, резюмируются следующие причины:

  1. При ожидании готовности дескриптора нет необходимости каждый раз передавать набор дескрипторов в ядро, достаточно зарегистрировать дескриптор в экземпляре epoll, а экземпляр epoll внутренне поддерживает все наборы дескрипторов.
  2. Красно-черное дерево и область кэша ядра используются внутри экземпляра epoll для поддержки набора дескрипторов, что повышает эффективность операций регистрации и удаления набора дескрипторов.
  3. epoll внутренне поддерживает список готовности с помощью механизма обратного вызова. Когда дескриптор готов, поместите его в список готовности.При вызове epoll_wait вам нужно только оценить, пуст ли список готовности.Если он не пуст, скопируйте список готовности в пользовательское пространство и очистите список готовности, в противном случае , он заснет.
  4. Нет необходимости повторно проходить все дескрипторы, когда есть готовые дескрипторы, epoll напрямую вернет готовый набор дескрипторов.

Кстати, вот реализация LT,epoll_waitПеред возвратом к готовому дескриптору будет проверен тип триггера дескриптора.Если это горизонтальный триггер и на дескрипторе есть необработанные данные, он будет добавлен в только что опустевший список готовых, чтобы следующий вызовepoll_waitГотовый список по-прежнему будет иметь дескриптор. Это также является фактической причиной разницы в производительности LT и ET.