В табл. 59.1 собраны модели уведомлений, которые используются при мультиплексировании ввода/вывода, в вводе/выводе на основе сигналов и в интерфейсе epoll. Последний отличается от первых двух возможностью применения обеих моделей: по уровню и по фронту.
Таблица 59.1. Использование уведомлений, срабатывающих по уровню и по фронту
Модель ввода/вывода
По уровню?
По фронту?
select() и poll()
*
Ввод/вывод на основе сигналов
*
Интерфейс epoll
*
*
Отличия между этими двумя моделями уведомлений будут проясняться по мере чтения настоящей главы. А пока что обсудим то, как выбор той или иной модели влияет на архитектуру нашей программы.
Используя уведомления, срабатывающие по фронту, можно проверять готовность файлового дескриптора в любое время. То есть, когда файловый дескриптор готов (например, при появлении в нем входящих данных), можно выполнить для него операцию ввода/вывода и затем повторно проверить, готов ли он для дальнейшего применения (например, остались ли в нем данные для чтения); в случае положительного ответа можно продолжить выполнение ввода/вывода. Иными словами, благодаря возможности постоянного наблюдения в любой момент времени можно не загружать приложение по максимуму (например, считывая как можно больше данных) при каждом уведомлении о готовности файлового дескриптора (на самом деле можно вообще не выполнять никакого ввода/вывода).
Для сравнения: если выбрать модель срабатывания по фронту, то уведомления будут приходить только в ответ на события ввода/вывода. Кроме того, при возникновении такого события обычно не известно, сколько именно данных можно прочитать или записать. Следовательно, программы, которые используют эту модель, как правило, проектируются в соответствии со следующими правилами.
• Получив уведомление о событии ввода/вывода, программа в какой-то момент должна прочитать или записать в заданный дескриптор как можно больше данных. Если ей не удастся это сделать, то она может потерять возможность выполнить определенный ввод/вывод, поскольку до получения следующего события не будет знать о необходимости дополнительных действий с файловым дескриптором. Так может возникнуть ложная потеря данных или блокировка программы. Словосочетание «в какой-то момент» выбрано неслучайно, так как в некоторых случаях ввод/вывод не стоит выполнять сразу же после получения уведомления о готовности дескриптора. Дело в том, что при чтении или записи большого объема данных можно оставить другие файловые дескрипторы без работы. Мы рассмотрим этот случай более подробно в подразделе 59.4.6, когда будем обсуждать применение модели уведомлений, срабатывающих по фронту, для интерфейса epoll.
• Если программа использует цикл, чтобы прочитать или записать как можно больше данных, а дескриптор помечен как блокирующий, то системный вызов ввода/вывода рано или поздно заблокируется, когда передача данных станет невозможной. В связи с этим каждый отслеживаемый файловый дескриптор обычно переводится в неблокирующий режим, а после события ввода/вывода операции чтения или записи продолжают выполняться до тех пор, пока соответствующий системный вызов (например, read() или write()) не завершится ошибкой EAGAIN или EWOULDBLOCK.
59.1.2. Применение неблокирующего режима в сочетании с альтернативными моделями ввода/вывода
Неблокирующий режим (флаг O_NONBLOCK) часто используется в сочетании с альтернативными моделями ввода/вывода, описанными в данной главе. Ниже приводятся некоторые примеры того, почему это может быть полезным.
• Как уже объяснялось в предыдущем разделе, неблокирующий режим обычно применяется в связке с моделями ввода/вывода, обеспечивающими срабатывание уведомлений о событиях чтения или записи по фронту.
• Если несколько процессов (или потоков) выполняют ввод/вывод в контексте одних и тех же описаний открытых файлов, то с практической точки зрения готовность дескриптора может измениться между моментом принятия уведомления и последующей операцией ввода/вывода. Таким образом, блокирующий вызов может заблокироваться и не дать процессу отслеживать другие файловые дескрипторы (это может случиться при использовании любых моделей ввода/вывода, описанных в настоящей главе, независимо от способа срабатывания — по уровню или по фронту).
• Даже после срабатывания уведомлений по уровню (интерфейсы select() или poll()), сообщающих о готовности файлового дескриптора для потокового сокета к чтению или записи, вызов может заблокироваться при попытке записать достаточно большой объем данных за одну операцию write() или send().
• В редких случаях программные интерфейсы уведомлений, срабатывающих по уровню, такие как select() или poll(), могут возвращать ложную информацию о готовности файлового дескриптора. Это может быть вызвано ошибкой в ядре или стать результатом нормальной работы в нестандартном сценарии.