Глава 63. Альтернативные модели ввода/ вывода
Содержание
Данная глава обсуждает три альтернативы обычной модели файлового ввода/ вывода, которые мы вы применяли в большинстве программ, показанных в данной книге:
-
Мультиплексирование ввода/ вывода (системные вызовы
select()
иpoll()
; -
управляемый сигналами ввод/ вывод; и
-
особый для Linux API
epoll
.
{Прим. пер.: рекомедуем также ознакомиться с примерами использования
select()
и epoll
в нашем
переводе второго издания "Книги рецептов сетевого программирования Python"
Прейдибэн Катхирейвелу и доктора М.О. Фарук Саркер, выпущенной Packt Publishing в августе 2017..}
Большая часть программ, которые мы представляли до сих пор в данной книге применяли некую модель ввода/ вывода,
при которой какой- то процесс выполнял ввод вывод только с одним файловым дескриптором за один раз, причём каждый
системный вызов ввода/ вывода блокировался пока передаются все данные. Например, при считывании из конвейера некий
вызов read()
обычно блокируется если в данном канале не представлены никакие
данные в настоящее время, в вызов write()
блокируется если в конвейере
недостаточно места для записи данных. Аналогичное поведение происходит и при выполнении ввода/ вывода с различными
прочими типами файлов, включая FIFO и сокеты.
Замечание | |
---|---|
Дисковые файлы являются особым случаем. Как описывалось в Главе 13,
имеющееся ядро предоставляет свой буфер кэширования для ускорения дисковых запросов на операции ввода/ вывода.
Таким образом, некий |
Традиционной модели блокируемого ввода/ вывода достаточно для многих приложений, но не для всех. В частности, некоторым приложениям необходимо иметь возможность делать одно из следующего:
-
Проверить: возможен ли ввод/ вывод для некоторого файлового дескриптора без его блокирования если он не возможен.
-
Отслеживать множество файловых дескрипторов для просмотра возможности ввода/ вывода на любом из них.
Мы уже встречали две технологии, которые могут применяться для частичного решения этих потребностей: неблокируемого ввода/ вывода и применения множества процессов или потоков.
Мы обсуждали неблокируемый ввод вывод с некоторыми подробностями в Разделах
5.9 и 44.9. Если мы помещаем некий файловый дескриптор в неблокируемый режим, разрешая флаг
состояния открытия файла O_NONBLOCK
,
тогда некий системный вызов операции ввода/ вывода, который не может быть немедленно выполнен возвращает
некую ошибку вместо блокирования. Неблокируемый ввод/ вывод может применяться с конвейерами, FIFO, сокетами,
терминалами, псевдотерминалами и некоторыми другими типами устройств.
Неблокируемый ввод/ вывод позволяет нам периодически проверять (&qout;опрашивать&qout; &qout;poll&qout;) возможен ли ввод/ вывод для данного файлового дескриптора. Например, мы можем сделать некий файловый дескриптор ввода неблокируемым и затем периодически выполнять неблокируеые чтения. Если нам необходимо отслеживать множество файловых дескрипторов, тогда мы помечаем их как неблокируемые и опрашиваем их все одного за другим. Однако, выполнение опроса таким образом обычно нежелательно. Если опрос выполняется достаточно редко, тогда получаемые задержки, прежде чем приложение откликнется на некое событие ввода/ вывода, могут быть неприемлемо длинными; с другой стороны, жёсткий цикл тратит время ЦПУ.
Замечание | |
---|---|
В данной главе мы применяем слово poll (опрос) двумя различными
способами. Один из них заключается в именовании имеющейся мультиплексированного системного вызова ввода/
вывода, |
Если мы не желаем выполнять блокировку некоторого процесса при выполнении операции ввода/ вывода для какого- либо файлового дескриптора, мы можем создать некий новый процесс для выполнения такого ввода/ вывода. Имеющийся родительский процесс затем позаботится о выполнении прочих задач, пока процесс потомок будет блокирова, пока выполняется данный ввод/ вывод. Если нам необходимо обрабатывать ввод/ вывод множества файловых дескрипторов, мы можем создавать по одному дочернему процессу для каждого дескриптора. Основными проблемами такого подхода являются затраты и сложность. Создание и сопровождение процессов размещает некую нагрузку на всю систему, к тому же, как правило, такие дочерние процессы потребуют применения некоторой формы IPC (межпроцессного взаимодействия) для информирования предка о состояниях операций ввода/ вывода.
Применение множества потоков вместо процессов менее требовательно к ресурсам, однако потоки всё ещё, скорее всего будут нуждаться в обмене информацией друг с другом о состоянии операций ввода/ вывода, а само программирование может быть сложным, в особенности, если мы применяем пулы потоков для минимизации общего число потоков, применяемого для большого числа одновременных клиентов. (Одним из мест, где потоки могут быть особенно полезными, это когда самому приложению требуется выполнять вызовы сторонних библиотек , которые выполняют блокирующий ввод/ вывод. Некое приложение может избежать блокирования в данном случае, выполняя такой библиотечный вызов в некотором отдельном потоке.)
Из- за ограничений и неблокируемого ввода/ вывода, и применения множества потоков или процессов, зачастую более предпочтительными является одна из следующих альтернатив:
-
Мультиплексирование ввода/ вывода делает возможным некоторому процессу одновременно отслеживать множество файловых дескрипторов для опредеделения того, возможен ли ввод/ вывод в одном из них. Мультиплексированный ввод/вывод осуществляют системные вызовы
select()
иpoll()
. -
Управляемая сигналом технология ввода/ вывода, посредством которой некий процесс запрашивает у своего ядра отправлять ему некий сигнал, когда доступен ввод или данны могут быть записаны в некий определённый фацйловый дескриптор. Данный процесс может затем заняться выполнением прчих действий и получает уведомление посредством определённого сигнала когда становится возможным ввод/ вывод. При отслеживании большого числа файловых дескрипторов управляемый сигналом ввод/ вывод предоставляет значитльно лучшую производительность, нежели
select()
иpoll()
. -
API epoll является присущим Linux свойством, которое впервые появилось в Linx 2.6. Как и прочие API с мультиплексированным вводом/выводом, сам API
epoll
позволяет некотоому процессу отслеживать множество файловых дескрипторов на предмет просмотра возможности ввода/ вывода из них. Как и управляемый сигналом ввод/ выод, сам по себе API иepoll
предоставляет намного лучшую производительность при выполнении отслеживания большого числа файловых дескрипторов.
Замечание | |
---|---|
В остальной части данной главы мы в основном ограничим само обсуждение перечисленных выше технологий условиями исполнения процессов. Однако, данные технологии могут также применяться к многопоточным приложениям. |
В действительности, мультиплексирование ввода/ вывода, управляемый сигналом ввод/ выод и
epoll
, всё это методологии достижения одного и того же результата -
отслеживания одного или, в общем случае, нескольких файловых дескрипторов одновременно, чтобы обнаружить то,
что они готовы для операции ввода/ вывода (чтобы быть более точным,
обнаружить может ли быть выполнен некий системный вызов ввода/ вывода без блокирования). Перемещение некоторого
файлового дескриптора в состояние готовности переключается некоторым типом события
ввода/ вывода, такого как появление ввода, завершение соединения какого- то сокета, или определённой доступности
пространства в каком- то первоначально заполненном буфере отправки сокета после обменов TCP поставленных в очередь
данных к определённому сокету однорангового взаимодействия. Отслеживание множества файловых дескрипторов полезно
в таких приложениях, как сетевые серверы, которые должны одновременно отслеживать множество клиентских сокетов
или для таких приложений, которые должны выполнять одновременный ввод из некоторого терминала и какого- то
конвейера или сокета.
Отметим, что никакая из техи технологий не выполняет собственно ввода/ вывода. Они главным образом сообщают нам о готовности некоторого файлового дескриптора. Впоследствии некоторые прочие системные вызовы могут затем применяться для реального исполнения необходимых операций ввода/ вывода.
Замечание | |
---|---|
Одна из моделей, которую мы не обсуждаем в данной главе, это асинхронный ввод/ вывод (AIO) POSIX. POSIX
AIO возможным некоторому процессу выстраивать какую- то очередь операций файлового ввода/ вывода, а затем
позже получать уведомления при завершении операции. Основное преимущество POSIX AIO состоит в том, что
первоначальный вызов ввода/ вывода возвращается немедленно, следовательно данный процесс не связан ожиданием
того что данные должны быть переданы в само ядро или что данная операция завершится. Это позволяет данному
процессу выполнять прочие задачи одновременно с самим вводом/ выводом (что может включать в себя опрос
дальнейших запросов ввода/ вывода). Для определённого типа приложений POSIX AIO также предоставляет
плоезные преимущества производительности. В настоящее время {Прим. пер.: вторая
половина 2010г} Linux предоставляет в рамках |
Какую технологию выбрать?
На протяжении данной главы мы рассмотрим все причины того почему мы можем выбрать одну из данных технологий вместо оставшихся. Пока же мы просуммируем некоторые моменты:
-
Системные вызовы
select()
иpoll()
являются давно работающими интерфейсами, котрые имеют место в системах UNIX на протяжении многих лет. В сравнении с прочими технологиями их основным преимуществом является переносимость. Их главным недостатком является то, что они не могут хорошо масштабироваться для наблюдения большого числа (измеряемого сотнями и тысячами) файловых дескрипторов. -
Ключевым преимуществом API
epoll
является то, что они делают возможным для приложений выполнять мониторинг больших количеств файловых дескрипторов. Его первичным недостатком является то, что этот API является специфичным для Linux.Замечание Некоторые прочие реализации UNIX предоставляют (нестандартные) механизмы, аналогичные
epoll
. Solaris предоставляет особый файл/dev/poll
, (описываемый в страницах руководства Solarispoll(7d)
, а некоторые имеющиеся BSD предоставляют свой APIkqueue
(который поставляет более универсальные возможности мониторинга чемepoll
). [Stevens et al., 2004] вкратце описывает оба механизма; более длинное обсуждениеkqueue
можно найти в [Lemon, 2001]. -
Как и
epoll
, управляемый сигналом ввод/ вывод делает возможным для приложений эффективно отслеживать большое число файловых дескрипторов. Однакоepoll
предоставляет ряд преимуществ над управляемым сигналом вводом/ выводом:-
Мы избегаем связанной с обработкой сигналов сложности
-
Мы можем определять конкретный вид мониторинга, который мы бы желали выполнять (например, готовность для чтения или готовность для записи).
-
Мы можем выбрать либо переключаемое уровнем, либо переключаемое фронтом уведомление (описывается в Разделе 63.1).
-
Более того, получение всех преимуществ управляемого сигналом ввода/ вывода требует применения несовместимых,
особенных для LInux свойств и, если мы это делаем, движимый сигналом ввод/ вывод не более перенсоим,
нежели epoll
.
Так как с одной стороны select()
и poll()
являются более переносимыми с одной стороны, но при том что управляемый сигналом ввод/ вывод и
epoll
предоставляют лучшую производительность для ряда приложений,
может оказаться представляющим больший результат написание абстрактного программного уровня для отслеживания
событий файлового дескриптора. Имея такой уровень, переносимая программа может применять
epoll
(или аналогичный API) в тех системах, которые его предоставляют,
и в то же время откатывать обратно к select()
и
poll()
в прочих системах.
Замечание | |
---|---|
Библиотека |
Прежде чем мы обсудим более подробно различные альтернативные механизмы ввода/ вывода, нам необходимо обсудить две модели уведомлений о готовности для некоторого файлового дескриптора:
-
Переклчаемое уровнем уведомление: Некий файловый дескриптор рассматривается как готовый если имеется возможность выполнениря какого- то системного вызова ввода/ вывода без блокирования.
-
Переклчаемое фронтом уведомление: Уведомление осуществляется если существует некая активность ввода/ вывода (например, новый ввод) в каком- то файловом дескрипторе с момента последнего мониторинга.
Таблице 63.1 суммирует все модели уведомления,
производимые моделями мультиплексирования, управляемого сигналом ввода/ вывода и epoll
.
API epoll
отличается от остальных двух моделей ввода/ вывода тем,
что она может предоставлять и управляемое уровнем уведомление (по умолчанию), и управляемое фронтом
уведомление.
Модель ввода/ вывода | Переключаемая уровнем? | Переключаемая фронтом? |
---|---|---|
|
+ |
|
Движимый сигналом ввод/ вывод |
|
+ |
|
+ |
+ |
Подробности отличий этих двух моделей уведомления станут яснее на протяжении данной главы. На текущий момент мы опишем как сам выбор модели уведомления воздействует на способ, которым мы разрабатываем некую программу.
Когда мы применяем управляемые уровнем уведомления, мы можем проверить готовность некоторого файлового дескриптора в любой момент. Это означает, что когда мы определили, что некий файловый дескриптор готов (например, он имеет доступным ввод), мы можем выполнить в этом дескрипторе некий ввод/ вывод, а затем повторить саму операцию мониторинга чтобы проверить, что данный дескриптор всё ещё готов (например, он всё ещё имеет доступным дополнительный ввод), и в этом случае мы можем выполнять дополнительный ввод/ вывод снова и снова. Другими словами, так как управляемая уровнем модель позволяет нам повторять подобную операцию мониторинга ввода/ вывода в любое время, нет необходимости выполнять столько операций ввода/ вывода, сколько возможно (например, считать все доступные байты) для данного файлового дескриптора (или даже вовсе не выполнять никакого ввода/ вывода) всякий раз, когда мы уведомлены что некий файловый дескриптор готов.
Напротив, когда мы применяем управляемое фронтом уведомление, мы получаем такое уведомление только когда происходит некое событие ввода/ вывода. Мы не получаем никаких последующих уведомлений пока не произойдёт другое обытие ввода/ вывода. Более того, когда мы получаем некоторое уведомление о каком- то событии ввода/ вывода для некоторого файлового дескриптора, мы обычно не знаем в точности сколько операций ввода/ вывода возможно (например, сколько байт доступно для считывания). Таким образом, реализующая переключаемое фронтом уведомление программа обычно разрабатывается в соответствии со следующими правилами:
-
После получения уведомления о некотором событии ввода/ вывода, данная программа должна - в некоторый момент - выполнить столько операций ввода/ вывода, сколько она может (например, считать столько байт, сколько возможно) для текущего файлового дескриптора. Если данная программа получает отказ исполнения таких операций, тогда она может упустить имеющуюся возможность выполнить некоторые операции ввода/ вывода, поскольку не будет знать о необходимости работы с данным файловым дескриптором пока не возникнет другое событие ввода/ вывода. Мы говорим "в некоторый момент", так как иногда может оказаться нежелательным выполнять все имеющиеся операции ввода/ вывода немедленно после того, как мы определили что данный файловый дескриптор готов. Проблема состоит в том, что мы можем посадить на голодный паёк прочие требующие внимание файловые дескрипторы, если мы выполняем большое число операций ввода/ вывода с одним файловым дескриптором. Мы рассмотрим более подробно этот момент когда мы будем обсуждать модель управляемого фронтом уведомления для
epoll
в Разделе 63.4.6. -
Если данная программа применяет некий цикл для выполнения такого числа операций ввода/ вывода, сколько их можно сделать для данного файлового дескриптора, а данный дескриптор помечен как блокируемый, тогда со временем некий системный вызов ввода/ вывода будет блокирован в случае, когда более нет возможных операций ввода/ вывода. По этой причине все отслеживаемый файловые дескрипторы обычно помещаются в неблокируемый режим, а после получения уведомления некоторого события ввода/ вывода, операции ввода/ вывода выполняются с повтором, пока соответствующий системный вызов (например,
read()
илиwrite()
) не получит отказ с определённой ошибкойEAGAIN
илиEWOULDBLOCK
.
Неблокируемый ввод/ вывод (с установленным флагом O_NONBLOCK
)
часто применяется совместно с той из моделей ввода/ вывода, которые описываются в данной главе. Некие примеры того,
почему это может быть полезным приводятся ниже:
-
Как было пояснено в предыдущем разделе, неблокируемый ввод/ вывод обычно применяется совместно с моделями ввода/ вывода, которые предоставляют управляемые фронтом уведомления событий ввода/ вывоода.
-
Если с одними и теми же файловыми дескрипторами выполняет операции ввода/ вывода множество процессов (или потоков), тогда, с точки зрения определённого процесса, готовность некоторого дескриптора может измениться между временем когда данный дескриптор уведомил о готовности и моментом времени последующего ввода/ вывода. Следовательно, блокируемый вызов ввода/ вывода может быть блокирован, что не даёт возможности данному процессу отслеживать прочие файловые дескрипторы. (Это может произойти для всех тех моделей ввода/ вывода, которые мы обсуждаем в данной главе вне зависимости от того будет она реализовывать уведомление управляемое уровнем или фронтом.)
-
Даже после того как управляемые уровнем API, такие как
select()
илиpoll()
проинформируют нас что некий файловый дескриптор для какого- то потокового сокета готов для записи, если мы записываем достаточно большой блок данных некоторым отдельнымselect()
илиpoll()
, тогда данный вызов будет заблокирован навсегда. -
В некоторых случаях переключаемые уровнем API, такие как
select()
илиpoll()
, могут возвращать фиктивные уведомления о готовности - они могут ложно информировать нас что некий файловый дескриптор готов. Это может быть вызвано некой ошибкой ядра или быть ожидаемым поведением в некотором необбычном сценарии.Замечание Раздел 16.6 [Stevens et al., 2004] описывает один из примеров неожиданных уведомлений в системах BSD для какого- то прослушиваемого сокета. Если некий клиент подключён к какому- то прослушиваемому сокету и затем сбрасывает это соединение, некий выполняемый данным сервером
select()
между такими двумя событиями будет указывать данный отслеживаемый сокет как находящийся в состоянии готовности, однако последующийaccept()
, который исполняется после данного сброса клиента будет блокирован.
Мультиплексирование ввода/ вывода позволяет нам одновременно отслеживать множество файловых дескрипторов с
тем, чтобы увидеть что в ком- то из них возможен ввод/ вывод. Мы можем выполнить мультиплексирование ввода/
вывода с применением с любым из двух системных вызовов с одной и той же функциональностью в конечном итоге.
Первый из них, select()
, появился в API для сокетов в BSD. Исторически он
наиболее широко распространён из данных двух системных вызовов. Другой системный вызов,
poll()
, появился в System V. Оба системных вызова, и
select()
, и poll()
в наши дни
являются необходимыми согласно SUSv3.
Мы можем применять select()
и poll()
для отслеживания файловых дескрипторов обычных файлов, терминалов, псевдотерминалов, конвейеров, FIFO, сокетов и
некоторых типов символьных устройств. Оба системных вызова позволяют некоторому процессу либо блокировать
неограниченное ожидание пока не станут готовыми файловые дескрипторы, либо определить некий таймаут для данного
вызова.
Системный вызов select()
блокируется пока не станут готовыми один или более
из некоторого набора файловых дескрипторов.
#include <sys/time.h> /* Для переносимости */
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,
struct timeval *timeout);
Возвращает общее число файловых дескрипторов, 0 в случае таймаута, либо -1 при ошибке
Перечисляемые аргументы nfds
, readfds
,
writefds
и exceptfds
определяют сами файловые
дескрипторы, которые должен отслеживать select()
. Аргумент
timeout
может быть применён для установки некоторого верхнего предела по времени,
на которое будет блокирован select()
. Далее мы подробно опишем все эти
аргументы.
Замечание | |
---|---|
В показанном выше прототипе для |
Наборы файловых дескрипторов
Аргументы readfds
, writefds
и
exceptfds
являются указателями на наборы
файловых дескрипторов, представляемых с применением установленного типа данных
fd_set
. Эти аргументы используются следующим образом:
-
readfds
является определённым набором файловых дескрипторов подлежащих тестированию на предмет того доступен ли ввод; -
writefds
является определённым набором файловых дескрипторов подлежащих тестированию на предмет того доступен ли вывод; -
exceptfds
является определённым набором файловых дескрипторов подлежащих тестированию на предмет того не произошло ли исключительное условие.
Термин исключительное условие (exceptional condition) зачастую неверно понимается как некое условие ошибки, произошедшей в определённом файловом дескрипторе. Это не так в данном случае. Некое исключительное условие происходит всего лишь при двух обстоятельствах в Linux (прочие реализации UNIX аналогичны):
-
На псевдотерминальном подчинённом устройстве, подключённом к хозяину, находящемся в пакетном режиме, происходит некое изменение состояния (раздел 64.5).
-
В некотором сокете потока принимаются данные выходящие за полосу пропускания раздел 61.13.1.
Обычно конкретный тип данных fd_set
реализуется как маска бит. Однако,
нам нет нужды знать подробности, так как все манипуляции с наборами файловых дескрипторов выполняются через
четыре макроса: FD_ZERO()
,
FD_SET()
,
FD_CLR()
и
FD_ISSET()
.
#include <sys/select.h>
void FD_ZERO(fd_set *fdset);
void FD_SET(int fd, fd_set *fdset);
void FD_CLR(int fd, fd_set *fdset);
int FD_ISSET(int fd, fd_set *fdset);
Возвращает истину (1),если fd присутствует в fdset, либо kj;m (0) в противном случае
Эти макросы работают следующим образом:
-
FD_ZERO()
инициализирует тот набор, на который указываетfdset
как пустой. -
FD_SET()
добавляет определённый файловый дескрипторfd
в тот набор, на который указываетfdset
. -
FD_CLR()
удаляет определённый файловый дескрипторfd
из того набора, на который указываетfdset
. -
FD_ISSET()
возвращает истину, если определённый файловый дескрипторfd
является участником того набора, на который указываетfdset
.
Некий набор файловых дескрипторов имеет какой- то максимальный размер, определяемый установленной константой
FD_SETSIZE
.
В Linux значение этой константы имеет величину 1024. (Прочие реализации UNIX имеют аналогичные значения для
данного предела.)
Замечание | |
---|---|
Даже несмотря на то, что макросы |
Аргументы readfds
, writefds
и
exceptfds
все являются передающими значение. Прежде чем вызвать
select()
, все структуры fd_set
,
указывающие на эти аргументы, должны быть проинициализированны (с применением
FD_ZERO()
или
FD_SET()
) чтобы содержать набор
представляющих интерес файловых дескрипторов. Сам вызов select()
изменяет эти структуры таким образом, что после возврата они содержат тот набор файловых дескрипторов, который
пребывает в состоянии готовности. (Так как эти структуры изменяются данным вызовом, мы должны обеспечивать
их повторную инициализацию если мы повторно вызываем select()
в рамках
некоторого цикла.) Эти структуры впоследствии могут исследоваться при помощи
FD_ISSET()
.
Если мы не интересуемся неким определённым классом событий, тогда соответствующий аргумент
fd_set
может быть определён как
NULL
. Мы обсудим дополнительно более
точное значение каждого из этих трёх типов событий в Разделе
63.2.3.
Аргумент nfds
должен быть установлен в значение болшее, чем наивысшее
число файловых дескрипторов, содержащихся во всех трёх наборах файловых дескрипторов. Этот аргумент позволяет
select()
быть более эффективным, так как ядро в этом случае будет осведомлено
об отсутствии необходимости проверять большее число файловых дескрипторов, нежели это значение для части каждого
набора файловых дескрипторов.
Аргумент timeout
Аргумент timeout
управляет поведением блокирования
select()
. Он может быть определён либо как
NULL
и в этом случае
select()
блокируется бесконечно, либо как указатель на некую структуру
timeout
:
struct timeval {
time_t tv_sec; /* Секунды */
suseconds_t tv_usec; /* Миллисекунды (long int) */
};
Если оба поля timeout
равны 0
,
тогда select()
не блокируется; он просто опрашивает все определённые
файловые дескрипторы для просмотра того, не готовы ли они и возвращает результат немедленно. В противном
случае timeout
определяет некий верхний предел времения, который
select()
должен выполнять ожидание.
Хотя данная структура timeval
в состоянии придерживаться точности в
микросекундах, реальная точность данного вызова ограничена гранулярностью программно определяемых часов
(Раздел 10.6). SUSv3 определяет, что значение таймаута
округляется вверх если не способно в точности быть кратной такой гранулярности.
Замечание | |
---|---|
SUSv3 требует чтобы значение максимума разрешённого интервала таймаута составляло по крайней мере 31 день.
Большая часть реализаций UNIX позволяет значительно более высокий предел. Так как Linux/ x86-32 применяет
некое 32- битное целое для типа |
Когда значение timeout
установлено в
NULL
или указывает на структуру,
содержащую ненулевые поля, select()
блокируется пока не произойдёт
одно из следующих событий:
-
по крайней мере один из тех файловых дескрипторов, которые определены в
readfds
,writefds
илиexceptfds
, не станет готовым; -
данный вызов прерывается неким обработчиком сигнала; или
-
всё определённое значением
timeout
время истекло.Замечание В более ранних реализациях UNIX, у которых отсутствовал вызов бездействия с точностью в долях секунды (например, для
nanosleep()
,select()
применялся для эмуляции данной функциональности путём определенияnfds
равным0
;readfds
,writefds
илиexceptfds
устанавливаются в значениеNULL
; а желаемое значение интервала бездействия вtimeout
.)
В Linux, если select()
получает возврат благодаря тому, что один или
более файловых дескрипторов становится готовым, а timeout
был не
NULL
, тогда
select()
обновляет саму структуру, некоторую указывает
timeout
для отражения того, сколько времени остаётся до выхода данного
вызова по таймауту. Однако, такое поведение зависит от реализации. SUSv3 также позволяет чтобы некая реализация
оставляла данную указываемую со стороны timeout
структуру неизменной, причём
большинство прочих реализаций UNIX не изменяют эту структуру.
Переносимые решения, которые применяют select()
внутри некоторого цикла
должны всегда обеспечивать чтобы та структура, на которую указывает timeout
,
инициализируется перед каждым вызовом select()
и должны игнорировать
ту информацию, которая возвращается в этой структуре после данного вызова.
SUSv3 устанавливает, что та структура, на которую указывает timeout
может быть изменён только после успешного возврата из select()
.
Однако, в Linux, если select()
прерывается неким обработчиком сигнала
(скажем, если получает отказ с возвращаемой ошибкой
EINTR
), тогда данная структура изменяется
с тем, чтобы отображать то время, которое осталось до выхода из некоторого таймаута (т.е. как это происходит
в случае некоторого успешного возврата).
Замечание | |
---|---|
Если мы применяем специфичный для Linux системный вызов |
Возвращаемое select()
значение
В результате своей работы select()
возвращает одно из следующего
списка:
-
Некое возвращаемое значение
-1
указывает на то, что произошла какая- то ошибка. Возможные ошибки включают в себяEBADF
иEINTR
.EBADF
указывает, что один изreadfds
,writefds
илиexceptfds
файловых дескрипторов является не верным (например, в настоящее время не открыт).EINTR
указывает, что данный вызов был прерван неким обработчиком сигнала. (Как отмечалось в Разделе 21.5,select()
никогда автоматически не запускается повторно если он прерван неким обработчиком сигнала.) -
Возвращаемое значение
0
означает, что данный вызов завершён по таймауту до того, как некий файловый дескриптор стал готовым. В этом случае, все возвращаемые наборы файловых дескрипторов будут пустыми. -
Положительное возвращаемое значение означает готовность одного или более файловых дескрипторов. Само возвращаемое значение является общим числом готовых дескрипторов. В данном случае, каждый из таких возвращаемых наборов файловых дескрипторов далжен быть опрошен (с применением
FD_ISSET()
) чтобы определить в ком из них произошли события ввода/ вывода. Если один и то же файловый дескриптор определён в более чем одном изreadfds
,writefds
илиexceptfds
, он перечисляется монжество раз, как если бы он был готов более чем для одного события. Другими словами,select()
возвращает общее число файловых дескрипторов помеченных как готовые во всех трёх возвращаемых наборах.
Пример программы
Приводимая в Листинге 63.1 программа
демонстрирует применение select()
. Используя аргументы командной
строки, мы можем определить значение timeout
и те файловые
дескрипторы, которые мы желаем отслеживать. Самый первый аргумент определяет значение
timeout
для select()
в
секундах. Если здесь определён некий дефис (-
), тогда
select()
вызывается с некоторой установкой в
NULL
величины таймаута, что
означает неограниченное блокирование. Все оставшиеся аргументы командной строки определяют определённое
число подлежащих отслеживанию файловых дескрипторов, за которыми следуют буквы, указывающие те операции,
которые должны проверяться для данного дескриптора. Теми буквами, которые мы можем здесь определить, являются
r
(готов к чтению) и w
(готов к записи).
Листинг 63.1: Применение select()
для отслеживания множества файловых дескрипторов
---------------------------------------------------------------------- altio/t_select.c
#include <sys/time.h>
#include <sys/select.h>
#include "tlpi_hdr.h"
static void
usageError(const char *progName)
{
fprintf(stderr, "Применение: %s {timeout|-} fd-num[rw]...\n", progName);
fprintf(stderr, " - означает неограниченный таймаут; \n");
fprintf(stderr, " r = отслеживать чтение\n");
fprintf(stderr, " w = отслеживать запись\n\n");
fprintf(stderr, " например: %s - 0rw 1w\n", progName);
exit(EXIT_FAILURE);
}
int
main(int argc, char *argv[])
{
fd_set readfds, writefds;
int ready, nfds, fd, numRead, j;
struct timeval timeout;
struct timeval *pto;
char buf[10]; /* Достаточно велик чтобы содержать "rw\0" */
if (argc < 2 || strcmp(argv[1], "--help") == 0)
usageError(argv[0]);
/* Таймаут для select() определяется в argv[1] */
if (strcmp(argv[1], "-") == 0) {
pto = NULL; /* Неограниченный таймаут */
} else {
pto = &timeout;
timeout.tv_sec = getLong(argv[1], 0, "timeout");
timeout.tv_usec = 0; /* Число микросекунд */
}
/* Остальные аргументы процесса для построения наборов файловых дескрипторов */
nfds = 0;
FD_ZERO(&readfds);
FD_ZERO(&writefds);
for (j = 2; j < argc; j++) {
numRead = sscanf(argv[j], "%d%2[rw]", &fd, buf);
if (numRead != 2)
usageError(argv[0]);
if (fd >= FD_SETSIZE)
cmdLineErr("file descriptor exceeds limit (%d)\n", FD_SETSIZE);
if (fd >= nfds)
nfds = fd + 1; /* Записать максимально fd + 1 */
if (strchr(buf, 'r') != NULL)
FD_SET(fd, &readfds);
if (strchr(buf, 'w') != NULL)
FD_SET(fd, &writefds);
}
/* Мы построили все необходимые аргументы; теперь мы вызываем select() */
ready = select(nfds, &readfds, &writefds, NULL, pto);
/* Игнорироввание исключающих событий */
if (ready == -1)
errExit("select");
/* Отобразить результаты select() */
printf("ready = %d\n", ready);
for (fd = 0; fd < nfds; fd++)
printf("%d: %s%s\n", fd, FD_ISSET(fd, &readfds) ? "r" : "",
FD_ISSET(fd, &writefds) ? "w" : "");
if (pto != NULL)
printf("timeout after select(): %ld.%03ld\n",
(long) timeout.tv_sec, (long) timeout.tv_usec / 10000);
exit(EXIT_SUCCESS);
}
---------------------------------------------------------------------- altio/t_select.c
В приводимом ниже журнале сеанса оболочки мы демонстрируем вариант использования данной программы из
Листинга 63.1. В данном первом примере мы выполняем
запрос для отслеживания файлового дескриптора 0
на ввод с неким
10- секундным timeout
:
$ ./t_select 10 0r
Нажмите Enter, чтобы в файловом дескрипторе 0 была доступна некая строка ввода
ready = 1
0: r
timeout after select(): 8.003
$ Отображается следующее приглашение оболочки
Приводимый выше вывод показывает нам что select()
определил,
что один из файловых дескрипторов был готов. Это был файловый дескриптор 0
,
который был готов для чтения. Мы также можем отметить, что значение timeout
было изменено. Самая последняя строка вывода, состоящая всего из самого приглашения оболочки
($
), появилось потому что данная программа
t_select
не считала данный символ новой строки, который сделал файловый
дескриптор 0
готовым и по этой причине данный символ был считан самой оболочкой,
которая ответила выводом очередного приглашения.
В нашем следующем примере мы снова отслеживаем файловый дескриптор 0
на ввод, однако в этот раз с неким timeout
в
0
секунд:
$ ./t_select 0 0r
ready = 0
timeout after select(): 0.000
Данный вызов select()
осуществляет немедленный возврат и не обнаруживает
никакого файлового дескриптора готовым.
В своём следующем примере мы отслеживаем два файловых дескриптора: дескриптор 0
для просмотра того доступен ли ввод и дескриптор 1
для просмотра возможен ли
вывод. В данном случае мы определили значение timeout
как
NULL
(самый первый аргумент командной
строки установлен в значение дефиса), что означает снятие ограничений по времени:
$ ./t_select - 0r 1w
ready = 1
0:
1: w
Данный вызов select()
возвращается немедленно, что сообщает нам о том,
что для файлового дескриптора 1
был возможен вывод.
Системный вызов poll()
выполняет задачу, аналогичную
select()
. Основная разница между этими двумя системными вызовами
состоит в том, как мы определяем все подлежащие мониторингу файловые дескрипторы. Для
select()
мы предоставляем три набора, причём каждый отмечает указание на
те файловые дескрипторы, которые представляют интерес. В случае с poll()
мы предоставляем некий список файловых дескрипторов, в котором каждый отмечает некий набор представляющих
интерес событий.
#include <poll.h>
int poll(struct pollfd fds[], nfds_t nfds, int timeout,
struct timeval *timeout);
Возвращает общее число файловых дескрипторов, 0 в случае таймаута, либо -1 при ошибке
Аргумент fds
и заданный массив pollfd
(nfds
) определяют все файловые дескрипторы, которые отслеживает
poll()
. Значение аргумента timeout
может применяться для установки некоторого верхнего предела по тому времени, которое
poll()
будет блокирован. Ниже мы определяем все эти аргументы более
подробно.
Массив pollfd
Аргумент fds
перечисляет все подлежащие мониторингу со стороны
poll()
файловые дескрипторы. Этот аргумент является неким массивом
со структурой pollfd
, определяемой следующим образом:
struct pollfd {
int fd; /* Файловый дескриптор */
short events; /* Битовая маска запрашиваемых событий */
short revents; /* Битовая маска возвращааемых событий */
};
Аргумент nfds
определяет общее число элементов в массиве
fds
. Тип данных nfds_t
применяется для указания типа самого аргумента nfds
в качестве
цлого типа без знака.
Поля events
и revents
самой
структуры pollfd
являются битовыми масками. Вызывающая сторона
инициализирует events
для определения тех событий, которые подлежат
мониторингу для определённого файлового дескриптора fd
. По возвращению из
poll()
, revents
установлены
но то, чтобы указывать какие именно из этих событий в действительности произошли для данного файлового
дескриптора.
Таблица 63.2 перечисляет все биты, которые могут
появится в полях events
и revents
.
Первая группа бит в данной таблице (POLLIN
,
POLLRDNORM
,
POLLRDBAND
,
POLLPRI
и
POLLRDHUP
) связаны с событиями на входе. Следующая группа
бит (POLLOUT
,
POLLWRNORM
и
POLLWRBAND
) связана с событиями на
выходе. Третья группа бит (POLLERR
,
POLLHUP
и
POLLNVAL
) является набором в нашем поле
revents
для возврата дополнительной информации о данном файловом
дескрипторе. Если они определены в поле нашего events
, тогда эти три
бита игнорируются. Самый последний бит (POLLMSG
)
не применяется в poll()
Linux.
Замечание | |
---|---|
В реализациях UNIX, поддерживающих устройства STREAMS,
|
Бит | Входной в events ? |
Возвращаемый revents ? |
Описание |
---|---|---|---|
|
+ |
+ |
Могут быть считаны данные с приоритетом, отличающимся от высокого |
|
+ |
+ |
Эквивалентно |
|
+ |
+ |
Могут быть считаны приоритетные данные (не применяется в Linux) |
|
+ |
+ |
Могут быть считаны данные с высоким приоритетом |
|
+ |
+ |
Отключить одноранговый сокет |
|
+ |
+ |
Могут быть записаны обычные данные |
|
+ |
+ |
Эквивалентно |
|
+ |
+ |
Могут быть записаны данные с высоким приоритетом |
|
|
+ |
Произошла ошибка |
|
|
+ |
Произошло отсоединение |
|
|
+ |
Файловый дескриптор не открыт |
|
|
|
Linux (и не определён в SUSv3) |
Допускается определять events
значением
0
если мы не интересуемся событиями в некотором определённом файловом
дескрипторе. Более того, определение некоторого отрицательного значения для данного поля
fd
(например, изменение знака для ненулевого значения) вызывает
игнорирование соответствующего поля в events
, а аналогичное в
revents
поле всегда будет возвращаться со значением
0
. Любой из этих подходов может быть применён (возможно, временно)
для отключения отслеживания некоторого отдельного файлового дескриптора без необходимости перестраивать весь список
fds
.
Отметим следующие моменты, зависящие от конкретной реализации poll()
в Linux:
-
Хотя они и определены как отдельные биты,
POLLIN
иPOLLRDNORM
являются синонимами. -
Хотя они и определены как отдельные биты,
POLLIN
иPOLLWRNORM
являются синонимами. -
POLLRDBAND
обычно не используется; то есть он игнорируется в соответствующем полеevents
и не устанавливается вrevents
.Замечание Единственное место где устанавливается
POLLRDBAND
, это реализация кода (устаревшего) сетевого протокола DECnet. -
Хотя он и устанавливается для сокетов при определённых обстоятельствах, передача
POLLWRBAND
не является полезной информацией. (Не существует обстоятельств, при которыхPOLLWRBAND
установлен, в то время какPOLLOUT
иPOLLRDNORM
не установлены.)Замечание POLLRDBAND
иPOLLWRBAND
представлены в реализациях, которые представляют System V STREAMS (чего не делает Linux). При использовании STREAMS, некому сообщению может быть назначен какой- то ненулевой приоритет и такие сообщения устанавливаются в очередь самого получателя в порядке убывания приоритета в некоторой полосе, превышающей обычные сообщения (со значением приоритета 0). -
Проверочная функция макроса
_XOPEN_SOURCE
должна быть определена для получения определений, которые содержатPOLLRDNORM
,POLLRDBAND
,POLLWRNORM
иPOLLWRBAND
из в<poll.h>
. -
POLLRDHUP
является флагом, особенным для Linux, доступным начиная с ядра 2.6.17. Чтобы получить это определение из<poll.h>
, должен быть определена тестовая функция макроса_GNU_SOURCE
. -
POLLNVAL
возвращается если определённый файловый дескриптор был закрыт в тот момент, когда выполнялся данный вызовpoll()
.
Суммируя все перечисленные выше моменты, теми флагами, которые представляют интерес для
poll()
, являются POLLIN
,
POLLOUT
,
POLLPRI
,
POLLRDHUP
,
POLLHUP
и
POLLERR
. Мы более подробно рассмотрим
значения этих флагов в Разделе 63.2.3.
Аргумент timeout
Аргумент timeout
определяет поведение блокирования
poll()
следующим образом:
-
Если
timeout
равен-1
, блокирование выполняется пока один из перечисленных в массивеfds
файловых дескрипторов не станет готовым (что определяется соответствующим полемevents
) или не будет перехвачен некий сигнал. -
Если
timeout
равен0
, блокировка не осуществляется - просто выполняется проверка на предмет того какие из файловых дескрипторов готовы. -
Если
timeout
больше0
, выполняется блокирование доtimeout
миллисекунд пока один из файловых дескрипторов вfds
не станет готовым или пока не перехвачен некий сигнал.
Как и в случае с select()
, точность
timeout
ограничена имеющейся гранулярностью программно определяемых
часов (Раздел 10.6), а SUSv3 определяет, что
timeout
всегда округляется вверх если он в точности не является кратным
гранулярности значения часов.
Возвращаемые poll()
значения
В качестве своего результата работы poll()
возвращает следующее:
-
Некое возвращаемое значение
-1
указывает на то, что произошла какая- то ошибка. Одной возможной ошибкой являетсяEINTR
, которая указывает, что данный вызов был прерван неким обработчиком сигнала. (Как отмечалось в Разделе 21.5,poll()
никогда автоматически не запускается повторно если он прерван неким обработчиком сигнала.) -
Возвращаемое значение
0
означает, что данный вызов завершён по таймауту до того, как некий файловый дескриптор стал готовым. -
Положительное возвращаемое значение означает готовность одного или более файловых дескрипторов. Само возвращаемое значение является общим числом структур
pollfd
в имеющемся массивеfds
, который имеет некое ненулевое полеrevents
.Замечание Отметим небольшое отличие положительного возвращаемого значения между
select()
иpoll()
. Системный вызовselect()
насчитывает файловые дескрипторы, помножая их на число раз, которое они появляются более чем в одном возвращаемом наборе файловых дескрипторов. Системный вызовpoll()
вовращает некое значение счётчика готовых файловых дескрипторов, причём файловый дескриптор считается только один раз, даже если установлено множество бит в соответствующем полеrevents
.
Пример программы
Листинга 63.2 предоставляет простую демонстрацию
применения poll()
. Эта программа создаёт некое число конвейеров (каждый
конвейер применяет последовательную пару файловых дескрипторов), записывает байты со своих сторон записи в
выбранных случайным образом конвейерах и затем выполняет poll()
для того,
чтобы определить какие конвейеры имеют доступные для чтения данные.
Следующий сеанс оболочки показывает пример того, что мы видим когда исполняем данную программу. Аргументы командной строки для данной программы определяют, что должно быть создано десять конвейеров, а запись должна выполняться в три, выбираемых случайным образом.
$ ./poll_pipes 10 3
Writing to fd: 4 (read fd: 3)
Writing to fd: 14 (read fd: 13)
Writing to fd: 14 (read fd: 13)
poll() returned: 2
Readable: 3
Readable: 13
Из приведённого выше вывода мы можем видеть, что poll()
обнаружил два конвейера, имеющие доступные для считывания данные.
Листинг 63.2: Применение poll()
для отслеживания множества файловых дескрипторов
---------------------------------------------------------------------- altio/poll_pipes.c
#include <time.h>
#include <poll.h>
#include "tlpi_hdr.h"
int
main(int argc, char *argv[])
{
int numPipes, j, ready, randPipe, numWrites;
int (*pfds)[2]; /* Файловые дескрипторы для всех конвейеров */
struct pollfd *pollFd;
if (argc < 2 || strcmp(argv[1], "--help") == 0)
usageErr("%s num-pipes [num-writes]\n", argv[0]);
/* разместить все применяемые нами массивы. Размеры этих массивов устанавливаются в
соответствии с общим числом определённых в коммандной строке конвейеров */
numPipes = getInt(argv[1], GN_GT_0, "num-pipes");
pfds = calloc(numPipes, sizeof(int [2]));
if (pfds == NULL)
errExit("malloc");
pollFd = calloc(numPipes, sizeof(struct pollfd));
if (pollFd == NULL)
errExit("malloc");
/* Создаём все определённые в командной строке конвейеры */
for (j = 0; j < numPipes; j++)
if (pipe(pfds[j]) == -1)
errExit("pipe %d", j);
/* Выполняем определённое число записей в случайные конвейеры */
numWrites = (argc > 2) ? getInt(argv[2], GN_GT_0, "num-writes") : 1;
srandom((int) time(NULL));
for (j = 0; j < numWrites; j++) {
randPipe = random() % numPipes;
printf("Writing to fd: %3d (read fd: %3d)\n",
pfds[randPipe][1], pfds[randPipe][0]);
if (write(pfds[randPipe][1], "a", 1) == -1)
errExit("write %d", pfds[randPipe][1]);
}
/* Строим полный список файловых дескрипторов, которые будут поддерживаться poll(). Этот список
устанавливается с тем, чтобы он содержал все файловые дескрипторы для сторон чтения для всех
определённых конвейеров. */
for (j = 0; j < numPipes; j++) {
pollFd[j].fd = pfds[j][0];
pollFd[j].events = POLLIN;
}
ready = poll(pollFd, numPipes, -1); /* Без блокирования */
if (ready == -1)
errExit("poll");
printf("poll() returned: %d\n", ready);
/* Проверяем какие из конвейеров имеют доступные для чтения данные */
for (j = 0; j < numPipes; j++)
if (pollFd[j].revents & POLLIN)
printf("Readable: %d %3d\n", j, pollFd[j].fd);
exit(EXIT_SUCCESS);
}
---------------------------------------------------------------------- altio/poll_pipes.c
Правильное применение select()
и
poll()
требует понимания тех условий при которых некоторый файловый
дескриптор указывается как готовый. SUSv3 утверждает, что некий файловый дескриптор (с очищенным
O_NONBLOCK
) рассматривается как
готовый, если некий вызов функции ввода/ вывода не будет блокирован вне зависимости
от того осуществляет ли данная функция реальный обмен данными. Ключевой момент выделен курсивом:
select()
и poll()
сообщают нам
не будет ли некая операция ввода/ вывода блокирована, вместо того будет ли она осуществлять успешный обмен
данными. В этом свете давайте рассмотрим как эти системные вызовы работают с различными типами файловых
дескрипторов. Мы отобразим эту информацию в таблицах, содержащих два столбца:
-
Столбец
select()
указывающий будет ли некий файловый дескриптор помечен как доступный на чтение (r
), доступный на запись (w
) или имеющий исключительное условие (x
). -
Столбец
poll()
указывающий те биты, которые возвращаются в его полеrevents
. В этих таблицах мы опускаем упоминание оPOLLRDNORM
,POLLWRNORM
,POLLRDBAND
иPOLLWRBAND
. Хотя некоторые из этих флагов и могут возвращаться вrevents
при различных обстоятельствах (если они определены вevents
), они не передают никакой полезной информации выходящей за рамки предоставляемойPOLLIN
,POLLOUT
,POLLHUP
иPOLLERR
.
Обычные файлы
Файловые дескрипторы, указывающие на обычные файлы всегда помечены как доступные для чтения и записи со
стороны select()
и возвращаются с установленными
POLLIN
и
POLLOUT
в
revents
для poll()
по
следующим причинам:
-
read()
будет всегда немедленно возвращать данные, признак конца-файла (eof) или некую ошибку (например, данный файл не был открыт на чтение). -
write()
будет всегда немедленно осуществлять обмен данными или выполнит отказ с некоторой ошибкой.Замечание SUSv3 утверждает, что
select()
также должен помечать некий дескриптор для какого- то обычного файла как имеющий исключительное условие (хотя это не имеет никакого очевидного значения для обычных файлов). Только некоторые реализации придерживаются этого; Linux относится к тем, которые не придерживаются этого.
Терминалы и псевдотерминалы
Таблица 63.3 суммирует поведение
select()
и poll()
для
терминалов и псевдотерминалов (Глава 64).
Когда одна половина некоторой пары псевдотерминала закрыта, те установки
revents
, которые возвращаются poll()
для другой половины данной пары зависят от конкретной реализации. В Linux устанавливается, по крайней мере,
флаг POLLHUP
. Однако прочие реализации
возвращают различные флаги для указания данного события - например,
POLLHUP
,
POLLERR
или
POLLIN
. Более того, в некоторых
реализациях устанавливаемые флаги зависят от того будет ли отслеживаться данное устройство хозяин или
подчинённый.
Условие или событие | select() |
poll() |
---|---|---|
Ввод доступен |
|
|
Вывод возможен |
|
|
После |
|
см. в тексте |
Хозяин псевдотерминала в пакетном режиме обнаружил изменение зависимого состояния |
|
|
Конвейеры и FIFO
Таблица 63.4 суммирует все подробности для
стороны чтения конвейера или FIFO. Столбец Данные в конвейере?
указывает имеет ли данный конвейер по крайней мере 1 байт доступных для чтения данных. В данной таблице мы
предполагаем, что POLLIN
был
определён в своём поле events
для
poll()
.
В некоторых прочих реализациях UNIX если сторона записи какого- то конвейера закрыта, тогда вместо возврата с
установленным POLLHUP
poll()
возвращается с установленным битом
POLLIN
(так как read()
возвращается немедленно с признаком конца-файла - eof).
переносимые приложения должны проверять установлен ли бит чтобы знать будет ли блокирована операция
read()
.
Таблица 63.5 суммирует все подробности для
стороны записи в конвейере. В данной таблице мы предполагаем, что для poll()
был определён POLLOUT
в поле
events
. Колонка Есть ли пространство для
байтов PIPE_BUF
? указывает имеет ли данный конвейер место для отомарной {неделимой}
записи PIPE_BUF
байт без
блокирования. Именно это тот критерий, по которому Linux рассматривает готовность некоторого конвейера к
записи. Некоторые прочие реализации UNIX применяют тот же самый критерий; остальные рассматривают конвейер
как готовый к записи если даже один байт может быть записан. (В Linux 2.6.10 и более ранних версиях данная
ёмкость конвейера была той же что и PIPE_BUF
).
Это означает, что некий конвейер рассматривается как не готовый к записи даже если она и содержит отдельный байт
данных.)
В некоторых прочих реализациях UNIX, если считываемая сторона конвейера закрыта, тогда вместо возврата
установленным POLLERR
,
poll()
возвращает установленным либо бит
POLLOUT
, либо
POLLHUP
. Переносимое приложение
требует проверки того установлен ли какой- либо из этих битов для определения блокирования некоторой
операции write()
.
Условие или событие | select() |
poll() |
|
---|---|---|---|
Данные в конвейере? |
Открыта ли сторона записи? |
||
нет |
нет |
|
|
да |
да |
|
|
да |
нет |
|
|
Условие или событие | select() |
poll() |
|
---|---|---|---|
Есть ли пространство для байтов |
Открыта ли сторона чтения? |
||
нет |
нет |
|
|
да |
да |
|
|
да |
нет |
|
|
Сокеты
Таблица 63.6 суммирует все поведения
select()
и poll()
для сокетов.
Для столбца poll()
мы предполагаем, что events
были определены как (POLLIN
|
POLLOUT
|
POLLPRI
). Для колонки
select()
мы предполагаем, что данный файловый дескриптор был проверен
на предмет того, возможен ли ввод, возможен ли вывод или не произошло ли исключительное состояние (т.е. что данный
файловый дескриптор определён во всех трёх передаваемых select()
наборах).
Данная таблица покрывает только общие случаи, не все возможные сценарии.
Замечание | |
---|---|
В Linux поведение |
Условие или событие | select() |
poll() |
---|---|---|
Ввод доступен |
|
|
Вывод возможен |
|
|
Установлено соединение на ввод в сокете ожидания |
|
|
Приняты данные, выходящие за полосу пропускания (только для TCP) |
|
|
Закрытое соединение однорангового потокового сокета или
исполнение |
|
|
Особенный для Linux флаг POLLHUP
(доступный начиная с Linux 2.6.17) требует некоторого дополнительного пояснения. Это флаг - в действительности
в виде EPOLLRDHUP
- спроектирован
в первую очередь для применения с режимом переключения фронтом для API epoll
Раздел 63.4. Он возвращается когда удалённая сторона
соединения некоторого потокового сокета была остановлена для записывающей половины данного соединения. Применение
данного флага позволяет некоторому использующему управляемый фронтом интерфейс epoll
приложению применять более простой код для определения некоторого удалённого выключения. (Альтернатива состоит в том,
что данное приложение помечает установку флага POLLIN
,
а затем выполняет некую операцию read()
, которая указывает на отключение этой
удалённой стороны возвратом 0
.)
В данном разделе мы рассмотрим некоторые сходства и различия между
select()
и poll()
.
Подробности реализации
Внутри ядра Linux и select()
, и poll()
оба применяют один и тот же набор внутренних процедур ядра для опроса
(poll). Эти процедуры poll
отличаются от самого системного вызова poll()
. Каждая процедура возвращает
информацию о готовности некоторого отдельного файлового дескриптора. Данная информация о готовности принимает
определённый вид битовой маски, чьи значения соотносятся с теми битвами, которые возвращаются в поле
revents
конкретного запроса poll()
(Таблице 63.2). Реализация определённого системного
вызова poll()
включает в себя вызываемые процедуры ядра
poll
для каждого файлового дескриптора и помещение результирующей
информации в соответствующее поле revents
.
Чтобы реализовать select
, применяется некий набор макросов, который
применяется для преобразования информации, возвращаемой имеющимися в ядре процедурами
poll
в те типы событий, которые возвращаются
select()
:
#define POLLIN_SET (POLLRDNORM | POLLRDBAND | POLLIN | POLLHUP | POLLERR)
/* Готовы для чтения */
#define POLLOUT_SET (POLLWRBAND | POLLWRNORM | POLLOUT | POLLERR)
/* Готовы для записи */
#define POLLEX_SET (POLLPRI) /* Исключительное условие */
Это определение макроса раскрывает семантическое соответствие между информацией, возвращаемой
select
и poll
. (Если мы взглянем
на колонки select
и poll
в Таблицах Раздела 63.2.3, мы увидим, что те индикаторы,
которые предоставляются каждым из системных вызовов находятся в соответствии с приведённым выше макросом.)
Единственная дополнительная информация, которой мы завершить всю картину сотоит в том, что
poll()
возвращает в поле revents
POLLNVAL
если один из отслеживаемых
файловых дескрипторов был закрыт во время данного вызова, в то время как
select()
вернёт -1
с errno
установленным в значение
EBADF
.
Различия API
Ниже приводятся некоторые отличия между API select()
и
poll()
:
-
Применение типа данных
fd_set
вносит некий ограничивающие сверху предел (FD_SETSIZE
) на тот диапазон файловых дескрипторов, которые могут отслеживатьсяselect()
. По умолчанию этим пределом в Linux является1024
, а его изменение требует повторной компиляции самого приложения. напротив,poll()
не размещает никаких внутренних ограничений на тот диапазон файловых дескрипторов, который может отслеживаться. -
Поскольку имеющиеся в
select()
аргументыfd_set
являются определяемыми значением, мы обязаны повторно инициализировать их если выполняем повторяющиеся вызовыselect()
внутри некоторого цикла. Примсеняя раздельные поляevents
(на вход) иrevents
(на выход),poll()
избегает данного требования. -
Допустимая точность
timeout
дляselect()
(микросекунды) больше чем допускаемаяpoll()
точность (миллисекунды). (Имеющаяся точность для таймаутов обоих системных вызовов, тем не менее, ограничена гранулярностью программно определяемых часов системы.) -
Если один из опрашиваемых файловых дескрипторов был закрыт, тогда
poll()
информирует нас в точности, какой именно с помощью битаPOLLNVAL
в соответаствующем полеrevents
. Напротив,select()
единственно возвращает-1
с установкойerrno
вEBADF
, оставляя на нас определение того, какой файловый дескриптор был закрыт путём проверки некоторой ошибки при выполнении какого- то системного вызова ввода/ вывода к данному дескриптору. Однако, это обычно не имеет какого либо значения, так как некое приложение может обычно отслеживать какие файловые дескрипторы она имеет закрытыми.
Переносимость
Исторически select()
был более широко представлен чем
poll()
. В наши дни оба интерфейса стандартизированы SUSv3 и широко
доступны в современных реализациях. Однако, имеются некие вариации поведения poll()
в различных реализациях, как это отмечено в Разделе 63.2.3.
Производительность
Производительность select()
и
poll()
аналогична если выполняется одно из следующиех условий:
-
Имеющийся диапазон подлежащих отслеживанию файловых дескрипторов небольшой (т.е. максимальный номер файлового дескриптора имеет низкое значение).
-
Подлежит отслеживанию большое число файловых дескрипторов, однако они плотно упакованы (т.е. отслеживаются все или почти все файловые дескрипторы от 0 до некоторого подлежащего мониторингу предела).
Однако, производительность select()
и
poll()
может заметно разниться если набор подлежащих отслеживанию
файловых дескрипторов разрежён; а именно, допустим, максимальный номер файлового дескриптора,
N
, является большим, однако отслеживается только один или несколько
файловых дескрипторов в данном диапазоне от 0
до
N
. В таком случае poll()
может
исполняться лучше чем select()
. Мы можем понять причину этого,
рассмотрев все аргументы, передаваемые этим двум системным вызовам. Для
select()
мы передаём один или более наборов файловых дескрипторов и
некое целое, nfds
, которое на единицу больше чем максимальное значение
подлежащего изучению набору файловых дескрипторов для каждого из наборов. Этот аргумент
nfds
имеет одно и то же значение вне зависимости от того будут ли
отслеживаться все файловые дескрипторы в определённом диапазоне (nfds -
1
) или только дескриптор (nfds - 1
). В обоих случаях
имеющееся ядро обязано изучить nfds
элементов в каждом из установленных
наборов чтобы в точности проверить какие из файловых дескрипторов подлежат отслеживанию. Напротив, применяя
poll()
мы определяем только те файловые дескриптор, которые представляют
для нас интерес, а наше ядро проверяет только эти дескрипторы.
Замечание | |
---|---|
Имеющаяся разница в производительности для |
Мы дополнительно рассмотрим значения производительности select()
и
poll()
в Разделе
63.4.5, в котором мы будем сравнивать производительность этих системных вызовов с
epoll
.
Системные вызовы select()
и
poll()
являются переносимыми, давно устоявшимися и широко применяемыми
методами отслеживания множества файловых дескрипторов на предмет их готовности. Однако, эти API испытывают
некоторые проблемы при отслеживании большого числа файловых дескрипторов:
-
При каждом вызове
select()
иpoll()
, имеющееся ядро должно проверять все определённые файловые дескрипторы, чтобы увидеть их готовность. При отслеживании некоторого большого числа файловых дескрипторов, которые находятся в некотором компактном диапазоне, то время, которое необходимо для данной операции значительно превосходит время, требующееся для следующих двух операций. -
Для каждого вызова
select()
иpoll()
, данная программа должна передавать некую структуру данных в само ядро, которая описывает все подлежащие отслеживанию файловые дескрипторы и, после проверки этих файловых дескрипторов, это ядро возвращает изменённую версию этой структуры данных в такую программу. (Более того, дляselect()
должны быть инициализированы все структуры данных перед каждым вызовом). Дляpoll()
имеющийся размер этой структуры данных возрастает с общим числом подлежащих отслеживанию файловых дескрипторов, и сама задача их копирования из пространства пользователя в пространство ядра, а также обратно, потребляет значительное время ЦПУ при отслеживании многих файловых дескрипторов. Дляselect()
имеющийся размер такой структуры данных фиксируетсяFD_SETSIZE
вне зависимости от общего числа подлежащих отслеживанию файловых дескрипторов. -
После определённого вызова
select()
илиpoll()
, данная программа обязана проверить все возвращаемые структуры данных чтобы определить какие файловые дескрипторы готовы.
Последствием всех указанных выше моментов является то, что то время ЦПУ, которое требуется
select()
и poll()
,
растёт по мере роста подлежащих отслеживанию файловых дескрипторов (см. Раздел 63.4.5 с дополнительными подробностями). Это создаёт проблемы для программ, которые
отслеживают большие числа файловых дескрипторов.
Плохая масштабируемость производительности select()
и
poll()
проистекает из простого ограничения этих API: обычно некоторая
программа делает повторяющиеся вызовы для отслеживания одного и того же набора файловых дескрипторов; однако
само ядро не запоминает весь список подлежащих отслеживанию файловых дескрипторов между последовательными
вызовами.
Управляемый сигналом ввод/ вывод и epoll
, которые мы исследуем в
последующих разделах, оба являются именно теми механизмами, которые позволят самому ядру записывать некий
постоянно присутствующий список файловых дескрипторов, в которых заинтересован какой- то процесс. Осуществление
этого исключает данную проблему масштабирования производительности select()
и poll()
, предоставляя решения, которые масштабируются в соответствии с
общим числом происходящих событий ввода/ вывода, аместо того чтобы соответствоввать общему числу подележащих
мониторингу файловых дескрипторов. Вследствие этого, управляемый сигналом ввод/ вывод и
epoll
предоставляют превосходную производительность при отслеживании
больших чисел файловых дескрипторов.
При вводе/ выводе с мультиплексированием некий процесс делает какой- то системный вызов (select()
или poll()
) чтобы проверить возможен ли ввод/ вывод для некоторого файлового
дескриптора. Для управляемого сигналом ввода/ вывода некий процесс запрашивает чтобы само ядро отправляло ему
какой- то сигнал когда ввод/ вывод возможен для какого- нибудь файлового дескриптора. Данный процесс может затем
выполнять любые прочие действия пока не станет возможен ввод/ вывод, после чего определённый сигнал доставляется
данному процессу. Для выполнения движимого сигналом ввода/ вывода некая программа формирует следующие этапы:
-
Устанавливает некий обработчик для того сигнала, который доставляется управляемым сигналом механизмом ввода/ вывода. По умолчанию таким сигналом уведомления является
SIGIO
. -
Устанавливает владельца данного файловогодескриптора - именно этот процесс или группа процессов принимают сигналы когда для данного файлового дескриптора возможен ввод/ вывод. Обычно мы делаем владельцем вызывающий процесс. Такой владелец устанавливается при помощи операции
F_SETOWN
некоторогоfcntl()
в следующем виде:fcntl(fd, F_SETOWN, pid);
-
Включает неблокируемый ввод/ вывод посредством установки флага состояния открытого файла
O_NONBLOCK
. -
Разрешает управляемый сигналом ввод/ вывод путём включения флага состояния открытого файла
O_ASYNC
. Это может быть объединено с предыдущим шагом, так как оба они требуют операцииF_SETOWN
определённогоfcntl()
(Раздел 5.3), как в приведённом ниже примере:flags = fcntl(fd, F_GETFL); /* Получить текущие флаги */ fcntl(fd, F_SETFL, flags | O_ASYNC | O_NONBLOCK);
-
Вызывающий процесс теперь может выполнять прочие задачи. Когда станет возможным ввод/ вывод, само ядро сгенерирует некий сигнал для данного процесса и исполнится тот обработчик сигнала, который был установлен на шаге 1.
-
Управляемый сигналом ввод/ вывод предоставляет управляемое фронтом уведомление (Раздел 63.1.1). Это означает, что как только данный процесс получил уведомление о возможности ввода/ вывода, он должен выполнить такой объём операций ввода/ вывода, сколько возможно (например, считать столько байт, сколько сможет). В предположении некоторого неблокируемого файлового дескриптора это означает исполнение какого- то цикла, который исполняет системные вызовы ввода/ вывода пока не получит отказ вызова с конкретной ошибкой
EAGAIN
илиEWOULDBLOCK
.
В Linux 2.4 или более ранних версиях управляемый сигналом ввод/ вывод мог применяться с файловыми дескрипторами
для сокетов, терминалов, псевдотерминалов и определённых типов устройств. Linux 2.6 дополнительно допускает
управляемый сигналом ввод/ вывод применительно к конвейерам и FIFO. Начиная с Linux 2.6.25, движимый сигналом
ввод/ вывод может также применяться для файловых дескрипторов inotify
.
На последующих страницах мы вначале представим некий пример использования управляемого сигналом ввода/ вывода, а затем объясним более подробно некоторые приведённые выше этапы.
Замечание | |
---|---|
Исторически управляемый сигналом ввод/ вывод иногда называется асинхронным
вводом/ выводом, и это отражается в самом имени флага состояния открытого файла
(
Некоторые реализации UNIX, в особенности более ранние, не определяют данную константу
|
Пример программы
Листинг 63.3 предоставляет некий простой пример
использования управляемого сигналом ввода/ вывода. Данная программа исполняет все описанные выше шаги для
разрешения движимого сигналом ввода/ вывода в стандартном вводе, а затем помещает данный терминал в режим cbreake
(Раздел 62.6.3) с тем, чтобы ввод был доступен по одному
символу за раз. Данная программа затем входит в бесконечный цикл, выполняет "работу" по инкрементированию
некоторой переменной, cnt
и в то же время ожидая доступности ввода.
Всякий раз как становится доступным ввод, имеющийся обработчик
SIGIO
устанавливает некий флаг,
gotSigio
, и именно он отслеживается имеющейся основной программой.
Когда основная программа видит этот флаг установленным, она считывает все доступные на входе символы и печатает
их вместе с имеющимся текущим значением cnt
. Если в данном
вводе считывается символ решётки (#
), данная программа
прекращается.
Вот прим ер того, что мы видим после исполнения данной программы и нажатия символа
x
некоторое число раз с последующим вводом символа решётки
(#
):
$ ./demo_sigio
cnt=37; read x
cnt=100; read x
cnt=159; read x
cnt=223; read x
cnt=288; read x
cnt=333; read #
Листинг 63.3: Применение управляемого сигналом ввода/ вывода
---------------------------------------------------------------------- altio/demo_sigio.c
#include <signal.h>
#include <ctype.h>
#include <fcntl.h>
#include <termios.h>
#include "tty_functions.h" /* Объявление ttySetCbreak() */
#include "tlpi_hdr.h"
static volatile sig_atomic_t gotSigio = 0;
/* Установка ненулевого значения на приём SIGIO */
static void
sigioHandler(int sig)
{
gotSigio = 1;
}
int
main(int argc, char *argv[])
{
int flags, j, cnt;
struct termios origTermios;
char ch;
struct sigaction sa;
Boolean done;
/* Устанавливаем обработчик для сигнала "I/O possible" */
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
sa.sa_handler = sigioHandler;
if (sigaction(SIGIO, &sa, NULL) == -1)
errExit("sigaction");
/* Устанавливаем процесс владельца, который должен принять сигнал "I/O possible" */
if (fcntl(STDIN_FILENO, F_SETOWN, getpid()) == -1)
errExit("fcntl(F_SETOWN)");
/* Включаем сигнализацию "I/O possible" и выполняем неблокируемый ввод/вывод
для файлового дескриптора */
flags = fcntl(STDIN_FILENO, F_GETFL);
if (fcntl(STDIN_FILENO, F_SETFL, flags | O_ASYNC | O_NONBLOCK) == -1)
errExit("fcntl(F_SETFL)");
/* Помещаем терминал в режим cbreak */
if (ttySetCbreak(STDIN_FILENO, &origTermios) == -1)
errExit("ttySetCbreak");
for (done = FALSE, cnt = 0; !done ; cnt++) {
for (j = 0; j < 100000000; j++)
continue; /* Слегка придушиваем медленный главный цикл */
if (gotSigio) { /* Доступен ли ввод? */
/* Прочитать весь доступный ввод вплоть до ошибки (скорее всего EAGAIN)
или EOF (в действительности не возможен в режиме cbreak) или считывания
некоторого символа решётки (#) */
while (read(STDIN_FILENO, &ch, 1) > 0 && !done) {
printf("cnt=%d; read %c\n", cnt, ch);
done = ch == '#';
}
gotSigio = 0;
}
}
/* Восстановление первоначальных установок терминала */
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &origTermios) == -1)
errExit("tcsetattr");
exit(EXIT_SUCCESS);
}
---------------------------------------------------------------------- altio/demo_sigio.c
Установка определённого обработчика сигнала перед включением управляемого сигналом ввода/ вывода
Поскольку определённым по умолчанию действием SIGIO
является завершение самого процесса, нам следует включить определённый обработчик для
SIGIO
прежде чем разрешить в некотором
файловом дескрипторе управляемый сигналом ввод/ вывод. Если мы разрешим управляемый сигналом ввод/ вывод до того
как установим конкретный обработчик SIGIO
,
тогда появится некое временное окно, на протяжении которого, если станет возможным ввод/ вывод, доставка
SIGIO
прекратит данный процесс.
Замечание | |
---|---|
В некоторых реализациях UNIX по умолчанию
|
Настройка владельца конкретного файлового дескриптора
Мы устанавливаем владельца данного файлового дескриптора применяя некую операцию
fcntl()
в следующем виде:
fcntl(fd, F_SETOWN, pid);
Мы можем определить будет ли доставляться сигнал некому отдельному процессу или всем процессам в какой- то
группе процессов когда возможен ввод/ вывод для данного файлового дескриптора. Если
pid
положителен, он воспринимается как идентификатор некого процесса.
Если pid
отрицателен, его абсолютное значение определяет идентификатор
некоторой группы процессов.
Замечание | |
---|---|
В более ранних реализациях UNIX для достижения того же самого эффекта, что и при
|
Обычно pid
определяется как идентификатор того процесса, который
выполнил данный вызов (тем самым соответствующий сигнал отправляется тому процессу, который имеет открытым
данный файловый дескриптор). Однако, имеется возможность определить другой процесс или некую группу процессов
(например, соответствующую группу вызывающих процессов) и сигналы будут отправляться такому получателю с учётом
тех проверок разрешений, которые описаны в Разделе 20.5,
причём отправляющим процессом считается тот процесс, который выполняет
F_SETOWN
.
Операция F_GETOWN
fcntl()
возвращает определённый идентификатор того процессора или
группы процессов, которые должны принимать сигналы когда в некотором определённом файловом дескрипторе
возможен ввод/ вывод:
id = fcntl(fd, F_GETOWN);
if (id == -1)
errExit("fcntl");
Данным вызовом идентификатор некоторой группы процессов возвращается как какое- то отрицательное значение.
Замечание | |
---|---|
Той операцией |
Некое ограничение о системном вызове, применяемом в некоторых архитектурах Linux (в частности, x86)
означает, что если владельцем некоторого файлового дескриптора идентификатор какоой- то группы процессов со
значением менее 4096, тогда, вместо возврата этого идентификатора как отрицательного значения функции из операции
F_GETOWN
fcntl()
, glibc
интерпретирует его как некую ошибку системного вызова. По этой причине функция обёртки
fcntl()
возвращает -1
и
errno
содержит определённый (положительный) идентификатор группы
процессов. Это является следствием того факта, что интерфейс системного вызова самого ядра указывает ошибки,
возвращая в виде результата функции некое отрицательное значение errno
,
причём имеется ряд случаев, когда необходимо отличать такие результаты от успешного вызова, который возвращает
верное отрицательное значение. Для выполнения такого различия glibc
интерпретирует возвращаемые системным вызовом значения в диапазоне от -1
до -4095
как указывающие некую ошибку, копируя это (абсолютное) значение в
errno
и возвращая -1
как
результат данной функции для своей прикладной программы. Такого механизма обычно достаточно для работы с
некоторыми процедурами служб системных вызовов, которые могут возвращать некий правильный отрицательный результат;
сама операция F_GETOWN
fcntl()
является единственным практическим случаем где это не работает.
Такое ограничение означает, что некое приложение, которое применяет группы процессов для получения сигналов
"I/O possible" (что является необычным) не может надёжно применять
F_GETOWN
для изучения того, какая
группа процессов владеет неким файловым дескриптором.
Замечание | |
---|---|
Начиная с версии 2.11 |
Теперь мы более подробно рассмотрим когда выставляется сигнал "I/O possible" для различных типов файлов.
Терминалы и псевдотерминалы
Для терминалов и псевдотерминалов некий сигнал вырабатывается всякий раз, когда становится возможным новый ввод, даже если предыдущий ввод ещё не был считан. "Input possible" также сигнализирует если возникло состояние конца-файла (eof) в каком- то терминале (но не в псевдотерминале).
Для теминалов отсутствует сигнализация "Output possible". Отсоединение терминала также не выставляет сигнал.
Начиная с ядра 2.4.19, Linux предоставляет сигнализацию "Output possible" для имеющейся подчинённой стороны некоторого псевдотерминала. Этот сигнал вырабатывается всякий раз, когда ввод потребляется со стороны хозяина данного псевдотерминала.
Конвейеры и FIFO
Для считывающей стороны конвейера или FIFO некий сигнал вырабатывается при следующих обстоятельствах:
-
В конкретный конвейер записаны данные (даже если уже имелся доступный несчитанный ввод).
-
Сторона записи закрыта.
Для записывающей стороны конвейера или FIFO некий сигнал вырабатывается при следующих обстоятельствах:
-
Некое считывание из данного конвейера увеличило общий размер свободного пространства в данном конвейере, поэтому теперь имеется возможность записать
PIPE_BUF
байт без блокирования. -
Сторона считывания закрыта.
Сокеты
Управляемый сигналом ввод/ вывод работает для сокетов дейтаграмм и в доменах UNIX, и в доменах Интернет. Некий сигнал вырабатывается при следующих обстоятельствах:
-
Появилась некая входная дейтаграмма в данном сокете (даже если уже имеются несчитанные дейтаграммы, ожидающие прочтения).
-
В данном сокете возникла некая асинхронная ошибка.
Управляемый сигналом ввод/ вывод работает для потоковых сокетов и в доменах UNIX, и в доменах Интернет. Некий сигнал вырабатывается при следующих обстоятельствах:
-
В некотором прослушивающем сокете принято какое- то новое соединение.
-
Выполнен запрос
connect()
TCP; то есть имеющаяся активная сторона какого- то соединения TCP вошла в состояниеESTABLISHED
, как это показано на Рисунке 61.5. Аналогичное условие для сокетов домена UNIX не выствляет сигнал. -
В данном сокете принят новый ввод (даже если уже доступен несчитанный ввод).
-
Одноранговое соединение закрывает свою половину записи данного соединения при помощи
hutdown()
или закрывает все его сокеты применяяclose()
. -
В данном сокете возможен вывод (например, пространство стало доступным в буфере отправки данного сокета).
-
В данном сокете возникла некая асинхронная ошибка.
Файловые дескрипторы inotify
Когда определённый файловый дескриптор inotify
становится
читаемым, вырабатывается некий сигнал - именно в этом случае происходит какое- то событие для одного из тех
файлов, которые отслеживаются данным файловым дескриптором
inotify
.
В приложении, которое должно одновременно отслеживать очень большие числа (т.е. тысячи) файловых дескрипторов -
например, определённые типы сетевых серверов - управляемый сигналом ввод/ вывод может предоставить значительные
преимущества производительности в сравнении с select()
и
poll()
. Управляемый сигналом ввод/ вывод предлагает превосходную
производительность по той причине, что само ядро "помнит" весь перечень подлежащих отслеживанию
файловых дескрипторов и сигнализирует конкретной программе только когда в действительности происходят события
ввода/ вывода для данных дескрипторов. Как результат, общая производительность некоторой программы, применяющей
управляемый сигналом ввод/ вывод масштабируется в соответствии с тем числом событий ввода/ вывода, которое
происходит, вместо того чтобы отслеживать всё количество файловых дескрипторов.
Для получения всех преимуществ управляемого сигналом ввода/ вывода мы должны выполнить два шага:
-
Применить специфичную для Linux операцию
fcntl()
,F_SETSIG
, чтобы определить некий сигнал реального времени, который должен доставляться вместо сигналаSIGIO
при возможности ввода/ вывода в некотором файловом дескрипторе. -
Определить флаг
SA_SIGINFO
при использованииsigaction()
для установки применения данного сигнала времени исполнения, применяемого в предыдущем шаге (см. Раздел 21.4).
Операция F_SETSIG
fcntl()
определяет некий альтернативный сигнал, который должен
доставляться вместо SIGIO
при
возможности ввода/ вывода в некотором файловом дескрипторе:
if (fcntl(fd, F_SETSIG, sig) == -1)
errExit("fcntl");
Операция F_GETSIG
выполняет
общение с F_SETSIG
, выбирая все
установленные в настоящее время сигналы для некоторого файлового дескриптора:
sig = fcntl(fd, F_GETSIG);
if (sig == -1)
errExit("fcntl");
(Чтобы получить само определение данных констант F_SETSIG
и F_GETSIG
из
<fcntl.h>
, мы должны определить макрос проверки свойства
_GNU_SOURCE
.)
Применение F_SETSIG
для изменения
самого сигнала используемого для уведомления "I/O possible" обслуживается двумя способами, причём оба
требуются если мы отслеживаем большие числа событий ввода/ вывода на множестве файловых дескрипторов:
-
Становленный по умолчанию сигнал "I/O possible",
SIGIO
является одним из применяемых стандартных сигналов без очередей. Если выставляется множество сигналов покаSIGIO
блокирован - возможно, потому что данный обработчикSIGIO
уже исполняется - будут утрачены все уведомления кроме самого первого. Если мы применяем для определения в качестве сигнала реального времени "I/O possible"F_SETSIG
, множество уведомлений может быть выстроено в очередь. -
Если обработчик для данного сигнала установлен с применением вызова
sigaction()
в котором флагSA_SIGINFO
определён имеющемся полеsa.sa_flags
, тогда в данный обработчик сигнала будет передана некая структураsiginfo_t
в качестве второго параметра (Раздел 21.4). Эта структура содержит поля, указывающие те файловые дескрипторы, в которых произошло данное событие, а также сам тип события.
Отметим, что необходимо применение обоих
F_SETSIG
и
SA_SIGINFO
, чтобы была передана
правильная структура siginfo_t
в данный обработчик сигнала.
Если мы выполним некую операцию F_SETSIG
,
определив sig
равным 0
, тогда
мы вернёмся к определённому по умолчанию поведению: доставляется
SIGIO
и аргумент
siginfo_t
не поставляется в данный обработчик.
Для некоторого события "I/O possible" теми представляющими интерес полями в структуре
siginfo_t
, которые передаются самому обработчику сигнала являются:
-
si_signo
: общее число тех сигналов, которое вызвало исполнение данного обработчика. Это значение то же самое, что и в самом первом аргументе данного обработчика сигнала. -
si_fd
: значение файлового дескриптора для которого произошло данное событие ввода/ вывода. -
si_code
: некий код, указывающий значение типа произошедшего события. Те значения, которые могут появляться в данном поле, а также их общие описания отражены в Таблице 63.7. -
si_band
: некая маска бит, содержащая те же самые биты, что и возвращаемые в полеrevents
описанного ранее системного вызоваpoll()
. Набор значений вsi_code
имеет соответствие один- в- один с соответсвтующими установками маски битsi_band
, как это показано в Таблице 63.7.
si_code |
Значение маскиsi_band |
Описание |
---|---|---|
|
|
Ввод возможен; условие конца-файла (eof) |
|
|
Вывод возможен |
|
|
Доступно сообщение на входе (неиспользуемое) |
|
|
Ошибка ввода/ вывода |
|
|
Доступен ввод с высоким приоритетом |
|
|
Произошло отсоединение |
В некотором приложении, котрое полностью управляется вводом, мы можем в последующем улучшить использование
F_SETSIG
. Вместо отслеживания событий
ввода/ вывода через некий обработчик сигнала, мы можем блокировать определённый назначенный сигнал
"I/O possible", а затем принимать все выстроенные в очередь сигналы через вызов
sigwaitinfo()
или sigtimedwait()
(Раздел 22.10). Эти системные вызовы возвращают некую
структуру siginfo_t
, которая передаётся в какой- то установленный
SA_SIGINFO
обработчик сигнала.
Приём сигналов таким образом возвращает нас к синхронной модели обработки событий, однако с тем преимуществом,
что мы намного более эффективно получаем уведомления о тех файловых дескрипторах, в которых произошли какие- либо
сорбытия ввода/ вывода, чем если бы мы применяли select()
или
poll()
.
Обработка переполнения очереди сигнала
В Разделе 22.08 мы видели, что имеется некий предел
на общее число сигналов времени исполнения, которые могут быть поставлены в очередь. Если достигается данный
предел, имеющееся ядро возвращается к доставке определённого по умолчанию сигнала
SIGIO
для уведомления
"I/O possible". Это информирует данный процесс что произошло переполнение очереди сигнала. Когда это
случилось, мы теряем информацию о том, какие файловые дескрипторы имеют события ввода/ вывода, посекольку
SIGIO
не имеет очереди.
(Более того, такой обработчик SIGIO
не принимает аргумент siginfo_t
, что означает, что данный обработчик
сигнала не может определить тот файловый дескриптор, который выработал данный сигнал.)
Мы можем снизить значение вероятности переполнения очереди сигнала увеличив имеющийся предел общего числа
сигналов времени исполнения, которые могут быть поставлены в очередь, как это описано в Разделе 22.08. Однако, это не исключит необходимость обрабатывать саму
вероятность некоторого переполнения. Разработанное надлежащим образом приложение применяет
F_SETSIG
для установки некоторого
сигнала времени исполнения в качестве имеющегося механизма уведомления об "I/O possible" должно также
установить некий обработчик для SIGIO
.
Если доставлен SIGIO
, тогда данное
приложение должно осушить имеющуюся очередь сигналов времени исполнения с помощью
sigwaitinfo()
и временно вернуться к использованию
select()
или poll()
для
получения некоторого полного списка файловых дескрипторов с невыполненными событиями ввода/ вывода.
Применение управляемого сигналом ввода/ вывода в многопоточных приложениях
Начиная с ядра 2.6.32 Linux предоставляет две новые нестандартные операции fcntl()
,
которые могут применяться для установки получателя сигналов "I/O possible":
F_SETOWN_EX
и
F_GETOWN_EX
.
Данная операция F_SETOWN_EX
похожа на F_SETOWN
, однако поскольку
также позволяет определять получателя как некий процесс или группу процессов, она также некоторому потоку быть
определённым как конкретная цель для сигналов "I/O possible". Для данной операции имеющийся третий
аргумент fcntl()
является неким указателем на структуру следующего
вида:
struct f_owner_ex {
int type;
pid_t pid;
};
Данное поле type
определяет значение самого поля
pid
и принимает имеет одно из следующих значений:
F_OWNER_PGRP
Поле
pid
определяет значение идентификатора некоторой группы процессов, которые должны быть конкретной целью сигналов "I/O possible". В отличии от варианта сF_SETOWN
, идентификатор некоторой группы процессов определяется как какое- то положительное значение.
F_OWNER_PID
Поле
pid
определяет значение идентификатора процесса, который должны быть конкретной целью сигналов "I/O possible".
F_OWNER_TID
Поле
pid
определяет значение идентификатора потока, который должны быть конкретной целью сигналов "I/O possible". Тот идентификатор, который определён вpid
является неким значением, возвращаемымclone()
илиgettid()
.
Имеющаяся операция F_GETOWN_EX
должна общаться с определённой операцией F_SETOWN_EX
.
Она применяет соответствующую структуру f_owner_ex
, указанную третьим
аргументом fcntl()
для возвращения тех установок, которые определяются
какой- то определённой выше операцией
F_SETOWN_EX
.
Замечание | |
---|---|
Поскольку данные операции |
Как и системные вызовы ввода/ вывода с мультиплексированием, и управляемый сигналами ввод/ вывод,
API epoll
Linux (опрос событий - event poll) применяется для
отслеживания множества файловых дескрипторов чтобы наблюдать за их готовностью к операциям ввода/ вывода.
Первоочередными преимуществами API epoll
являются
следующие:
- Производительность
epoll
масштабируется намного лучше чем в случаеselect()
иpoll()
при отслеживании большого числа файловых дескрипторов. - API
epoll
делает возможной оповещение по переключению уровнем или фронтом. В противоположность этомуselect()
иpoll()
предоставляют только уведомления по переключению уровнем, а управляемый сигналами ввод/ вывод предоставляет только переключаемое фронтом оповещение.
Производительность epoll
и управляемого сигналами ввода/ вывода
аналогична. Однако, epoll
имеет некоторые преимущества при
сопоставлении с управляемым сигналами вводом/ выводом:
- Мы избегаем сложностей обработки сигналов (например, переполнения очереди сигналов).
- Мы получаем большую гибкость при определении того вида мониторинга, который мы желаем осуществлять (например, проверка того факта, что некий файловый дескриптор для сокета готов к чтению, записи, или к обеим операциям).
API epoll
специфичен именно для Linux и появился в Linux 2.6.
Центральной структурой данных всего API epoll
является
экземпляр epoll
, ссылки к которому выполняются через некий
открытый файловый дескриптор. Данный файловый дескриптор не применяется для ввода/ вывода. Вместо этого имеется
некий обработчик для структур данных ядра, который обслуживает две цели:
- выполняет записи в некий перечень дескрипторов, которые данный процесс объявил как
представляющие некий интерес для мониторинга - определённый
перечень участия
(interest list); а также - сопровождает некий список файловых дескрипторов, которые готовы к выполнению операций
ввода/ вывода - определённый
перечень готовности
(ready list).
Все участники перечня готовности являются подмножеством общего списка интереса.
Для каждого отслеживаемого epoll
файлового дескриптора мы можем
определить битовую маску, указывающую события, в которых мы заинтересованы. Эти битовые маски тесно связаны с теми
битовыми масками, которые применяются в poll()
.
Весь API epoll
состоит из трёх системных вызовов:
- Системный вызов
epoll_create()
создаёт некий экземпляр иepoll
и возвращает ссылающийся на данный экземпляр файловый дескриптор. -
Системный вызов
epoll_ctl()
манипулирует связанным с определённым перечнем интереса некоторогоepoll
. Применяяepoll_ctl()
мы можем добавить некий новый файловый дескриптор в этот список, удалить какой- то существующий дескриптор из данного списка, а также изменить ту маску, которая определяет какие события отслеживаются для некого дескриптора. -
Системный вызов
epoll_wait()
возвращает элементы из имеющегося перечня готовности, связанного с данным экземпляромepoll
.
Системный вызов epoll_create()
создаёт некий новый экземпляр
epoll
, причём его список интереса первоначально пустой.
#include <sys/epoll.h>
int epoll_create(int size);
Возвращает в случае успеха файловый дескриптор, либо -1 при ошибке
Аргумент size
определяет общее число файловых дескрипторов, которое
мы ожидаем наблюдать через данный экземпляр epoll
. Данный аргумент
не является неким верхним пределом, а всего лишь некоторой подсказкой для вашего ядра о том, какова начальная
размерность внутренних структур данных. (Начиная с Linux 2.6.8 данный аргумент
size
игнорируется, так как изменения в данной реализации подразумевают,
что подобная информация более не требуется.)
В качестве результата своей функции Code
возвращает некий файловый
дескриптор, ссылающийся на такой новый экземпляр epoll
. Такой
файловый дескриптор применяется для ссылки на данный экземпляр epoll
при прочих системных вызовах epoll
. Когда данный файловый дескриптор более
не требуется, он должен быть закрыт своим обычным способом, с применением close()
.
Когда все файловые дескрипторы, ссылающиеся на некий экземпляр epoll
являются закрытыми, данный экземпляр уничтожается и все связанные с ним ресурсы возвращаются обратно в имеющуюся
систему. (Множество файловых дескрипторов могут ссылаться на один и тот же экземпляр
epoll
, как некий результат вызовов fork()
или дублирования дескриптора с применением dup()
, или аналогичных
действий.)
Замечание | |
---|---|
Начиная с ядра 2.6.27, Linux поддерживает некий новый системный вызов,
|
Системный вызов epoll_create()
изменяет тот перечень интереса,
определённого экземпляра epoll
, на который выполняется ссылка
имеющегося файлового дескриптора epfd
.
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *ev);
Возвращает 0 в случае успеха, либо -1 при ошибке
Аргумент fd
указывает какой из файловых дескрипторов в данном списке
интереса должен изменить свои установки. Этот аргумент может быть неким файловым дескриптором для какого- то
конвейера, FIFO, сокета, очереди сообщений POSIX, экземпляра inotify
,
терминала, устройства, или даже другого дескриптора epoll
(т.е. мы можем
построить некий вид иерархии отслеживаемых дескрипторов). Однако, fd
не может быть неким файловым дескриптором для какого- то обычного файла или каталога (результатом будет
ошибка EPERM
).
Аргумент op
определяет саму подлежащую исполнению операцию и имеет одно
из следующих значений:
EPOLL_CTL_ADD
Добавляет данный файловый дескриптор
fd
в имеющийся список интереса дляepfd
. тот набор событий, которым мы интересуемся при отслеживании дляfd
определяется в самом буфере, на который указываетev
, как это описано далее. Если мы пытаемся добавить некий файловый дескриптор, который уже имеется в данном списке интереса, мы получим отказepoll_ctl()
с возвращаемой ошибкойEEXIST
.
EPOLL_CTL_MOD
Изменяет данный файловый дескриптор
fd
, применяя ту информацию, которая определена в буфере, на который указываетev
. Если мы пытаемся изменить установки некоторого файлового дескриптора, который отсутствует в данном списке интереса дляepfd
, мы получим отказepoll_ctl()
с возвращаемой ошибкойENOENT
.
EPOLL_CTL_DEL
Удаляет данный файловый дескриптор
fd
из имеющегося дляepfd
списка интереса. Если мы пытаемся удалить некий файловый дескриптор, который отсутствует в списке интереса дляepfd
, мы получим отказepoll_ctl()
с возвращаемой ошибкойENOENT
. Закрытие некоторого файлового дескриптора автоматически удаляет его из всех перечней интереса тогоepoll
, чьим участником он является.
EPOLL_CTL_DISABLE
{Прим. пер.: Добавлено в Linux 3.7 для безопасного отключения наблюдения за файловым дескриптором в многопоточных приложениях, подробнее см. Майкл Керриск, 17 октября, 2012.}
Аргумент ev
является неким указателем на какую- то структуру с типом
epoll_event
, определённую следующим образом:
struct epoll_event {
uint32_t events; /* события epoll (битовая маска) */
epoll_data_t data; /* Данные пользователя */
};
Поле data
в этой структуре epoll_event
определяется типом следующего вида:
typedef union epoll_data {
void *ptr; /* Указатель на определённые пользователем данные */
int fd; /* Файловый дескриптор */
uint32_t u32; /* 32-bit integer */
uint64_t u64; /* 64-bit integer */
} epoll_data_t;
Аргумент ev
определяет установки для определённого файлового
дескриптора fd
следующим образом:
-
Вложенное поле
events
является битовой маской, определяющей тот набор событий, в котором мы заинтересованы при отслеживанииfd
. Мы рассмотрим дополнительно все битовые значения которые могут применяться в этом поле в своём следующем разделе. -
Вложенное поле
data
является неким объединением (union), один из участников которого может быть использован для определения информации, которая возвращается обратно самому вызывавшему процессу (черезepoll_wait()
) еслиfd
позже становится готовым.
Листинг 63.4 отображает определённый пример использования epoll_create()
и epoll_ctl()
.
Листинг 63.4
int epfd;
struct epoll_event ev;
epfd = epoll_create(5);
if (epfd == -1)
errExit("epoll_create");
ev.data.fd = fd;
ev.events = EPOLLIN;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, ev) == -1)
errExit("epoll_ctl");
Предел max_user_watches | |
---|---|
Так как каждый зарегистрированный в некотором списке интереса |
Системный вызов epoll_wait()
возвращает информацию о готовых
файловых дескрипторах из определённого экземпляра epoll
, на который ссылается
заданный файловый дескриптор epfd
. Отдельный вызов
epoll_wait()
может возвращать информацию о множестве готовых файловых
дескрипторов.
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *evlist, int maxevents, int timeout);
Возвращает число файловых дескрипторов, 0 в случае таймаута, либо -1 при ошибке
Вся информация о готовых файловых дескрипторах возвращается в общем массиве структуры
epoll_event
, на которую указывает evlist
(Сама структура epoll_event
была описана в предыдущем разделе.) Этот массив evlist
выделяется самой
вызывающей стороной, а общее число содержащихся в нём элементов определяется в
maxevents
.
Каждый элемент в возвращаемом массиве evlist
возвращает информацию о
некотором отдельном готовом файловом дескрипторе. Все вложенные поля events
возвращают некую маску тех событий, которые произошли в данном файловом дескрипторе. Определённое подполе
data
возвращает любое данное, определённое в
ev.data
при регистрации интереса в данном файловом дескрипторе с применением
epoll_ctl()
. Отметим, что это поле data
предоставляет единственный механизм для определения самого номера данного файлового дескриптора, связанного с данным
событием. Таким образом, когда мы выполняем вызов epoll_ctl()
, который
помещает некий файловый дескриптор в имеющемся списке интереса, мы должны либо установить
ev.data.fd
в значение номера файлового дескриптора (как это показано в
Листинге 63.4), либо установить
ev.data.ptr
указателем на некую структуру, которая содержит конкретный номер
файлового дескриптора.
Аргумент timeout
определяет поведение блокирования
epoll_wait()
следующим образом:
- Если
timeout
равен-1
, блокирование производится до тех пор, пока не произойдёт некое событие для одного из рассматриваемых файловых дескрипторов в данном списке интереса дляepfd
либо пока не был захвачен некий сигнал. - Если
timeout
равен0
, выполняется неблокируемая проверка на предмет того, какие события в настоящий момент доступны во всех файловых дескрипторах данного перечня интереса дляepfd
. - Если
timeout
больше0
, выполняется блокировка доtimeout
миллисекунд до те пор пока не произойдёт некое событие в одном из определённых в данном списке интереса дляepfd
файловых дескрипторов, либо пока не будет перехвачен некий сигнал.
В случае успешного завершения, epoll_wait()
возвращает общее
число элементов, которое было помещено в определённый массив evlist
,
или 0
если не было никаких готовых файловых дескрипторов в пределах,
отведённых timeout
. В случае возникновения ошибки
epoll_wait()
возвращает -1
с
errno
установленным указывающим на данную ошибку.
В некоторой программе со множеством потоков имеется возможность одному из потоков применять
epoll_ctl()
для добавления файловых дескрипторов в общий список интереса
некоторого экземпляра epoll
, который уже находится под присмотром
epoll_wait()
в другом потоке. Такие изменения в данном перечне интереса
будут приняты во внимание немедленно, а сам вызов epoll_wait()
вернёт
информацию о готовности о таких вновь добавленных файловых дескрипторах.
События epoll
Значения битов, которые могут быть определены в ev.events
при вызове epoll_ctl()
и которые помещаются в определённые поля
evlist[].events
, возвращаемые epoll_wait()
,
отображаются в Таблице 63.8. Помимо добавления
префикса E
, большая часть этих бит имеет
те же самые названия, что и соответствующие события, применяемые в poll()
.
(Исключение составляют EPOLLET
и
EPOLLONESHOT
, которые более подробно
описаны ниже.) Причина данного соответствия состоит в том, что при определении на входе в
epoll_ctl()
или при возврате через epoll_wait()
,
все эти биты имеют в точности те же значения, что и соответствующие биты событий
poll()
.
Бит | Входной параметр epoll_ctl() ? |
Возвращаемый epoll_wait() параметр? |
Описание |
---|---|---|---|
|
+ |
+ |
Могут считываться прочие данные помимо данных с высоким приоритетом |
|
+ |
+ |
Могут считываться данные с высоким приоритетом |
|
+ |
+ |
Останов однорангового сокета (начиная с Linux 2.6.17) |
|
+ |
+ |
Могут выполняться запись обычных данных |
|
+ |
|
Применяется переключаемое фронтом (edge-triggered) уведомление о событию |
|
+ |
|
Отключение мониторинга после получения уведомления о событии |
|
|
+ |
Произошла ошибка |
|
|
+ |
Произошло отключение |
Флаг EPOLLONESHOT
По умолчанию, при добавлении некоторого файлового дескриптора в перечень интересов
epoll
с помощью операции
EPOLL_CTL_ADD
epoll_ctl()
, он остаётся активным (т.е. последующие
вызовы epoll_wait()
будут сообщать нам всякий раз когда данный файловый
дескриптор готов) пока мы в явном виде не удалим его из этого списка при помощи операции
EPOLL_CTL_DEL
epoll_ctl()
. Если мы желаем получить только одно уведомление об некотором
определённом файловом дескрипторе, тогда мы можем определить именно такой флаг
EPOLLONESHOT
(доступный начиная с
Linux 2.6.2) в том значении ev.events
, которое передаётся в
epoll_ctl()
. Если определён данный флаг, тогда после следующего вызова
epoll_wait()
, который проинформирует нас что соответствующий файловый
дескриптор готов, данный файловый дескриптор помечается как неактивный в своём списке интереса и мы не сможем
получать информацию об его состоянии последующими вызовами epoll_wait()
.
Если есть такая потребность, мы можем впоследствии повторно разрешить отслеживание данного файлового дескриптора
с помощью операции EPOLL_CTL_MOD
epoll_ctl()
. (Для этой цели мы не можем применить операцию
EPOLL_CTL_ADD
, так как данный
неактивный файловый дескриптор всё ещё является частью имеющегося списка интереса
epoll
.)
Пример программы
Листинге 63.5 демонстрирует применение API
epoll
. В качестве аргументов командной строки данная программа ожидает
определяемые имена путей одного или более терминалов или FIFO. Эта программа осуществляет следующие этапы:
-
Создаёт некий экземпляр
epoll
(1). -
Открывает каждый из файлов, поименованных в командной строке ввода (2) и добавляет свой файловый дескриптор результата в имеющийся в заданном в данном
epoll
списке интереса (3), определяемом установленнымEPOLLIN
набором событий для наблюдения. -
Исполняется некий цикл (4), который вызывает
epoll_wait()
(5) для мониторинга заданного в данном экземпляреepoll
списка интересов и обрабатывает возвращаемые в каждом вызове события. Отметим следующие моменты в данном цикле:-
После вызова
epoll_wait()
данная программа проверяет возвращаемыеEINTR
(6), которые могут произойти если данная программа была остановлена неким сигналом посреди вызоваepoll_wait()
и затем возобновленаSIGCONT
(подробнее см. Раздел 21.5). Если случилось именно это, данная программа повторно запускает вызовepoll_wait()
. -
В случае если вызов
epoll_wait()
был успешен, данная программа использует оставшуюся часть цикла для проверки всех присутствующих вevlist
готовых событий (7). Для каждого элемента изevlist
данная программа проверяет полеevents
на наличие или отсутствиеEPOLLIN
(8), а такжеEPOLLHUP
иEPOLLERR
. Эти последние события могут произойти в случае если другой конец FIFO был закрыт или произошло некое отсоединение терминала. Если был возвращёнEPOLLIN
, тогда данная программа считывает некий ввод из соответствующего файлового дескриптора и отображает его в стандартный вывод. В противном случае, если произошлиEPOLLHUP
илиEPOLLERR
, данная программа закрывает соответствующий файловый дескриптор (10) и уменьшает на единицу общий счётчик открытых файлов (numOpenFds
). -
Данный цикл завершается когда были закрыты все открытые файловые дескрипторы (т.е. когда
numOpenFds
равен0
).
-
Мы применяем два окна терминалов. В одном окне мы применяем программу из Листинга 63.5 для отслеживания двух FIFO для ввода. (Всякое открытие некоторого FIFO на чтение
данной программой будет выполнено только после того как другой процесс уже откре этот FIFO на запись, как это
описано в Разделе 44.7). В своём другом окне мы исполняем
экземпляры cat(1)
, которые записывают данные в эти FIFO.
Terminal window 1 Terminal window 2
$ mkfifo p q
$ ./epoll_input p q
$ cat > p
Opened "p" on fd 4
Нажимаем Control-Z для приостановки cat
[1]+ Stopped cat >p
$ cat > q
Opened "q" on fd 5
About to epoll_wait()
Нажимаем Control-Z для приостановки своей программы epoll_input
[1]+ Stopped ./epoll_input p q
Выше мы приостановили свою программу мониторинга с тем, чтобы мы могли теперь создать ввод обоих FIFO и закрыть саму сторону записи в одном из них:
qqq
Нажимаем Control-D для завершения “cat > q”
$ fg %1
$ cat > p
ppp
Теперь мы возобновляем свою программу мониторинга переводя её в фоновый режим в котором пункт
epoll_wait()
возвращает два события:
$ fg
./epoll_input p q
About to epoll_wait()
Ready: 2
fd=4; events: EPOLLIN
read 4 bytes: ppp
fd=5; events: EPOLLIN EPOLLHUP
read 4 bytes: qqq
closing fd 5
About to epoll_wait()
Две пустые строки в предыдущем выводе являются символами новой строки, которые были считаны имеющимися
экземплярами cat
, записанными в соответствующие FIFO, а затем считаны
и отображённые эхом нашей программой мониторинга.
Теперь мы нажимаем Control-D
во втором терминальном окне
чтобы прекратить остающиеся экземпляры cat
, которые вызвали ещё
один возврат epoll_wait()
, причём в этот раз с единственным событием:
Нажимаем Control-D для завершения “cat > q”
Ready: 1
fd=4; events: EPOLLHUP
closing fd 4
All file descriptors closed; bye
Листинг 63.5
---------------------------------------------------------------------- altio/epoll_input.c
#include <sys/epoll.h>
#include <fcntl.h>
#include "tlpi_hdr.h"
#define MAX_BUF 1000 /* Максимум байт, выбираемых отдельным read() */
#define MAX_EVENTS 5 /* Максимальное число событий подлежащее возврату из
некоего отдельного вызова epoll_wait() */
int
main(int argc, char *argv[])
{
int epfd, ready, fd, s, j, num0penFds;
struct epoll_event ev;
struct epoll_event evlist[MAX_EVENTS];
char buf[MAX_BUF];
if (argc < 2 || strcmp(argv[1], "--help") == 0)
usageErr("%s file...\n", argv[0]);
(1) epfd = epoll_create(argc - 1);
if (epfd == -1)
errExit("epoll_create");
/* Открываем каждый файл в командной строке и добавляем его с свой "список
интереса" для имеющегося экземпляра epoll */
(2) for (j = 1; j < argc; j++) {
fd = open(argv[j], O_RDONLY);
if (fd == -1)
errExit("open");
printf("Opened \"%s\" on fd %d\n", argv[j], fd);
ev.events = EPOLLIN; /* Интерес представляют только events из ввода */
ev.data.fd = fd;
(3) if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev) == -1)
errExit("epoll_ctl");
}
numOpenFds = argc - 1;
(4) while (numOpenFds > 0) {
/* Из имеющегося списка готовых извлекаем не более MAX_EVENTS элементов */
printf("About to epoll_wait()\n");
(5) ready = epoll_wait(epfd, evlist, MAX_EVENTS, -1);
if (ready == -1) {
(6) if (errno == EINTR)
continue; /* В случае прерывания сигналом выполняем повторный запуск */
else
errExit("epoll_wait");
}
printf("Ready: %d\n", ready);
/* Обработка возвращённого списка событий */
(7) for (j = 0; j < ready; j++) {
printf(" fd=%d; events: %s%s%s\n", evlist[j].data.fd,
(evlist[j].events & EPOLLIN) ? "EPOLLIN " : "",
(evlist[j].events & EPOLLHUP) ? "EPOLLHUP " : "",
(evlist[j].events & EPOLLERR) ? "EPOLLERR " : "");
(8) if (evlist[j].events & EPOLLIN) {
s = read(evlist[j].data.fd, buf, MAX_BUF);
if (s == -1)
errExit("read");
printf(" read %d bytes: %.*s\n", s, s, buf);
(9) } else if (evlist[j].events & (EPOLLHUP | EPOLLERR)) {
/* Если были установлены и EPOLLIN, и EPOLLHUP, тогда помимо считывания
MAX_BUF байт имеется дополнительная работа. По этой причине мы закрываем
данный файловый дескриптор только если не был установлен EPOLLIN.
После последующего epoll_wait() мы будем считывать поступающие и далее байты. */
printf(" closing fd %d\n", evlist[j].data.fd);
(10) if (close(evlist[j].data.fd) == -1)
errExit("close");
numOpenFds--;
}
}
}
printf("All file descriptors closed; bye\n");
exit(EXIT_SUCCESS);
}
---------------------------------------------------------------------- altio/epoll_input.c
Сейчас мы рассмотрим некоторые тонкости взаимодействия открытых файлов, файловых дескрипторов и
epoll
. Для целей данного обсуждения будет не лишним повторно рассмотреть
Рисунок 5.2, который отображает взаимосвязь между
файловыми дескрипторами, описаниями открытых файлов и таблицей файловых i-node всей системы.
Когда мы создали некий экземпляр epoll
при помощи
epoll_create()
, наше ядро создало некий новый i-node присутствующий в
оперативной памяти и описание открытого файла, а также разместило новый файловый дескриптор в том вызывающем
процессе, который ссылается на данное описание открытого файла. Сам список интереса для некоторого экземпляра
epoll
связан с таким описанием открытого файла, а не с файловым дескриптором
данного epoll
. Это имеет следующие последствия:
-
Если мы дублируем некий файловый дескриптор
epoll
применяяdup()
(или аналогичный вызов), тогда сам дублированный дескриптор ссылается на те же самые списки интереса и готовностиepoll
что и его первоначальный дескриптор. Мы можем изменить этот список интереса определив какой- либо файловый дескриптор в качестве аргументаepfd
в некотором вызовеepoll_ctl()
. Аналогично мы можем выбрать элементы из имеющегося перечня готовности определив либо какой- то файловый дескриптор в качестве определённого аргументаepfd
в вызовеepoll_wait()
. -
Приведённые выше соображения также применяются после вызова
fork()
. Сам потомок наследует некий дубликат файлового дескриптора своего родительскогоepoll
и этот дублированный дескриптор ссылается на ту же самую структуру данныхepoll
.
Когда мы исполняем операцию EPOLL_CTL_ADD
epoll_ctl()
, наше ядро добавляет некий элемент в имеющийся список интереса
epoll
, который записывает оба номера отслеживаемых файловых дескрипторов
и некую ссылку на соответствующее описание открытого файла. Для целей вызовов
epoll_wait()
само ядро осуществляет наблюдение за данным описанием
открытого файла. Это означает, что мы должны уточнить своё более раннее утверждение, что когда файловый дескриптор
закрыт, он автоматически удаляется из всех списков интереса, в которых он является участником. Уточнение
следующее: некий открытый файловый дескриптор удаляется из определённого списка интереса
epoll
когда все ссылающиеся на него файловые дескрипторы были закрыты.
Это означает, что если мы создали дублированные дескрипторы, ссылающиеся на некий открытый файл - с применением
dup()
(или аналогичного), либо fork()
- тогда данный открытый файл будет удалён только после того, как сам первоначальный дескриптор и все его
дубликаты были закрыты.
Такая семантика может привести к некоторому поведению, которое первоначально кажется неожиданным.
Допустим, что мы исполнили тот код, который приведён в Листинге 63.6. Имеющийся в этом коде вызов epoll_wait()
сообщит нам что определённый файловый дескриптор fd1
готов
(другими словами, evlist[0].data.fd
будет эквивалентен
fd1
), даже хотя fd1
был
закрыт. Это происходит по той причине, что всё ещё имеется один открытый файловый дескриптор,
fd2
, ссылающийся на определённое описание открытого файла, содержащееся
в нашем списке интереса epoll
. Аналогичный сценарий происходит и когда
два процесса содержат дублирующие дескрипторы одного и того же описания открытого файла (обычно как некий результат
какого- то fork()
), а данный процесс исполнил
epoll_wait()
, закрывший свой файловый дескриптор, однако другой
имеющийся процесс всё ещё держит свой дублированный дескриптор открытым.
Листинг 63.6: Семантика epoll
для дублированных файловых дескрипторов
int epfd, fd1, fd2;
struct epoll_event ev;
struct epoll_event evlist[MAX_EVENTS];
/* Опущено: код для открытия 'fd1' и создания файлового дескриптора epoll 'epfd' ... */
ev.data.fd = fd1
ev.events = EPOLLIN;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd1, ev) == -1)
errExit("epoll_ctl");
/* Допустим что 'fd1' теперь стал готовым для ввода */
fd2 = dup(fd1);
close(fd1);
ready = epoll_wait(epfd, evlist, MAX_EVENTS, -1);
if (ready == -1)
errExit("epoll_wait");
Таблица 63.9 отображает результаты (для Linux 2.6.25)
когда отслеживается N
подряд идущих файловых дескрипторов в диапазоне
от 0
до N-1
с применением
poll()
, select()
и
epoll
. (Данное тестирование было выравнено таким образом, что на
протяжении каждой операции мониторинга в состоянии готовности пребывал в точности один выбранный случайным образом
файловый дескриптор.) исходя из данной таблицы мы видим, что по мере роста общего числа подлежащих
отслеживанию файловых дескрипторов, poll()
и
select()
выполняются плохо. В противовес им незначительно снижается
по мере возрастания N
. (Отмеченное небольшое возрастание затрат по
мере роста N
, вероятно, является результатом достижения пределов
кэширования ЦПУ в этой тестовой системе.)
Замечание | |
---|---|
Для целей данного тестирования, |
Общее число отслеживаемых дескрипторов N |
Время ЦПУ poll() (секунды) |
Время ЦПУ select() (секунды) |
Время ЦПУ epoll (секунды) |
---|---|---|---|
|
0.61 |
0.73 |
0.41 |
|
2.9 |
3.0 |
0.42 |
|
35 |
35 |
0.53 |
|
990 |
930 |
0.66 |
В Разделе 63.2.5 мы видели почему
poll()
и select()
плохо работают при отслеживании большого числа файловых дескрипторов. Теперь мы рассмотрим те причины,
по которым epoll
работает лучше:
-
При каждом вызове
poll()
илиselect()
само ядро должно проверять все имеющиеся определёнными в данном вызове файловые дескрипторы. Напротив, когда мы помечаем некоторый дескриптор как подлежащий отслеживанию при помощиepoll_ctl()
, наше ядро записывает этот факт в некотором списке, связанным с лежащими в основе открытыми файловыми дескрипторами и всякий раз, когда некая операция ввода/ вывода делает данный файловый дескриптор готовым к исполнению, наше ядро добавляет некий элемент в имеющийся перечень готовности для данного дескриптораepoll
. (Некое событие ввода/ вывода на отдельном описании открытого файла может вызвать готовность множества связанных с этим описанием файловых дескрипторов.) Последующий вызовepoll_wait()
просто осуществляет выборку из данного списка готовности. -
Всякий раз когда мы вызываем
poll()
илиselect()
, мы передаём своему ядру некую структуру данных, которая указывает все имеющиеся подлежащие мониторингу файловые дескрипторы и, наоборот, наше ядро возвращает обратно некую структуру данных, описывающую готовность всех этих дескрипторов. В противоположность этому, при использованииepoll
, мы применяемepoll_ctl()
для построения некоторой структуры данныхв пространстве ядра
, которая перечисляет всё множество подлежащих мониторингу файловых дескрипторов, а сам вызовepoll_wait()
возвращает информацию только о тех дескрипторах, которые являются готовыми.
Замечание | |
---|---|
Помимо всего указанного выше, в случае с |
Очень схематично мы можем сказать, что для больших значений N
(общего число подлежащих отслеживанию файловых дескрипторов), значение производительности
select()
и poll()
линейно
масштабируется со значением N
. Мы начали наблюдать такое поведение для
случаев N = 100
и N = 1000
в
Таблице 63.9. Со временем мы достигли
N = 10000
, причём масштабирование в действительности оказалось хуже
линейного.
Напротив, epoll
масштабируется (линейно) в соответствии с общим числом
имеющих место событий ввода/ вывода. API epoll
таким образом,
в особенности эффективен для некоторого сценария, который является распространённым в серверах,
которые обрабатывают одновременно большое число клиентов: когда при подлежащих отслеживанию больших
значений файловых дескрипторов большинство простаивают; готовыми является лишь небольшое число.
По умолчанию механизм epoll
предоставляет уведомления,
переключаемые уровнем
(level-triggered). Под этим мы подразумеваем,
что epoll
сообщает нам может ли какая- либо операция ввода/ вывода
быть выполнена с неким файловым дескриптором без блокировки. Именно этот тип уведомления предоставляется
poll()
и select()
.
API epoll
также предоставляет нам уведомления
переключаемые фронтом
(edge-triggered) - то есть, некий вызов
epoll_wait()
сообщит нам если с момента предыдущего вызова
epoll_wait()
произошло некое действие ввода/ вывода с неким файловым
дескриптором (или с момента открытия данного дескриптора при отсутствии предыдущего вызова). Применение
epoll
с управляемым фронтом уведомлением семантически аналогично
управляемому сигналом вводу/ выводу за исключением того, что если возникает множество событий,
epoll
сливает их в некое единое уведомление, возвращаемое через
epoll_wait()
; при управляемом сигналом вводе/ выводе может
вырабатываться множество сигналов.
Для применения управляемых фронтом уведомлений мы определяем в ev.events
флаг EPOLLET
при вызове
epoll_ctl()
:
struct epoll_event ev;
ev.data.fd = fd
ev.events = EPOLLIN | EPOLLET;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, ev) == -1)
errExit("epoll_ctl");
Мы проиллюстрируем основное отличие управляемого уровнем и управляемого фронтом уведомления
epoll
с использованием некоторого примера. Предположим, что мы применяем
epoll
для мониторинга некоторого сокета на ввод
(EPOLLIN
) и происходят следующие
этапы:
-
В данном сокете возникает ввод.
-
Мы исполняем некий
epoll_wait()
. Данный вызов сообщает нам, что данный сокет готов вне зависимости от того, применили ли мы уведомление управляемое уровнем или фронтом. -
Мы выполняем некий повторный вызов
epoll_wait()
.
Если мы применяем управляемое уровнем уведомление, тогда второй вызов
epoll_wait()
проинформирует нас, что данный сокет готов. Если же мы
применяем управляемое фронтом уведомление, тогда второй вызов epoll_wait()
букет заблокирован, так как никакого нового ввода не возникло с момента предыдущего вызова
epoll_wait()
.
Как мы уже упоминали в Разделе 63.1.1, переключаемое
фронтом уведомление обычно применяется совместно с неблокируемыми файловыми дескрипторами. Таким образом, общая
структура для использования управляемого фронтом уведомления epoll
следующая:
-
Сделать все подлежащие мониторингу файловые дескрипторы неблокируемыми.
-
С помощью
epoll_ctl()
построить необходимый список интересаepoll
. -
Обрабатывать события ввода/ вывода с применением следующего цикла:
-
При помощи
epoll_wait()
выбрать список готовых дескрипторов. -
Для каждого готового файлового дескриптора обрабатывать ввод/ вывод пока соответствующий системный вызов (например,
read()
,write()
,recv()
,send()
илиaccept()
) не будет возвращён с ошибкойEAGAIN
илиEWOULDBLOCK
.
-
Предотвращение неопределённого блокирования файловых дескрипторов при применении управляемого фронтом уведомления
Предположим, что мы отслеживаем множество файловых дескрипторов применяя управляемые фронтом уведомления и что некий готовый файловый дескриптор имеет достаточно большой объём (возможно, некий бесконечный поток) доступного входа. Если, после определения того, что такой файловый дескриптор готов, мы пытаемся принять весь имеющийся ввод с применением неблокируемых чтений, тогда мы рискуем посадить на голодный паёк (выполнить неопределённое блокирование) прочие требующие внимания файловые дескрипторы (т.е. может пройти длительный промежуток времени пока мы снова проверим их готовность и выполним в них ввод/ вывод). Одно из решений данной проблемы состоит в том, что данное приложение сопровождает некий список получивших уведомление о готовности файловых дескрипторов и выполняет некий цикл, который непрерывно выполняет следующие действия:
-
Отслеживает необходимые файловые дескрипторы с применением
epoll_wait()
и добавляет готовые дескрипторы в имеющийся список приложения. Если какой- либо дескриптор уже зарегистрирован готовым в данном списке приложения, тогда значение таймаута для этого шага мониторинга должно быть малым или нулевым с тем, чтобы когда нет готовых новых дескрипторов, данное приложение могло быстро проследовать к следующему этапу и и обслужить все файловые дескрипторы, которые уже известны как готовые. -
Выполнить ограниченный объём ввода/ вывода на тех зарегистрированных файловых дескрипторах, которые готовы в имеющемся перечне приложения (возможно, выполняя их циклический обход карусельным - round-robin - образом, вместо того чтобы начинать с самого начала имеющегося списка после каждого вызова
epoll_wait()
). Некий файловый дескриптор может быть удалён из данного перечня приложения в случае отказа соответствующего неблокируемого системного вызова ввода/ вывода или при ошибкеEAGAIN
либоEWOULDBLOCK
.
Хотя это требует дополнительной работы по программированию, такой подход предлагает прочие преимущества помимо
предотвращения режима голода файловых дескрипторов. Например, мы можем в приведённый выше цикл дополнительные
шаги, такие как обработка таймеров и приём сигналов с помощью sigwaitinfo()
(или аналогичных вызовов).
Необходимо также уделять внимание неопределённому блокированию при использовании управляемого сигналами ввода/ вывода, так как он также представляет некий механизм управляемого фронтом уведомления. Напротив, рассмотрение режима голодания не требуется в приложениях, использующих механизм уведомления с применением переключения уровнем. Это обусловлено тем, что мы можем применять при управляемом уровнем уведомлении файловых дескрипторов с блокированием и использовать цикл, который непрерывно проверяет дескрипторы на предмет готовности, а затем выполняет некий ввод/ вывод для готовых дескрипторов прежде чем снова проверит готовность файловых дескрипторов.
Порой некому процессу необходимо одновременно дождаться ввода/ вывода чтобы получить возможность
на одном из наборов файловых дескрипторов или доставки некого сигнала. Мы можем попробовать выполнить такую
операцию с помощью select()
, как это показано в Листинге 63.7.
Листинг 63.7: Неправильный метод неблокируемых сигналов и вызова select()
sig_atomic_t gotSig = 0;
void
handler(int sig)
{
gotSig = 1;
}
int
main(int argc, char *argv[])
{
struct sigaction sa;
...
sa.sa_sigaction = handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
if (sigaction(SIGUSR1, &sa, NULL) == -1)
errExit("sigaction");
/* Что случится если сигнал будет доставлен сейчас? */
ready = select(nfds, &readfds, NULL, NULL, NULL);
if (ready > 0) {
printf("%d file descriptors ready\n", ready);
} else if (ready == -1 && errno == EINTR) {
if (gotSig)
printf("Got signal\n");
} else {
/* Какие- то ещё ошибки */
}
...
}
Проблема данного кода в том, что если определённый сигнал (в данном примере
SIGUSR1
) появится после установления
данного обработчика, но до вызова select()
, тогда данный вызов
select()
будет тем не менее блокирован. (Это некий вид условия
соперничества). Теперь мы рассмотрим некоторые решения данной проблемы.
Замечание | |
---|---|
Начиная с версии 2.6.27, Linux предоставляет некую дополнительную технику, которая может применяться
для одновременного ожидания сигналов и файловых дескрипторов: описанный в Разделе
22.11 механизм |
Системный вызов pselect()
выполняет задачу аналогичную
select()
. Самое главное семантическое отличие состоит в некотором
дополнительном аргументе, sigmask
, который определяет какоё- то набор
подлежащих маскированию при блокировке данного вызова сигналов.
#define _XOPEN_SOURCE 600
#include <sys/select.h>
int pselect(int >nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,
struct timespec *timeout, const sigset_t *sigmask);
Возвращает число готовых файловых дескрипторов, 0 в случае таймаута, либо -1 при ошибке
Чтобы быть более точным, допустим, у нас имеется следующий вызов
pselect()
:
ready = pselect(nfds, &readfds, &writefds, &exceptfds, timeout, &sigmask);
Данный код автоматически эквивалентен исполнению следующих шагов:
sigset_t origmask;
sigprocmask(SIG_SETMASK, &sigmask, &origmask);
ready = select(nfds, &readfds, &writefds, &exceptfds, timeout);
sigprocmask(SIG_SETMASK, &origmask, NULL); /* Восстановление маскирования сигналов */
При помощи pselect()
мы можем записать самую первую часть
тела нашей основной программы в Листинге 63.7,
так, как это показано в Листинге 63.8.
Помимо самого аргумента sigmask
,
select()
и pselect()
отличаются следующими способами:
-
Сам аргумент
timeout
дляpselect()
является некоторой структуройtimeout
(Раздел 23.4.2), которая допускает определение таймаута с точностью до наносекунд (вместо микрсекунд). -
SUSv3 явно устанавливает что
pselect()
не может изменять возвращаемое значение аргументаtimeout
.
Если мы определили значение аргумента sigmask
в
pselect()
как
NULL
, тогда
pselect()
эквивалентен select()
(т.е. не выполняет никаких манипуляций с маскированием сигналов данного процесса), за исключением только
что упомянутых различий.
Интерфейс pselect()
представляет собой изобретение POSIX.1g и в
настоящее время включён в SUSv3. Он недоступен для всех реализаций UNIX и был добавлен в Linux только в ядре
2.6.16.
Замечание | |
---|---|
Первоначально библиотечная функция |
Листинг 63.8: Применение pselect()
sigset_t emptyset, blockset;
struct sigaction sa;
sigemptyset(&blockset);
sigaddset(&blockset, SIGUSR1);
if (sigprocmask(SIG_BLOCK, &blockset, NULL) == -1)
errExit("sigprocmask");
sa.sa_sigaction = handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
if (sigaction(SIGUSR1, &sa, NULL) == -1)
errExit("sigaction");
sigemptyset(&emptyset);
ready = pselect(nfds, &readfds, NULL, NULL, NULL, &emptyset);
if (ready == -1)
errExit("pselect");
Системные вызовы ppoll()
и epoll_pwait()
Linux 2.6.16 также добавил новый, нестандартный системный вызов, ppoll()
,
связь которого с poll()
аналогична взаимоотношению
pselect()
к select()
.
Аналогично, начиная с Linux 2.6.19, Linux также включил epoll_pwait()
,
предоставляющее аналогичное расширение для epoll_wait()
. Для
ознакомления с подробностями обратитесь к страницам руководства
ppoll(2)
(рус.яз.)
epoll_pwait(2)
(рус.яз.)
Так как pselect()
не имеет широко распространённой реализации,
переносимые приложения должны придерживаться других стратегий избежания условий соперничества при одновременном
ожидании сигналов и вызова select()
на некотором множестве
файловых дескрипторов. Одно общее решение состоит в следующем:
-
Создать некий конвейер и пометить его стороны чтения и записи как неблокируемые.
-
Помимо всех прочих представляющих интерес файловых дескрипторов, включите сторону считывания данного конвейера в свой набор
readfds
, относящийся кselect()
. -
Установите некий обработчик для того сигнала, который представляет интерес. Когда этот обработчик сигнала вызывается, он записывает некие байтовые данные в данный конвейер. Относительно данного обработчика отметим следующие моменты:
Та сторона записи данного конвейера, которая была отмечена как неблокируемая на самом первом шаге для предотвращения той возможности, сигналы возникают настолько быстро, что повторяющиеся вызовы данного обработчика сигналов заполняют весь конвейер, что имеет результатом то, что данный обработчик сигнала
write()
(а таким образом и сам процесс) блокируется. (Если произойдет отказ записи в некий заполненный конвейер, это не будет иметь значения, так как все предыдущие записи уже указали доставку данного сигнала.)-
Данный обработчик сигнала установлен после создания рассматриваемого конвейера с тем, чтобы предотвратить такое условие состязательности которое может возникнуть если некий сигнал был доставлен до создания данного конвейера.
-
Внутри данного обработчика сигнала применение
write()
является безопасным, так как это одна из функций async-signal-safe (безопасного асинхронного сигнала), перечисленных в Таблице 21-1
-
Поместите вызов
select()
в некий цикл с тем, чтобы он перезапускался если выполняется прерывание неким обработчиком сигнала. (Предоставленный таким образом перезапуск строго говоря не обязателен; он просто означает, что мы можем проверить возникновение некоторого сигнала путём инспектированияreadfds
вместо того чтобы проверять наличие некоторой возвращаемой ошибкиEINTR
.) -
В случае успешного завершения данного вызова
select()
, мы можем определить возник ли данный сигнал проверив установлен ли тот файловый дескриптор, который относится к нашей стороне считывания данного конвейера вreadfds
. -
Всякий раз когда мы получаем некий сигнал, считываем все имеющиеся в данном конвейере байты. Так как может возникнуть множество сигналов, примените некий цикл, который считывает байты пока не будет получен отказ данного (неблокируемого)
read()
с ошибкой, имеющей значениеEAGAIN
. После осушения данного конвейера выполните любые действия, которые должны быть осуществлены в ответ на доставку данного сигнала.
Данная техника имеет распространённое название self-pipe trick (хитрость конвейеризации самого себя), а код, демонстрирующий эту технику показан в Листинге 63.9
Листинг 63.9: Применение трюка конвейеризации самого себя
---------------------------------------------------------------------- altio/self_pipe.c
static int pfd[2]; /* Файловые дескрипторы для конвейера */
static void
handler(int sig)
{
int savedErrno; /* На случай изменения нами 'errno' */
savedErrno = errno;
if (write(pfd[1], "x", 1) == -1 && errno != EAGAIN)
errExit("write");
errno = savedErrno;
}
int
main(int argc, char *argv[])
{
fd_set readfds;
int ready, nfds, flags;
struct timeval timeout;
struct timeval *pto;
struct sigaction sa;
char ch;
/* ... Инициализируем 'timeout', 'readfds' и 'nfds' для select() */
if (pipe(pfd) == -1)
errExit("pipe");
FD_SET(pfd[0], &readfds); /* Добавление стороны считывания конвейера в 'readfds' */
nfds = max(nfds, pfd[0] + 1); /* И выравнивание 'nfds', если это необходимо */
flags = fcntl(pfd[0], F_GETFL);
if (flags == -1)
errExit("fcntl-F_GETFL");
flags |= O_NONBLOCK; /* Делаем сторону считывания неблокируемой */
if (fcntl(pfd[0], F_SETFL, flags) == -1)
errExit("fcntl-F_SETFL");
flags = fcntl(pfd[1], F_GETFL);
if (flags == -1)
errExit("fcntl-F_GETFL");
flags |= O_NONBLOCK; /* Делаем сторону записи неблокируемой */
if (fcntl(pfd[1], F_SETFL, flags) == -1)
errExit("fcntl-F_SETFL");
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART; /* Перезапуск прерванных read() */
sa.sa_handler = handler;
if (sigaction(SIGINT, &sa, NULL) == -1)
errExit("sigaction");
while ((ready = select(nfds, &readfds, NULL, NULL, pto)) == -1 &&
errno == EINTR)
continue; /* Перезапуск в случае прерывания сигналом */
if (ready == -1) /* Непредвиденная ошибка */
errExit("select");
if (FD_ISSET(pfd[0], &readfds)) { /* Обработчик был вызван */
printf("A signal was caught\n");
for (;;) { /* Поедаем байты из конвейера */
if (read(pfd[0], &ch, 1) == -1) {
if (errno == EAGAIN)
break; /* Байты закончились */
else
errExit("read"); /* Какая- то иная ошибка */
}
/* Выполняем все необходимые действия, которые следует предпринять в ответ на сигнал */
}
}
/* Опрашиваем возвращённых select() наборы файловых дескрипторов чтобы посмотреть
какие прочие файловые дескрипторы в состоянии готовности */
}
---------------------------------------------------------------------- altio/self_pipe.c
В данной главе мы исследовали различные альтернативы стандартным моделям выполнения ввода/ вывода:
мультиплексированный ввод/ вывод (select()
и
poll()
), управляемый сигналами ввод/ вывод и особенный для Linux
API epoll
. Все эти механизмы позволяют нам отслеживать множество
файловых дескрипторов на предмет того, возможны ли к кому- либо из них операции ввода/ вывода. Никакой
из этих механизмов в действительности не выполняет ввод/ вывод. Вместо этого, как только мы определили
что некий файловый дескриптор готов, мы применяем обычные системные вызовы ввода/ вывода для выполнения
собственно операций ввода/ вывода.
Мультиплексированные вызовы ввода/ вывода select()
и
poll()
одновременно отслеживают множество файловых дескрипторов
чтобы увидеть наличие возможности ввода/ вывода в одном из имеющихся дескрипторов. Для обоих системных
вызовов мы передаём полный перечень подлежащих проверке файловых дескрипторов в своё ядро для каждого
системного вызова, а само ядро возвращает изменённый список, отображающий какой из дескрипторов является
готовым. Тот факт, что полный перечень файловых дескрипторов передаётся и проверяется при каждом вызове
означает что select()
и poll()
имеют плохую производительность при отслеживании большого числа участвующих файловых дескрипторов.
Управляемый сигналами ввод/ вывод позволяет некоторому процессу изымать некий сигнал когда в каком- то
файловом дескрипторе возможен ввод/ вывод. Чтобы разрешить управляемый сигналом ввод/ вывод, мы должны
установить некий обработчик для своего сигнала
SIGIO
, настроить определённый
процесс владелец, который должен принимать этот сигнал, а также разрешить выработку сигнала установив флаг
состояния файла O_ASYNC
. Этот
механизм предлагает значительные преимущества производительности в сравнении с мультиплексированным вводом/
выводом при отслеживании большого числа файловых дескрипторов. Linux делает для нас возможным изменять
применяемый для уведомления сигнал и если мы вместо этого будем применять сигнал времени исполнения, тогда
множественное уведомление может быть выстроено в очередь, а имеющийся обработчик сигнала может применять
свой аргумент siginfo_t
для определения необходимого файлового
дескриптора и типа события, который породил данный сигнал.
Как и управляемый сигналом ввод/ вывод, epoll
предоставляет
наивысшую производительность при отслеживании большого числа файловых дескрипторов. Основное преимущество
производительности epoll
(и управляемого сигналом ввода/ вывода)
проистекает из того факта, что само ядро "запоминает" весь список файловых дексрипторов, которые
отслеживает некий процесс (в отличии от select()
и
poll()
, когда каждый системный вызов должен вновь сообщать своему
ядру какие файловые дескрипторы проверять). API epoll
имеет
некоторые ощутимые преимущества в сопоставлении с применением управляемого сигналом ввода/ вывода: мы
избегаем всей сложности обработки сигналов и можем определять какие типы событий ввода/ вывода (например,
ввод или вывод) подлежат отслеживанию.
По ходу данной главы мы обрисовали некоторые отличия между уведомлениями о готовности, управляемыми
уровнем или фронтом (level-triggered и edge-triggered). При модели переключения уровнем мы информируемся
всякий раз когда ввод/ вывод в настоящий момент доступен для некоторого файлового дескриптора. Напротив,
управляемое фронтом уведомление ставит нас в известность всякий раз когда произошло действие ввода/ вывода
в некотором файловом дескрипторе с момента его последнего отслеживания. Все системы ввода/ вывода с
мультиплексированием предлагают некую модель уведомления, переключаемых уровнем; управляемый сигналом ввод/
вывод ориентирован на модель переключения фронтом; а epoll
способен
работать с любой моделью (принимаемой по умолчанию является модель управления уровнем). Движимое фронтом
уведомление обычно применяется совместно с неблокируемым вводом/ выводом.
Мы завершили данную главу изучением некоторой проблемы, с которой иногда сталкиваются программы,
которые отслеживают множество файловых дескрипторов: как в то же самое время ожидать доставки некоторого
сигнала. Обычным решением данной проблемы обычно является так называемая хитрость собственной конвейеризации
(self-pipe trick), в то время как некий обработчик данного сигнала записывает какой- то байт в конвейер, сторона
чтения которого содержится в имеющемся наборе отслеживаемых файловых дескрипторов. SUSv3 определяет
pselect()
, видоизменение select()
,
который предоставляет другое решение данной проблемы. Однако pselect()
не доступен во всех реализациях UNIX. Linux также предоставляет аналогичные (но не стандартные
ppoll()
и epoll_pwait()
).
Дальнейшая информация | |
---|---|
[Stevens et al., 2004] описывает мультеплексироанный ввод/ вывод и управляемый сигналом ввод/ вывод,
причём особое внимание уделяет конкретному применению этих механизмов с сокетами. [Gammo et al, 2004]
это статья, сопоставляющая производительность Особый интерес представляет интернет ресурс с адресом http://www.kegel.com/c10k.html. Написанная Деном Кегелом и имеющее название "Проблема C10K", данная веб страница исследует ту проблему, с которой сталкиваются разработчики веб серверов, проектируемые для одновременного обслуживания десятков тысяч клиентов. Эта веб страница содержит множество ссылок на соответствующую информацию. |
63.1
Измените текст программы в Листинге 63.2 (
poll_pipes.c
) на использованиеselect()
вместоpoll()
.
63.2
Напишите некий сервер
echo
(см. Раздел 60.2 и Раздел 60.3), который обрабатывает и клиентов TCP, и клиентов UDP. Для этого данный сервер должен создать и сокет ожидания TCP, и сокет ожидания UDP, а затем отслеживать оба сокета применяя одну и из тех техник, которые описанв в данной главе.
63.3
Раздел 63.5 отметил, что
select()
может применяться для ожидания и сигналов и файловых дескрипторов, а также описывает некое решение применения обработчика сигналов и конвейера. Некая родственная проблема имеется когда некоторой программе требуется ожидать ввод и в каком- то файловом дескрипторе и в какой- то очереди сообщений System V (так как очереди сообщений System V не применяют файловые дескрипторы). Одно из решений состоит в ответвлении некоторого отдельного дочернего процесса, который копирует все сообщения из данной очереди в некий конвейер, содержащий определённые файловые дескрипторы, отслеживаемые его предком. Напишите некую программу, которая применяет данную схему сselect()
для отслеживания ввода сразу и из самого терминала, и из очереди сообщений.
63.4
Самый последний этап имеющегося в Раздел 63.5.2 описания техники собственной конвейеризации постулирует, что данная программа должна вначале осушить сам конвейер, а затем выполнить все действия, которые данная программа должна предпринять в ответ на данный сигнал. Что модет приключиться, если эти подэтапы будут изменены в порядке следования своими местами?
63.5
Измените текст программы в Листинге 63.9 (
self_pipe.c
) на использованиеpoll()
вместоselect()
.
63.6
Напишите программу, которая применяет
epoll_create()
для создания некоего экземпляраepoll
и затем немедленно переходит к ожиданию на возвращённом файловом дескрипторе при помощиepoll_wait()
. Что произойдёт, когда, как в данном случае,epoll_wait()
придаётся некий файловый дескрипторepoll
с пустым списком интереса? Почему это может оказаться полезным?
63.7
Предположим, у нас имеется некий файловый дескриптор
epoll
, который отслеживается множеством файловых дескрипторов, причём все они уже в состоянии готовности. Если мы выполним некую последовательность вызововepoll_wait()
при которойmaxevents
намного меньше чем общее число готовых файловых дескрипторов (например,epoll_wait()
равен 1), без выполнения всего возможного ввода/ вывода в этих готовых дескрипторах между вызовами, какой(какие) дескриптор(ы) будет возвращатьepoll_wait()
при каждом вызове? Напишите некую программу чтобы получить точный ответ. (Для целей данного эксперимента будет достаточным не выполнять никакого ввода/ вывода между вашими системными вызовамиepoll_wait()
.) Зачем может понадобиться такое поведение?
63.8
Измените текст программы в Листинге 63.3 (
demo_sigio.c
) на использование сигнала времени исполнения вместоSIGIO
. Измените имеющийся обработчик сигнала на приём некоторого аргументаsiginfo_t
и отображения всех значений полейsi_fd
иsi_code
данной структуры.