Использование epoll() Для Организации Асинхронной Работы С Сетевыми Соединениями
Одним из самых распространенных способов реализации серверов 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.
Related posts:

12 Responses to this entry
It should be pointed out that if you use this approach, all code from handle_io_on_socket must avoid blocking no matter what. This can be nearly impossible in an application that’s not multi-threaded.
This is exactly what I was looking for, thanks for the great information.
Not if you use the aio functions.
Is it possible to use FD (file descriptor) meant for poll() with epoll()?
Eranga, yes.
OT: Great tutorial, even though you never declared events.
Well done Scoundrel.
I found a website with an epoll example written by zhoulifa(zhoulifa@163.com). Its comments are in chinese, could anyone translate it/document it in english so it will be understood better? http://zhoulifa.bokee.com/6081520.html
Btw, why must it avoid nonblocking on function handle_io_on_socket? It accessing db like MySQL nonblocking?
Sonny,
Just use google’s translator. CLick this link to see a translation (should help a little):
http://translate.google.com/translate?hl=en&sl=zh-CN&u=http://zhoulifa.bokee.com/6081520.html&sa=X&oi=translate&resnum=9&ct=result&prev=/search%3Fq%3Depoll%2Bserver%26complete%3D1%26hl%3Den%26client%3Dfirefox-a%26rls%3Dorg.mozilla:en-US:official%26sa%3DG
Hi,
Thanks for the info for epoll. These are helpful.
I had one question regarding the user data variable given as part of epoll_event structure.
If only “fd” is used for epolling, why are u32/u64 and void pointers provided.
thanks,
Prashanth
actually “fd” is not “used” for epolling. The fd is passed separately to epoll_wait, the data structure is used for passing in any data the user requires. As it is a union structure writing to “void *ptr” will overwrite fd.
This data structure is useful for passing in data that may be useful to the user of the data. For example you can cache data that has been gathered by the connection previously and then store this in a structure which the void *ptr points to. When more data is ready you then have access to the previously stored data which you can add to with the further communication.
James
занимательно, надо будет py-epoll погонять)
тем более сейчас джангу перевёл на асинхронный сервер который использует epoll В)
кстати если не ошибаюсь, результатом C10K problem стал сервер lighttpd
Great write up. I couldn’t find a more detailed example than this. However, how do you know when a client closes a connection? It seems that you should be able to check your event against EPOLLHUP but the event number shown when a client closes is 0×5 while EPOLLHUP is defined as 0×10. So is there some bit masking I have to do?
Thanks,
Addisu
When the client closes the connection you will get a read of 0 bytes.