Книга: Разработка приложений в среде Linux. Второе издание

13.1.5. Мультиплексирование с помощью epoll

13.1.5. Мультиплексирование с помощью epoll

В версии 2.6 ядра Linux был предложен третий метод для мультиплексированного ввода-вывода по имени epoll. Будучи более сложным, чем poll() или select(), epoll ликвидирует узкие места, связанные с производительностью, которые характерны для обоих методов.

Оба системных вызова poll() и select() передают на проверку полный список файловых дескрипторов при каждом вызове. Каждый из этих дескрипторов должен быть обработан системным вызовом, даже если только один из них готов к чтению или записи. Когда проверяются десятки, сотни или тысячи файловых дескрипторов, эти системные вызовы превращаются в узкие места; ядро тратит много времени на выяснение того, какие именно файловые дескрипторы приложению необходимо проверить.

При использовании epoll приложения обеспечивают ядро списком файловых дескрипторов для проверки с помощью одного системного вызова, а затем для проверки этих дескрипторов с помощью другого системного вызова. После создания списка ядро постоянно проверяет эти дескрипторы для событий, интересующих приложение[79], а затем сообщает о событии. Как только приложение запрашивает у ядра файловые дескрипторы, готовые для дальнейшей обработки, ядро предоставляет список без необходимости проверки каждого файлового дескриптора.

Преимущества в плане производительности epoll требуют более сложного, чем у poll() или select(), интерфейса системных вызовов. В то время как poll() использует массив struct pollfd для предоставления набора файловых дескрипторов, a select() с той же целью — три разных структуры fd_set, epoll перемещает эти наборы файловых дескрипторов в ядро, а не хранит их в адресном пространстве программы. На каждый из этих наборов ссылаются с помощью дескриптора epoll, являющегося файловым дескриптором, который можно применять только для системных вызовов epoll. Новые дескрипторы epoll распределяются системным вызовом epoll_create().

#include <sys/epoll.h>
int epoll_create (int numDescriptors);

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

Хотя дескриптор epoll является файловым дескриптором, его следует применять только с двумя системными вызовами.

#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event * event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents,
 int timeout);

Большинство этих параметров используют структуру struct epoll_event, которая определяется, как показано ниже.

#include <sys/epoll.h>
struct epoll_event {
 int events;
 union {
  void * ptr;
  int fd;
  unsigned int u32;
  unsigned long long u64;
 } data;
};

Эта структура обслуживает три цели: определяет, какие типы событий следует проверять, определяет типы произошедших событий и ассоциирует отдельный элемент данных с файловым дескриптором. Поле events предназначено для первых двух функций и является одной или несколькими перечисленными далее значениями, объединенными с помощью логического "ИЛИ"[80].

EPOLLIN Определяет, что операция read() не блокируется; данные или уже готовы, или их уже не осталось для считывания.
EPOLLOUT Связанный файл готов для записи.
EPOLLPRI Файл имеет внешние данные, готовые для чтения.

Второй элемент struct epoll_event, data, представляет собой объединение, содержащее целое число (для хранения файлового дескриптора), указатель, а также 32- и 64-битные целые числа[81]. Этот элемент данных хранится в epoll и возвращается в программу всякий раз, когда происходит событие подходящего типа. Элемент data — это единственный способ, с помощью которого программе нужно выяснить, какой файловый дескриптор необходимо обслужить; интерфейс epoll не передает файловый дескриптор программе, в отличие от poll() и select() (если data не содержит файловый дескриптор). Этот метод обеспечивает дополнительную гибкость приложениям, которые отслеживают файлы как нечто, более сложное, чем простые файловые дескрипторы.

Системный вызов epoll_ctl() добавляет файловые дескрипторы к набору, на который ссылается дескриптор epfdepoll, и удаляет их из него.

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

EPOLL_CTL_ADD Файловый дескриптор fd добавляется к набору файловых дескрипторов набором событий events. Если файловый дескриптор уже присутствует, он возвращает EEXIST. (Несколько потоков могут добавлять тот же файловый дескриптор к набору epoll более одного раза, но это действие ничего не меняет.)
EPOLL_CTL_DEL Файловый дескриптор fd удаляется из контролируемого набора файловых дескрипторов. Параметр events должен указывать на struct epoll_event, но содержимое этой структуры игнорируется. (Это еще раз доказывает, что events должен быть допустимым указателем; он не может быть NULL.)
EPOLL_CTL_MOD Системный вызов struct epoll_event для fd обновляется на основе информации, на которую указывает events. Это позволяет контролировать набор событий и обновлять элемент данных, ассоциируемый с файловым дескриптором, не создавая условий состязания.

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

