[lang_en]Using epoll() For Asynchronous Network Programming[/lang_en][lang_ru]Использование epoll() Для Организации Асинхронной Работы С Сетевыми Соединениями[/lang_ru]
13 Apr2006

[lang_en]

General way to implement tcp servers is “one thread/process per connection”. But on high loads this approach can be not so efficient and we need to use another patterns of connection handling. In this article I will describe how to implement tcp-server with synchronous connections handling using epoll() system call of Linux 2.6. kernel.

[/lang_en]

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

[lang_en]

epoll is a new system call introduced in Linux 2.6. It is designed to replace the deprecated select (and also poll). Unlike these earlier system calls, which are O(n), epoll is an O(1) algorithm – this means that it scales well as the number of watched file descriptors increase. select uses a linear search through the list of watched file descriptors, which causes its O(n) behaviour, whereas epoll uses callbacks in the kernel file structure.

Another fundamental difference of epoll is that it can be used in an edge-triggered, as opposed to level-triggered, fashion. This means that you receive “hints” when the kernel believes the file descriptor has become ready for I/O, as opposed to being told “I/O can be carried out on this file descriptor”. This has a couple of minor advantages: kernel space doesn’t need to keep track of the state of the file descriptor, although it might just push that problem into user space, and user space programs can be more flexible (e.g. the readiness change notification can just be ignored).

To use epoll method you need to make following steps in your application:

  • Create specific file descriptor for epoll calls:

      epfd = epoll_create(EPOLL_QUEUE_LEN);
    


    where EPOLL_QUEUE_LEN is the maximum number of connection descriptors you expect to manage at one time. The return value is a file descriptor that will be used in epoll calls later. This descriptor can be closed with close() when you do not longer need it.

  • After first step you can add your descriptors to epoll with following call:

      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);
    


    where ev is epoll event configuration sctucture, EPOLL_CTL_ADD – predefined command constant to add sockets to epoll. Detailed description of epoll_ctl flags can be found in epoll_ctl(2) man page. When client_sock descriptor will be closed, it will be automatically deleted from epoll descriptor.

  • When all your descriptors will be added to epoll, your process can idle and wait to something to do with epoll’ed sockets:

      while (1) {
        // wait for something to do...
        int nfds = epoll_wait(epfd, events, 
                                    MAX_EPOLL_EVENTS_PER_RUN, 
                                    EPOLL_RUN_TIMEOUT);
        if (nfds < 0) die("Error in epoll_wait!");
      
        // for each ready socket
        for(int i = 0; i < nfds; i++) {
          int fd = events[i].data.fd;
          handle_io_on_socket(fd);
        }
      }
    

Typical architecture of your application (networking part) is described below. This architecture allow almost unlimited scalability of your application on single and multi-processor systems:

  • Listener – thread that performs bind() and listen() calls and waits for incoming conncetions. Then new connection arrives, this thread can do accept() on listening socket an send accepted connection socket to one of the I/O-workers.
  • I/O-Worker(s) – one or more threads to receive connections from listener and to add them to epoll. Main loop of the generic I/O-worker looks like last step of epoll using pattern described above.
  • Data Processing Worker(s) – one or more threads to receive data from and send data to I/O-workers and to perform data processing.

As you can see, epoll() API is very simple but believe me, it is very powerful. Linear scalability allows you to manage huge amounts of parallel connections with small amout of worker processes comparing to classical one-thread per connection.

If you want to read more about epoll or you want to look at some benchmarks, you can visit epoll Scalability Web Page at Sourceforge. Another interesting resources are:

  • The C10K problem: a most known page about handling many connections and various I/O paradigms including epoll().
  • libevent: high-level event-handling library ontop of the epoll. This page contains some information about performance tests of epoll.

[/lang_en]

[lang_ru]

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.

[/lang_ru]