Использование epoll() Для Организации Асинхронной Работы С Сетевыми Соединениями
13 Apr2006

Одним из самых распространенных способов реализации серверов tcp является “один поток/процесс на соединение”. Но при высокой нагрузке этот метод может быть не слишком эффективным, и необходимо использовать другие паттерны обработки соединений. В этой статье я расскажу, как реализовать tcp-сервер с синхронной обработкой запросов с помощью системного вызова ecall в ядре Linux 2.6.

epoll – это новый системный вызов, который появился в Linux 2.6. Он призван заменить устаревший select (а также poll). В отличие от старых системных вызовов, сложность которых O(n), epoll использует алгоритм O(1), что означает хорошее масштабирование при увеличении количества прослушиваемых дескрипторов. select реализует линейный поиск по списку прослушиваемых дескрипторов, что приводит к сложности O(n), в то время как epoll использует обратные вызовы структуры файла в ядре.

Другим фундаментальным отличием epoll является то, что он может быть использован как edge-triggered, в отличие от level-triggered. Это означает, что вы получаете “подсказки”, когда ядро полагает, что файловый дескриптор готов для ввода/вывода, в противоположность сообщениям типа “Можно производить операции ввода-вывода над данным дескриптором”. Это дает несколько дополнительных преимуществ: пространство ядра не должно отслеживать состояние файлового дескриптора, вместо этого оно может просто взвалить задачу на пользовательское пространство, а программы последнего получают большую гибкость (например, уведомление об изменении готовности может быть просто проигнорировано).

Для использования epoll Вам нужно выполнить в Вашем приложении следующие шаги:

  • Создать специальный файловый дескриптор для вызовов epoll:

      epfd = epoll_create(EPOLL_QUEUE_LEN);
    


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

  • После выполнения первого шага Вы можете добавлять Ваши дескрипторы в epoll при помощи следующего вызова:

      static struct epoll_event ev;
      int client_sock;
      ...
      ev.events = EPOLLIN | EPOLLPRI | EPOLLERR | EPOLLHUP;
      ev.data.fd = client_sock;
      int res = epoll_ctl(epfd, EPOLL_CTL_ADD, client_sock, &ev);
    


    где ev – это структура для конфигурирования epoll, EPOLL_CTL_ADD – предопределенная константа для команды добавления сокетов в epoll. Детальное описание флагов для epoll_ctl может быть найдено на странице epoll_ctl(2) руководства man. Когда дескриптор client_sock будет закрыт, об будет автоматически удален из дескриптора epoll.

  • Когда все Ваши дескрипторы будут добавлены в epoll, Ваш процесс может отдохнуть в ожидании момента когда нужно будет что-нибудь делать с сокетами в epoll:

      while (1) {
        // ожидаем момента, когда надо будет работать...
        int nfds = epoll_wait(epfd, events, 
                                    MAX_EPOLL_EVENTS_PER_RUN, 
                                    EPOLL_RUN_TIMEOUT);
        if (nfds < 0) die("Ошибка в epoll_wait!");
      
        // для каждого готового сокета
        for(int i = 0; i < nfds; i++) {
          int fd = events[i].data.fd;
          handle_io_on_socket(fd);
        }
      }
    

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

  • Listener – поток, выполняющий вызовы bind() и listen() и слушающий входящий сокет в ожидании входящих соединений. Когда приходит новое соединение, этот потокделает вызов accept() на слушающем сокете и отправляет полученный сокет соединения к I/O-workers.
  • I/O-Worker(s) – один или несколько потоков, получающих соединения от listener и добавляющих их в epoll. Главный цикл таких потоков может выглядеть как цикл, описанный в последнем шагу паттерна использования epoll(), описанном выше.
  • Data Processing Worker(s) – один или несколько потоков для получения и отправки данных для I/O-workers и выполняющих обработку данных.

Как видите, epoll() API является достаточно простым но, поверьте мне, очень мощным. Линейная масштабируемость позволяет Вам обслуживать огромнейшие количества параллельных соединений с использованием небольшого количества рабочих процессов по сравнению с классической схемой “один процесс на одно соединение”.

Если Вы хотите знать больше о epoll или у Вас есть желание проанализировать сравнительные тесты производительности, Вы можете посетить epoll Scalability Web Page на Sourceforge. Дополнительными интересными и полезными ресурсами могут быть:

  • The C10K problem: самый известный ресурс о проблемах обработки большого количества соединений с приведением различных парадигм ввода-вывода включая epoll().
  • libevent: высокоуровневая библиотека работы с собитиями, построенная на базе epoll. Эта страница содержит информацию о различных тестах проихводительности epoll.