Два промежуточных параметра определяют буфер для ядра, в который можно копировать структуры struct epoll_event. Параметр events указывает на буфер, maxevents определяет, какое количество структур struct epoll_event помещается в буфер, а возвращаемое значение сообщает программе количество структур, помещенных в этот буфер (пока вызов не попадет в состояние тайм-аута либо не произойдет ошибка).

Каждый системный вызов struct epoll_event сообщает программе полное состояние контролируемого файлового дескриптора. Элемент events может иметь установленные флаги EPOLLIN, EPOLLOUT или EPOLLPRI, а также два новых флага, которые описаны ниже.

EPOLLERR С файлом связано ожидающее состояние ошибки; это случается, если ошибка происходит в сокете, когда приложение не считывает из него или не записывает в него.
EPOLLHUP Файловый дескриптор завис; в главе 10 дана информация о том, когда это обычно происходит.

На первый взгляд это все может показаться сложным, но на самом деле это очень похоже на работу poll(). Вызов epoll_create() — это то же, что и распределение массива struct pollfd, a epoll_ctl() — это то же, что и инициализация элементов этого массива. Главный цикл, обрабатывающий файловые дескрипторы, использует epoll_wait() вместо системного вызова poll(), а close() аналогичен освобождению памяти, занимаемой массивом struct pollfd. Эти параллели помогают переписывать с применением epoll программы мультиплексирования, которые изначально были реализованы с помощью poll() или select().

Интерфейс epoll предлагает еще одну возможность, которую невозможно сравнить с poll() или select(). Поскольку дескриптор epoll в действительности является файловым дескриптором (вот почему его можно передавать close()), имеется возможность контролировать дескриптор epoll как часть еще одного дескриптора epoll либо через poll() или select(). Дескриптор epoll будет готов к чтению из любого места, а вызов epoll_wait() вернет события.

В окончательном решении проблемы мультиплексирования каналов, предложенном в данном разделе, используется epoll. Оно очень похоже на другие примеры, вот только определенная часть кода инициализации перемещена в новую функцию addEvent() для предотвращения нежелательного удлинения программы.

 1: /* mpx-epoll.c */
 2:
 3: #include <fcntl.h>
 4: #include <stdio.h>
 5: #include <stdlib.h>
 6: #include <sys/epoll.h>
 7: #include <unistd.h>
 8:
 9: #include <sys/poll.h>
10:
11: void addEvent(int epfd, char * filename) {
12:  int fd;
13:  struct epoll_event event;
14:
15:  if ((fd = open (filename, O_RDONLY | O_NONBLOCK)) < 0) {
16:   perror("open");
17:   exit(1);
18:  }
19:
20:  event.events = EPOLLIN;
21:  event.data.fd = fd;
22:
23:  if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event)) {
24:   perror("epoll_ctl(ADD)");
25:   exit(1);
26:  }
27: }
28:
29: int main(void) {
30:  char buf[4096];
31:  int i, rc;
32:  int epfd;
33:  struct epoll_event events[2];
34:  int num;
35:  int numFds;
36:
37:  epfd = epoll_create(2);
38:  if (epfd < 0) {
39:   perror("epoll_create");
40:   return 1;
41:  }
42:
43:  /* открыть оба канала и добавить их в набор epoll */
44:  addEvent(epfd, "p1");
45:  addEvent(epfd, "p2");
46:
47:  /* продолжать, пока есть один или более файловых дескрипторов
48:     для слежения */
49:  numFds = 2;
50:  while (numFds) {
51:   if ((num = epoll_wait(epfd, events,
52:    sizeof(events) / sizeof(* events),
53:    -1)) <= 0) {
54:   perror("epoll_wait");
55:   return 1;
56:  }
57:
58:  for (i = 0; i < num; i++) {
59:   /* events[i].data.fd готов для чтения */
60:
61:   rc = read(events[i].data.fd, buf, sizeof(buf) - 1);
62:   if (rc < 0) {
63:    perror("read");
64:    return 1;
65:   } else if (!rc) {
66:    /* этот канал закрыт, не пытаться
67:       читать из него снова */
68:    if (epoll_ctl(epfd, EPOLL_CTL_DEL,
69:     events[i].data.fd, &events[i])) {
70:     perror("epoll_ctl (DEL)");
71:     return 1;
72:    }
73:
74:    close(events[i].data.fd);
75:
76:    numFds--;
77:   } else {
78:    buf[rc] = '';
79:    printf("чтение: %s", buf);
80:
81:   }
82:  }
83:
84:  close(epfd);
85:
86:  return 0;
87: }

Оглавление книги


Генерация: 1.214. Запросов К БД/Cache: 3 / 0
поделиться
Вверх Вниз