Введение
Данная заметка родилась в попытках разобраться с базовым принципом работы linux epoll, так как он часто используется в разных языках программирования для реализации асинхронности.
Все что написано ниже, сугубо мое понимание и оно не претендует на 100% точность, если кто-то оставит полезные примечания, то буду благодарен.
Файловые дескрипторы ОС
Все процессы ссылаются на потоки ввода-вывода (IO) через дескрипторы. Каждый процесс поддерживает таблицу файловых дескрипторов, которые ему доступны.
Каждая запись в этой таблице состоит из флагов операций доступных дескриптору и указатель на базовую структуру ядра.
Дескрипторы либо создаются явно, либо наследуются от родительского процесса.
Как работает epoll.
Epoll структура в ядре linux, которая позволяет мультиплексировать IO операции для нескольких файловых деcкрипторов.
Создание экземпляра epoll происходит системным вызовом epoll_create
, результатом возвращается файловый дескриптор, который вызывающий процесс может использовать для добавления, изменения и удаления других файловых дескрипторов для которых будет отслеживать IO операции созданный экземпляр epoll.
Далее процесс с помощью системного вызова epoll_ctl
, добавляет файловые дескрипторы и события (структура epoll_event) для отслеживания их созданным экземпляром epoll.
Все добавленные дескрипторы помещаются набор epoll (epoll set) и в последствии, когда любой из зарегистрированных дескрипторов становится готовым для IO операций, они считаются находящимися в списке готовности (ready list). Список готовности является подмножеством набора epoll.
Когда наступает какое-то из зарегистрированных событие поток уведомляется об этом системным вызовом epoll_wait
, который . В зависимости от заданного таймаута epoll_wait имеет следующее поведение:
- когда для тайм-аута задан 0, epoll_wait не блокируется, а возвращается сразу после проверки того, какие файловые дескрипторы в набор epoll для текущего процесса готовы (дескрипторы из списка готовности)
- когда для тайм-аута задан -1, epoll_wait будет блокироваться «навсегда». Когда epoll_wait блокируется, ядро может перевести процесс в спящий режим до тех пор, пока не вернется epoll_wait. Блокироваться epoll_wait будет до тех пор, пока не наступит одно из условий:
- один или несколько дескрипторов, указанных в наборе epoll для текущего процесса готовы (дескрипторы из списка готовности)
- вызов прерван обработчиком сигнала
- когда для тайм-аута задано неотрицательное и ненулевое значение, epoll_wait будет блокироваться до тех пор, не наступит одно из условий:
- один или несколько дескрипторов, указанных в наборе epoll для текущего процесса готовы
- вызов прерван обработчиком сигнала
- истекло общее время указанное в тайм-ауте (миллисекунды)
Есть несколько способов нотификации о событии (задаются влагами в epoll_event при регистрации дескриптора):
- level-triggered (флаг EPOLLIN) - epoll_wait разблокируется если дескриптор находится в заданном состоянии и будет полагать его активным пока данное состояние не будет снято
- edge-triggered (флаги EPOLLIN | EPOLLET) - epoll_wait разблокируется только по изменению текущего данного заказанного состояния.
Подробнее этот вопрос рассмотрен в [2].
Вывод
После изучения материала у меня сложилось впечатление, что производительность решений на базе epoll очень сильно зависит от конкретной реализации механизма работы с ним. И чтобы это узнать необходимо лезть в кишки конкретного инструмента.