Глава 63. Альтернативные модели ввода/ вывода

Содержание

Глава 63. Альтернативные модели ввода/ вывода
63.1 Обзор
63.1.1 Уведомления переключаемые уровнем и фронтом
63.1.2 Применение неблокирующего ввода/ вывода в альтернативных моделях ввода/ вывода
63.2 Мультиплексирование ввода/ вывода
63.2.1 Системный вызов select()
63.2.2 Системный вызов poll()
63.2.3 Когда готов файловый дескриптор?
63.2.4 Сравнение select() и poll()
63.2.5 Проблемы с select() и poll()
63.3 Движимый сигналами ввод/ вывод
63.3.1 Когда выставляется сигнал "ввод/ вывод возможен"?
63.3.2 Совершенствование применения движимого сигналами ввода/ вывода
63.4 API epoll
63.4.1 Создание экземпляра epoll: epoll_create()
63.4.2 Изменение списка участия epoll: epoll_ctl()
63.4.3 Ожидание событий: epoll_wait()
63.4.4 Более подробное рассмотрение семантики epoll
63.4.5 Сопоставление производительности epoll и мультиплексирования
63.4.6 Переключаемые фронтом уведомления
63.5 Ожидание сигналов и файловых дескрипторов
63.5.1 Системный вызов pselect()
63.5.2 Хитрость конвейеризации самого себя
63.6 Выводы
63.7 Упражнения

Данная глава обсуждает три альтернативы обычной модели файлового ввода/ вывода, которые мы вы применяли в большинстве программ, показанных в данной книге:

  • Мультиплексирование ввода/ вывода (системные вызовы select() и poll();

  • управляемый сигналами ввод/ вывод; и

  • особый для Linux API epoll.

{Прим. пер.: рекомедуем также ознакомиться с примерами использования select() и epoll в нашем переводе второго издания "Книги рецептов сетевого программирования Python" Прейдибэн Катхирейвелу и доктора М.О. Фарук Саркер, выпущенной Packt Publishing в августе 2017..}

63.1 Обзор

Большая часть программ, которые мы представляли до сих пор в данной книге применяли некую модель ввода/ вывода, при которой какой- то процесс выполнял ввод вывод только с одним файловым дескриптором за один раз, причём каждый системный вызов ввода/ вывода блокировался пока передаются все данные. Например, при считывании из конвейера некий вызов read() обычно блокируется если в данном канале не представлены никакие данные в настоящее время, в вызов write() блокируется если в конвейере недостаточно места для записи данных. Аналогичное поведение происходит и при выполнении ввода/ вывода с различными прочими типами файлов, включая FIFO и сокеты.

[Замечание]Замечание

Дисковые файлы являются особым случаем. Как описывалось в Главе 13, имеющееся ядро предоставляет свой буфер кэширования для ускорения дисковых запросов на операции ввода/ вывода. Таким образом, некий write() на диск выполняет возврат как только все запрошенные данные были переданы в имеющийся буфер кэширования ядра, вместо того чтобы ожидать записи на диск всех данных (только если при открытии данного файла не был определён флаг O_SYNC). Соответственно, некий read() передаёт данные из имеющегося буфера кэширования в какой- то буфер пользователя и, если все необходимые данные не находятся в данном буфере кэширования, тогда данное ядро помещает данный процесс в спящий режим пока не выполнится чтение с диска.

Традиционной модели блокируемого ввода/ вывода достаточно для многих приложений, но не для всех. В частности, некоторым приложениям необходимо иметь возможность делать одно из следующего:

  • Проверить: возможен ли ввод/ вывод для некоторого файлового дескриптора без его блокирования если он не возможен.

  • Отслеживать множество файловых дескрипторов для просмотра возможности ввода/ вывода на любом из них.

Мы уже встречали две технологии, которые могут применяться для частичного решения этих потребностей: неблокируемого ввода/ вывода и применения множества процессов или потоков.

Мы обсуждали неблокируемый ввод вывод с некоторыми подробностями в Разделах 5.9 и 44.9. Если мы помещаем некий файловый дескриптор в неблокируемый режим, разрешая флаг состояния открытия файла O_NONBLOCK, тогда некий системный вызов операции ввода/ вывода, который не может быть немедленно выполнен возвращает некую ошибку вместо блокирования. Неблокируемый ввод/ вывод может применяться с конвейерами, FIFO, сокетами, терминалами, псевдотерминалами и некоторыми другими типами устройств.

Неблокируемый ввод/ вывод позволяет нам периодически проверять (&qout;опрашивать&qout; &qout;poll&qout;) возможен ли ввод/ вывод для данного файлового дескриптора. Например, мы можем сделать некий файловый дескриптор ввода неблокируемым и затем периодически выполнять неблокируеые чтения. Если нам необходимо отслеживать множество файловых дескрипторов, тогда мы помечаем их как неблокируемые и опрашиваем их все одного за другим. Однако, выполнение опроса таким образом обычно нежелательно. Если опрос выполняется достаточно редко, тогда получаемые задержки, прежде чем приложение откликнется на некое событие ввода/ вывода, могут быть неприемлемо длинными; с другой стороны, жёсткий цикл тратит время ЦПУ.

[Замечание]Замечание

В данной главе мы применяем слово poll (опрос) двумя различными способами. Один из них заключается в именовании имеющейся мультиплексированного системного вызова ввода/ вывода, poll(). При другом применении мы подразумеваем &qout;выполнение неблокируемой проверки состояния некоторого файлового дескриптора&qout;.

Если мы не желаем выполнять блокировку некоторого процесса при выполнении операции ввода/ вывода для какого- либо файлового дескриптора, мы можем создать некий новый процесс для выполнения такого ввода/ вывода. Имеющийся родительский процесс затем позаботится о выполнении прочих задач, пока процесс потомок будет блокирова, пока выполняется данный ввод/ вывод. Если нам необходимо обрабатывать ввод/ вывод множества файловых дескрипторов, мы можем создавать по одному дочернему процессу для каждого дескриптора. Основными проблемами такого подхода являются затраты и сложность. Создание и сопровождение процессов размещает некую нагрузку на всю систему, к тому же, как правило, такие дочерние процессы потребуют применения некоторой формы 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 предоставляет в рамках glibc реализацию POSIX AIO на основе потоков. На время написания книги велась работа по реализации предоставления реализации POSIX AIO внутри ядра, что должно лучше масштабировать производительность. POSIX AIO описывается в [Gallmeister, 1995] и [Robbins & Robbins, 2003].

 

Какую технологию выбрать?

На протяжении данной главы мы рассмотрим все причины того почему мы можем выбрать одну из данных технологий вместо оставшихся. Пока же мы просуммируем некоторые моменты:

  • Системные вызовы select() и poll() являются давно работающими интерфейсами, котрые имеют место в системах UNIX на протяжении многих лет. В сравнении с прочими технологиями их основным преимуществом является переносимость. Их главным недостатком является то, что они не могут хорошо масштабироваться для наблюдения большого числа (измеряемого сотнями и тысячами) файловых дескрипторов.

  • Ключевым преимуществом API epoll является то, что они делают возможным для приложений выполнять мониторинг больших количеств файловых дескрипторов. Его первичным недостатком является то, что этот API является специфичным для Linux.

    [Замечание]Замечание

    Некоторые прочие реализации UNIX предоставляют (нестандартные) механизмы, аналогичные epoll. Solaris предоставляет особый файл /dev/poll, (описываемый в страницах руководства Solaris poll(7d), а некоторые имеющиеся BSD предоставляют свой API kqueue (который поставляет более универсальные возможности мониторинга чем epoll). [Stevens et al., 2004] вкратце описывает оба механизма; более длинное обсуждение kqueue можно найти в [Lemon, 2001].

  • Как и epoll, управляемый сигналом ввод/ вывод делает возможным для приложений эффективно отслеживать большое число файловых дескрипторов. Однако epoll предоставляет ряд преимуществ над управляемым сигналом вводом/ выводом:

    • Мы избегаем связанной с обработкой сигналов сложности

    • Мы можем определять конкретный вид мониторинга, который мы бы желали выполнять (например, готовность для чтения или готовность для записи).

    • Мы можем выбрать либо переключаемое уровнем, либо переключаемое фронтом уведомление (описывается в Разделе 63.1).

  • Более того, получение всех преимуществ управляемого сигналом ввода/ вывода требует применения несовместимых, особенных для LInux свойств и, если мы это делаем, движимый сигналом ввод/ вывод не более перенсоим, нежели epoll.

Так как с одной стороны select() и poll() являются более переносимыми с одной стороны, но при том что управляемый сигналом ввод/ вывод и epoll предоставляют лучшую производительность для ряда приложений, может оказаться представляющим больший результат написание абстрактного программного уровня для отслеживания событий файлового дескриптора. Имея такой уровень, переносимая программа может применять epoll (или аналогичный API) в тех системах, которые его предоставляют, и в то же время откатывать обратно к select() и poll() в прочих системах.

[Замечание]Замечание

Библиотека libevent является неким программным уровнем, которые предоставляет какую- то абстракцию для мониторинга событий файловых дескрипторов. Она была портирована в целый ряд систем UNIX. В качестве своего базового уровня libevent может (прозрачно) любую описываемую в данной главе технологию: select(), poll(), управляемый сигналом ввод/ вывод и epoll, так же как присущий Solaris интерфейс /dev/poll или имеющийся в BSD интерфейс kqueue. (Таким образом, libevent также служит примером того, как применять каждую из этих технологий.) Написанная Найилсом Поровосом (Niels Provos), libevent доступна по адресу http://monkey.org/~provos/libevent/ .

63.1.1 Уведомления переключаемые уровнем и фронтом

Прежде чем мы обсудим более подробно различные альтернативные механизмы ввода/ вывода, нам необходимо обсудить две модели уведомлений о готовности для некоторого файлового дескриптора:

  • Переклчаемое уровнем уведомление: Некий файловый дескриптор рассматривается как готовый если имеется возможность выполнениря какого- то системного вызова ввода/ вывода без блокирования.

  • Переклчаемое фронтом уведомление: Уведомление осуществляется если существует некая активность ввода/ вывода (например, новый ввод) в каком- то файловом дескрипторе с момента последнего мониторинга.

Таблице 63.1 суммирует все модели уведомления, производимые моделями мультиплексирования, управляемого сигналом ввода/ вывода и epoll. API epoll отличается от остальных двух моделей ввода/ вывода тем, что она может предоставлять и управляемое уровнем уведомление (по умолчанию), и управляемое фронтом уведомление.

Таблица 63.1: Использование управляемых уровнем и фронтом моделей уведомления
Модель ввода/ вывода Переключаемая уровнем? Переключаемая фронтом?

select(), poll()

+

 

Движимый сигналом ввод/ вывод

 

+

epoll

+

+

Подробности отличий этих двух моделей уведомления станут яснее на протяжении данной главы. На текущий момент мы опишем как сам выбор модели уведомления воздействует на способ, которым мы разрабатываем некую программу.

Когда мы применяем управляемые уровнем уведомления, мы можем проверить готовность некоторого файлового дескриптора в любой момент. Это означает, что когда мы определили, что некий файловый дескриптор готов (например, он имеет доступным ввод), мы можем выполнить в этом дескрипторе некий ввод/ вывод, а затем повторить саму операцию мониторинга чтобы проверить, что данный дескриптор всё ещё готов (например, он всё ещё имеет доступным дополнительный ввод), и в этом случае мы можем выполнять дополнительный ввод/ вывод снова и снова. Другими словами, так как управляемая уровнем модель позволяет нам повторять подобную операцию мониторинга ввода/ вывода в любое время, нет необходимости выполнять столько операций ввода/ вывода, сколько возможно (например, считать все доступные байты) для данного файлового дескриптора (или даже вовсе не выполнять никакого ввода/ вывода) всякий раз, когда мы уведомлены что некий файловый дескриптор готов.

Напротив, когда мы применяем управляемое фронтом уведомление, мы получаем такое уведомление только когда происходит некое событие ввода/ вывода. Мы не получаем никаких последующих уведомлений пока не произойдёт другое обытие ввода/ вывода. Более того, когда мы получаем некоторое уведомление о каком- то событии ввода/ вывода для некоторого файлового дескриптора, мы обычно не знаем в точности сколько операций ввода/ вывода возможно (например, сколько байт доступно для считывания). Таким образом, реализующая переключаемое фронтом уведомление программа обычно разрабатывается в соответствии со следующими правилами:

  • После получения уведомления о некотором событии ввода/ вывода, данная программа должна - в некоторый момент - выполнить столько операций ввода/ вывода, сколько она может (например, считать столько байт, сколько возможно) для текущего файлового дескриптора. Если данная программа получает отказ исполнения таких операций, тогда она может упустить имеющуюся возможность выполнить некоторые операции ввода/ вывода, поскольку не будет знать о необходимости работы с данным файловым дескриптором пока не возникнет другое событие ввода/ вывода. Мы говорим "в некоторый момент", так как иногда может оказаться нежелательным выполнять все имеющиеся операции ввода/ вывода немедленно после того, как мы определили что данный файловый дескриптор готов. Проблема состоит в том, что мы можем посадить на голодный паёк прочие требующие внимание файловые дескрипторы, если мы выполняем большое число операций ввода/ вывода с одним файловым дескриптором. Мы рассмотрим более подробно этот момент когда мы будем обсуждать модель управляемого фронтом уведомления для epoll в Разделе 63.4.6.

  • Если данная программа применяет некий цикл для выполнения такого числа операций ввода/ вывода, сколько их можно сделать для данного файлового дескриптора, а данный дескриптор помечен как блокируемый, тогда со временем некий системный вызов ввода/ вывода будет блокирован в случае, когда более нет возможных операций ввода/ вывода. По этой причине все отслеживаемый файловые дескрипторы обычно помещаются в неблокируемый режим, а после получения уведомления некоторого события ввода/ вывода, операции ввода/ вывода выполняются с повтором, пока соответствующий системный вызов (например, read() или write()) не получит отказ с определённой ошибкой EAGAIN или EWOULDBLOCK.

63.1.2 Применение неблокирующего ввода/ вывода в альтернативных моделях ввода/ вывода

Неблокируемый ввод/ вывод (с установленным флагом O_NONBLOCK) часто применяется совместно с той из моделей ввода/ вывода, которые описываются в данной главе. Некие примеры того, почему это может быть полезным приводятся ниже:

  • Как было пояснено в предыдущем разделе, неблокируемый ввод/ вывод обычно применяется совместно с моделями ввода/ вывода, которые предоставляют управляемые фронтом уведомления событий ввода/ вывоода.

  • Если с одними и теми же файловыми дескрипторами выполняет операции ввода/ вывода множество процессов (или потоков), тогда, с точки зрения определённого процесса, готовность некоторого дескриптора может измениться между временем когда данный дескриптор уведомил о готовности и моментом времени последующего ввода/ вывода. Следовательно, блокируемый вызов ввода/ вывода может быть блокирован, что не даёт возможности данному процессу отслеживать прочие файловые дескрипторы. (Это может произойти для всех тех моделей ввода/ вывода, которые мы обсуждаем в данной главе вне зависимости от того будет она реализовывать уведомление управляемое уровнем или фронтом.)

  • Даже после того как управляемые уровнем API, такие как select() или poll() проинформируют нас что некий файловый дескриптор для какого- то потокового сокета готов для записи, если мы записываем достаточно большой блок данных некоторым отдельным select() или poll(), тогда данный вызов будет заблокирован навсегда.

  • В некоторых случаях переключаемые уровнем API, такие как select() или poll(), могут возвращать фиктивные уведомления о готовности - они могут ложно информировать нас что некий файловый дескриптор готов. Это может быть вызвано некой ошибкой ядра или быть ожидаемым поведением в некотором необбычном сценарии.

    [Замечание]Замечание

    Раздел 16.6 [Stevens et al., 2004] описывает один из примеров неожиданных уведомлений в системах BSD для какого- то прослушиваемого сокета. Если некий клиент подключён к какому- то прослушиваемому сокету и затем сбрасывает это соединение, некий выполняемый данным сервером select() между такими двумя событиями будет указывать данный отслеживаемый сокет как находящийся в состоянии готовности, однако последующий accept(), который исполняется после данного сброса клиента будет блокирован.

63.2 Мультиплексирование ввода/ вывода

Мультиплексирование ввода/ вывода позволяет нам одновременно отслеживать множество файловых дескрипторов с тем, чтобы увидеть что в ком- то из них возможен ввод/ вывод. Мы можем выполнить мультиплексирование ввода/ вывода с применением с любым из двух системных вызовов с одной и той же функциональностью в конечном итоге. Первый из них, select(), появился в API для сокетов в BSD. Исторически он наиболее широко распространён из данных двух системных вызовов. Другой системный вызов, poll(), появился в System V. Оба системных вызова, и select(), и poll() в наши дни являются необходимыми согласно SUSv3.

Мы можем применять select() и poll() для отслеживания файловых дескрипторов обычных файлов, терминалов, псевдотерминалов, конвейеров, FIFO, сокетов и некоторых типов символьных устройств. Оба системных вызова позволяют некоторому процессу либо блокировать неограниченное ожидание пока не станут готовыми файловые дескрипторы, либо определить некий таймаут для данного вызова.

63.2.1 Системный вызов select()

Системный вызов 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(). Далее мы подробно опишем все эти аргументы.

[Замечание]Замечание

В показанном выше прототипе для select() нами включён <sys/time.h>, так как именно этот заголовок был определён в SUSv2 и некоторые реализации UNIX требуют этот заголовок. (Заголовок <sys/time.h> представлен в Linux и его включение не приносит вреда.)

 

Наборы файловых дескрипторов

Аргументы 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 имеют аналогичные значения для данного предела.)

[Замечание]Замечание

Даже несмотря на то, что макросы FD_* работают со структурами данных из пользовательского пространства имён, а реализация select() может обрабатывать наборы дескрипторов с большими размерами, glibc не предоставляет простого способа изменения определения FD_SETSIZE. Если мы желаем изменить данный предел, мы должны изменить само его определение в имеющихся файлах заголовка glibc. Однако, по описываемым позже в данной главе причинам, если нам необходимо отслеживать большое число дескрипторов, тогда, скорее всего, применение epoll окажется более предпочтительным, нежели использование select().

Аргументы 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- битное целое для типа time_t, самый верхний предел составляет многие года {Прим. пер.: более 136 лет.}.

Когда значение 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 системный вызов personality() для установки некоторой персонализации, которая включает бит персонализации STICKY_TIMEOUTS, тогда select() не изменяет ту структуру, на которую указывает timeout.

 

Возвращаемое 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 был возможен вывод.

63.2.2 Системный вызов poll()

Системный вызов 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, POLLMSG указывает, что некое содержащее сигнал SIGPOLL сообщение достигло заголовка данного потока. POLLMSG не применяется в Linux, так как Linux не реализует STREAMS.

Таблица 63.2: Значения битовой маски events и revents в структуре данных pollfd
Бит Входной в events? Возвращаемый revents? Описание

POLLIN

+

+

Могут быть считаны данные с приоритетом, отличающимся от высокого

POLLRDNORM

+

+

Эквивалентно POLLIN

POLLRDBAND

+

+

Могут быть считаны приоритетные данные (не применяется в Linux)

POLLPRI

+

+

Могут быть считаны данные с высоким приоритетом

POLLRDHUP

+

+

Отключить одноранговый сокет

POLLOUT

+

+

Могут быть записаны обычные данные

POLLWRNORM

+

+

Эквивалентно POLLOUT

POLLWRBAND

+

+

Могут быть записаны данные с высоким приоритетом

POLLERR

 

+

Произошла ошибка

POLLHUP

 

+

Произошло отсоединение

POLLNVAL

 

+

Файловый дескриптор не открыт

POLLMSG

 

 

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
 	   

63.2.3 Когда готов файловый дескриптор?

Правильное применение 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. Более того, в некоторых реализациях устанавливаемые флаги зависят от того будет ли отслеживаться данное устройство хозяин или подчинённый.

Таблица 63.3: Обозначения select() и poll() для терминалов и псевдотерминалов
Условие или событие select() poll()

Ввод доступен

r

POLLIN

Вывод возможен

w

POLLOUT

После close() одноранговым обменом псевдотерминала

rw

см. в тексте

Хозяин псевдотерминала в пакетном режиме обнаружил изменение зависимого состояния

x

POLLPRI

 

Конвейеры и 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().

Таблица 63.4: Обозначения select() и poll() для стороны чтения конвейера или FIFO
Условие или событие select() poll()
Данные в конвейере? Открыта ли сторона записи?

нет

нет

r

POLLHUP

да

да

r

POLLIN

да

нет

r

POLLIN | POLLHUP

Таблица 63.5: Обозначения select() и poll() для стороны записи конвейера или FIFO
Условие или событие select() poll()
Есть ли пространство для байтов PIPE_BUF? Открыта ли сторона чтения?

нет

нет

w

POLLERR

да

да

w

POLLOUT

да

нет

w

POLLOUT | POLLERR

 

Сокеты

Таблица 63.6 суммирует все поведения select() и poll() для сокетов. Для столбца poll() мы предполагаем, что events были определены как (POLLIN | POLLOUT | POLLPRI). Для колонки select() мы предполагаем, что данный файловый дескриптор был проверен на предмет того, возможен ли ввод, возможен ли вывод или не произошло ли исключительное состояние (т.е. что данный файловый дескриптор определён во всех трёх передаваемых select() наборах). Данная таблица покрывает только общие случаи, не все возможные сценарии.

[Замечание]Замечание

В Linux поведение poll() для домена UNIX после однораногового close() отличается от отображённого в Таблице 63.6. Помимо всех прочих флагов, poll() дополнительно возвращает в events POLLHUP.

Таблица 63.6: Обозначения select() и poll() для сокетов
Условие или событие select() poll()

Ввод доступен

r

POLLIN

Вывод возможен

w

POLLOUT

Установлено соединение на ввод в сокете ожидания

r

POLLIN

Приняты данные, выходящие за полосу пропускания (только для TCP)

x

POLLPRI

Закрытое соединение однорангового потокового сокета или исполнение shutdown(SHUT_WR)

rw

POLLIN | POLLOUT | POLLRDHUP

Особенный для Linux флаг POLLHUP (доступный начиная с Linux 2.6.17) требует некоторого дополнительного пояснения. Это флаг - в действительности в виде EPOLLRDHUP - спроектирован в первую очередь для применения с режимом переключения фронтом для API epoll Раздел 63.4. Он возвращается когда удалённая сторона соединения некоторого потокового сокета была остановлена для записывающей половины данного соединения. Применение данного флага позволяет некоторому использующему управляемый фронтом интерфейс epoll приложению применять более простой код для определения некоторого удалённого выключения. (Альтернатива состоит в том, что данное приложение помечает установку флага POLLIN, а затем выполняет некую операцию read(), которая указывает на отключение этой удалённой стороны возвратом 0.)

63.2.4 Сравнение select() и poll()

В данном разделе мы рассмотрим некоторые сходства и различия между 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() мы определяем только те файловые дескриптор, которые представляют для нас интерес, а наше ядро проверяет только эти дескрипторы.

[Замечание]Замечание

Имеющаяся разница в производительности для poll() и select() достаточно значительна в Linux 2.4. Некоторая оптимизация в Linux 2.6 значительно снизила этот зазор в производительности.

Мы дополнительно рассмотрим значения производительности select() и poll() в Разделе 63.4.5, в котором мы будем сравнивать производительность этих системных вызовов с epoll.

63.2.5 Проблемы с select() и poll()

Системные вызовы 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 предоставляют превосходную производительность при отслеживании больших чисел файловых дескрипторов.

63.3 Движимый сигналами ввод/ вывод

При вводе/ выводе с мультиплексированием некий процесс делает какой- то системный вызов (select() или poll()) чтобы проверить возможен ли ввод/ вывод для некоторого файлового дескриптора. Для управляемого сигналом ввода/ вывода некий процесс запрашивает чтобы само ядро отправляло ему какой- то сигнал когда ввод/ вывод возможен для какого- нибудь файлового дескриптора. Данный процесс может затем выполнять любые прочие действия пока не станет возможен ввод/ вывод, после чего определённый сигнал доставляется данному процессу. Для выполнения движимого сигналом ввода/ вывода некая программа формирует следующие этапы:

  1. Устанавливает некий обработчик для того сигнала, который доставляется управляемым сигналом механизмом ввода/ вывода. По умолчанию таким сигналом уведомления является SIGIO.

  2. Устанавливает владельца данного файловогодескриптора - именно этот процесс или группа процессов принимают сигналы когда для данного файлового дескриптора возможен ввод/ вывод. Обычно мы делаем владельцем вызывающий процесс. Такой владелец устанавливается при помощи операции F_SETOWN некоторого fcntl() в следующем виде:

    
    fcntl(fd, F_SETOWN, pid);
     	   
  3. Включает неблокируемый ввод/ вывод посредством установки флага состояния открытого файла O_NONBLOCK.

  4. Разрешает управляемый сигналом ввод/ вывод путём включения флага состояния открытого файла O_ASYNC. Это может быть объединено с предыдущим шагом, так как оба они требуют операции F_SETOWN определённого fcntl() (Раздел 5.3), как в приведённом ниже примере:

    
    flags = fcntl(fd, F_GETFL);                 /* Получить текущие флаги */
    fcntl(fd, F_SETFL, flags | O_ASYNC | O_NONBLOCK);
     	   
  5. Вызывающий процесс теперь может выполнять прочие задачи. Когда станет возможным ввод/ вывод, само ядро сгенерирует некий сигнал для данного процесса и исполнится тот обработчик сигнала, который был установлен на шаге 1.

  6. Управляемый сигналом ввод/ вывод предоставляет управляемое фронтом уведомление (Раздел 63.1.1). Это означает, что как только данный процесс получил уведомление о возможности ввода/ вывода, он должен выполнить такой объём операций ввода/ вывода, сколько возможно (например, считать столько байт, сколько сможет). В предположении некоторого неблокируемого файлового дескриптора это означает исполнение какого- то цикла, который исполняет системные вызовы ввода/ вывода пока не получит отказ вызова с конкретной ошибкой EAGAIN или EWOULDBLOCK.

В Linux 2.4 или более ранних версиях управляемый сигналом ввод/ вывод мог применяться с файловыми дескрипторами для сокетов, терминалов, псевдотерминалов и определённых типов устройств. Linux 2.6 дополнительно допускает управляемый сигналом ввод/ вывод применительно к конвейерам и FIFO. Начиная с Linux 2.6.25, движимый сигналом ввод/ вывод может также применяться для файловых дескрипторов inotify.

На последующих страницах мы вначале представим некий пример использования управляемого сигналом ввода/ вывода, а затем объясним более подробно некоторые приведённые выше этапы.

[Замечание]Замечание

Исторически управляемый сигналом ввод/ вывод иногда называется асинхронным вводом/ выводом, и это отражается в самом имени флага состояния открытого файла (O_ASYNC). Однако, в наши дни такой термин асинхронный ввод/ вывод применяется для именования определённого типа функциональности, предоставляемого имеющейся спецификацией AIO POSIX. Используя AIO POSIX, некий процесс запрашивает само ядро исполнять какую- то операцию ввода/ вывода, и само ядро инициирует данную операцию, однако немедленно передаёт обратно управление в сам вызывающий процесс; данный процесс затем позже получает уведомление о завершении данной операции ввода/ выводили некоторой произошедшей ошибке.

O_ASYNC был определён в POSIX.1g, однако не был включён в SUSv3, так как сама спецификация всего необходимого поведения для данного флага представлялась недостаточной.

Некоторые реализации UNIX, в особенности более ранние, не определяют данную константу O_ASYNC для применения с fcntl(). Вместо этого такая константа именуется FASYNC, а glibc позже определяет это имя как синоним для O_ASYNC.

 

Пример программы

Листинг 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 по умолчанию SIGIO игнорируется.

 

Настройка владельца конкретного файлового дескриптора

Мы устанавливаем владельца данного файлового дескриптора применяя некую операцию fcntl() в следующем виде:


fcntl(fd, F_SETOWN, pid);
 	   

Мы можем определить будет ли доставляться сигнал некому отдельному процессу или всем процессам в какой- то группе процессов когда возможен ввод/ вывод для данного файлового дескриптора. Если pid положителен, он воспринимается как идентификатор некого процесса. Если pid отрицателен, его абсолютное значение определяет идентификатор некоторой группы процессов.

[Замечание]Замечание

В более ранних реализациях UNIX для достижения того же самого эффекта, что и при F_SETOWN некая операция ioctl() - либо FIOSETOWN, либо SIOCSPGRP. С целью совместимости данные операции ioctl() также предоставляются в Linux.

Обычно pid определяется как идентификатор того процесса, который выполнил данный вызов (тем самым соответствующий сигнал отправляется тому процессу, который имеет открытым данный файловый дескриптор). Однако, имеется возможность определить другой процесс или некую группу процессов (например, соответствующую группу вызывающих процессов) и сигналы будут отправляться такому получателю с учётом тех проверок разрешений, которые описаны в Разделе 20.5, причём отправляющим процессом считается тот процесс, который выполняет F_SETOWN.

Операция F_GETOWN fcntl() возвращает определённый идентификатор того процессора или группы процессов, которые должны принимать сигналы когда в некотором определённом файловом дескрипторе возможен ввод/ вывод:


id = fcntl(fd, F_GETOWN);
if (id == -1)
    errExit("fcntl");
 	   

Данным вызовом идентификатор некоторой группы процессов возвращается как какое- то отрицательное значение.

[Замечание]Замечание

Той операцией ioctl(), которая соответствует F_GETOWN в более ранних реализациях UNIX это была FIOGETOWN или SIOCGPGRP. Обе эти операции ioctl() также представлены в Linux.

Некое ограничение о системном вызове, применяемом в некоторых архитектурах 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 glibc, функция оболочки fcntl() исправила эту проблему F_GETOWN в пространстве пользователя с применением операции F_GETOWN_EX (Раздел 63.3.2), что предоставляется Linux 2.6.32 и более поздними версиями.

63.3.1 Когда выставляется сигнал "ввод/ вывод возможен"?

Теперь мы более подробно рассмотрим когда выставляется сигнал "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.

63.3.2 Совершенствование применения движимого сигналами ввода/ вывода

В приложении, которое должно одновременно отслеживать очень большие числа (т.е. тысячи) файловых дескрипторов - например, определённые типы сетевых серверов - управляемый сигналом ввод/ вывод может предоставить значительные преимущества производительности в сравнении с 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.

Таблица 63.7: Значения si_code и si_band в структуре siginfo_t для событий с "возможным вводом/ выводом"
si_code Значение маскиsi_band Описание

POLL_IN

POLLIN | POLLRDNORM

Ввод возможен; условие конца-файла (eof)

POLL_OUT

POLLOUT | POLLWRNORM | POLLWRBAND

Вывод возможен

POLL_MSG

POLLIN | POLLRDNORM | POLLMSG

Доступно сообщение на входе (неиспользуемое)

POLL_ERR

POLLERR

Ошибка ввода/ вывода

POLL_PRI

POLLPRI | POLLRDNORM

Доступен ввод с высоким приоритетом

POLL_HUP

POLLHUP | POLLERR

Произошло отсоединение

В некотором приложении, котрое полностью управляется вводом, мы можем в последующем улучшить использование 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.

[Замечание]Замечание

Поскольку данные операции F_SETOWN_EX и F_GETOWN_EX представляют идентификаторы группы процессов как положительные значения, F_GETOWN_EX более не испытывает проблем описанных ранее для F_GETOWN, когдаиспользует идентификаторы групп процессов со значениями меньше чем 4096.

63.4 API epoll

Как и системные вызовы ввода/ вывода с мультиплексированием, и управляемый сигналами ввод/ вывод, 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.

63.4.1 Создание экземпляра epoll: epoll_create()

Системный вызов 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_create1(). Этот системный вызов вызывает исполнение той же самой задачи, что и epoll_create(), однако отбрасывает аргумент size и добавляет аргумент flags, который может применяться для изменения самого поведения данного системного вызова. В настоящее время поддерживается один флаг: EPOLL_CLOEXEC, который вызывает разрешение самому ядру флага закрытия-по-исполнению (FD_CLOEXEC) для данного нового файлового дескриптора. Данный флаг полезен по той же самой причине, что и описанный в разделе 4.3.1 флаг O_CLOEXEC для open().

63.4.2 Изменение списка участия epoll: epoll_ctl()

Системный вызов 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 файловый дескриптор требует небольшого объёма памяти ядра, не подлежащего сбросу в область подкачки, само ядро предоставляет некое взаимодействие, определяющее какой- то предел общего числа файловых дескрипторов, которое может зарегистрировать в списках epoll каждый пользователь. Само значение данного предела может быть просмотрено и изменено через max_user_watches, некий присущий Linux файл в каталоге /proc/sys/fs/epoll. Определяемое по умолчанию значение данного предела вычисляется на основе доступной системной памяти (см. страницу руководства epoll(7)).

63.4.3 Ожидание событий: epoll_wait()

Системный вызов 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().

Таблица 63.8: Значения битовой маски для поля epoll events
Бит Входной параметр epoll_ctl()? Возвращаемый epoll_wait() параметр? Описание

EPOLLIN

+

+

Могут считываться прочие данные помимо данных с высоким приоритетом

EPOLLPRI

+

+

Могут считываться данные с высоким приоритетом

EPOLLRDHUP

+

+

Останов однорангового сокета (начиная с Linux 2.6.17)

EPOLLOUT

+

+

Могут выполняться запись обычных данных

EPOLLET

+

 

Применяется переключаемое фронтом (edge-triggered) уведомление о событию

EPOLLONESHOT

+

 

Отключение мониторинга после получения уведомления о событии

EPOLLERR

 

+

Произошла ошибка

EPOLLHUP

 

+

Произошло отключение

 

Флаг 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
 	   

63.4.4 Более подробное рассмотрение семантики epoll

Сейчас мы рассмотрим некоторые тонкости взаимодействия открытых файлов, файловых дескрипторов и 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.4.5 Сопоставление производительности epoll и мультиплексирования

Таблица 63.9 отображает результаты (для Linux 2.6.25) когда отслеживается N подряд идущих файловых дескрипторов в диапазоне от 0 до N-1 с применением poll(), select() и epoll. (Данное тестирование было выравнено таким образом, что на протяжении каждой операции мониторинга в состоянии готовности пребывал в точности один выбранный случайным образом файловый дескриптор.) исходя из данной таблицы мы видим, что по мере роста общего числа подлежащих отслеживанию файловых дескрипторов, poll() и select() выполняются плохо. В противовес им незначительно снижается по мере возрастания N. (Отмеченное небольшое возрастание затрат по мере роста N, вероятно, является результатом достижения пределов кэширования ЦПУ в этой тестовой системе.)

[Замечание]Замечание

Для целей данного тестирования, FD_SETSIZE был изменён в заголовке glibc на 16'384 чтобы позволить данной программе тестирования выполнение наблюдения за большим числом файловых дескрипторов с применением select().

Таблица 63.9: Времена, затраченные poll(), select() и epoll на 100'000 операций мониторинга
Общее число отслеживаемых дескрипторов N Время ЦПУ poll() (секунды) Время ЦПУ select() (секунды) Время ЦПУ epoll (секунды)

10

0.61

0.73

0.41

100

2.9

3.0

0.42

1000

35

35

0.53

10000

990

930

0.66

В Разделе 63.2.5 мы видели почему poll() и select() плохо работают при отслеживании большого числа файловых дескрипторов. Теперь мы рассмотрим те причины, по которым epoll работает лучше:

  • При каждом вызове poll() или select() само ядро должно проверять все имеющиеся определёнными в данном вызове файловые дескрипторы. Напротив, когда мы помечаем некоторый дескриптор как подлежащий отслеживанию при помощи epoll_ctl(), наше ядро записывает этот факт в некотором списке, связанным с лежащими в основе открытыми файловыми дескрипторами и всякий раз, когда некая операция ввода/ вывода делает данный файловый дескриптор готовым к исполнению, наше ядро добавляет некий элемент в имеющийся перечень готовности для данного дескриптора epoll. (Некое событие ввода/ вывода на отдельном описании открытого файла может вызвать готовность множества связанных с этим описанием файловых дескрипторов.) Последующий вызов epoll_wait() просто осуществляет выборку из данного списка готовности.

  • Всякий раз когда мы вызываем poll() или select(), мы передаём своему ядру некую структуру данных, которая указывает все имеющиеся подлежащие мониторингу файловые дескрипторы и, наоборот, наше ядро возвращает обратно некую структуру данных, описывающую готовность всех этих дескрипторов. В противоположность этому, при использовании epoll, мы применяем epoll_ctl() для построения некоторой структуры данных в пространстве ядра, которая перечисляет всё множество подлежащих мониторингу файловых дескрипторов, а сам вызов epoll_wait() возвращает информацию только о тех дескрипторах, которые являются готовыми.

[Замечание]Замечание

Помимо всего указанного выше, в случае с select(), мы должны инициализировать все входные структуры данных для каждого вызова, причём и для select(), и для poll() мы обязаны проверять все возвращённые структуры данных для определения того, какие из возвращённых N файловых дескрипторов являются готовыми. Однако, некоторые тесты показывают, что то время, которое необходимо для таких прочих шагов было незначительным в сопоставлении с тем временем, которое требуется системным вызовам для отслеживания N дескрипторов. Таблица 63.9 не содержит значений времени для таких шагов инспектирования.

Очень схематично мы можем сказать, что для больших значений N (общего число подлежащих отслеживанию файловых дескрипторов), значение производительности select() и poll() линейно масштабируется со значением N. Мы начали наблюдать такое поведение для случаев N = 100 и N = 1000 в Таблице 63.9. Со временем мы достигли N = 10000, причём масштабирование в действительности оказалось хуже линейного.

Напротив, epoll масштабируется (линейно) в соответствии с общим числом имеющих место событий ввода/ вывода. API epoll таким образом, в особенности эффективен для некоторого сценария, который является распространённым в серверах, которые обрабатывают одновременно большое число клиентов: когда при подлежащих отслеживанию больших значений файловых дескрипторов большинство простаивают; готовыми является лишь небольшое число.

63.4.6 Переключаемые фронтом уведомления

По умолчанию механизм 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) и происходят следующие этапы:

  1. В данном сокете возникает ввод.

  2. Мы исполняем некий epoll_wait(). Данный вызов сообщает нам, что данный сокет готов вне зависимости от того, применили ли мы уведомление управляемое уровнем или фронтом.

  3. Мы выполняем некий повторный вызов epoll_wait().

Если мы применяем управляемое уровнем уведомление, тогда второй вызов epoll_wait() проинформирует нас, что данный сокет готов. Если же мы применяем управляемое фронтом уведомление, тогда второй вызов epoll_wait() букет заблокирован, так как никакого нового ввода не возникло с момента предыдущего вызова epoll_wait().

Как мы уже упоминали в Разделе 63.1.1, переключаемое фронтом уведомление обычно применяется совместно с неблокируемыми файловыми дескрипторами. Таким образом, общая структура для использования управляемого фронтом уведомления epoll следующая:

  1. Сделать все подлежащие мониторингу файловые дескрипторы неблокируемыми.

  2. С помощью epoll_ctl() построить необходимый список интереса epoll.

  3. Обрабатывать события ввода/ вывода с применением следующего цикла:

    1. При помощи epoll_wait() выбрать список готовых дескрипторов.

    2. Для каждого готового файлового дескриптора обрабатывать ввод/ вывод пока соответствующий системный вызов (например, read(), write(), recv(), send() или accept()) не будет возвращён с ошибкой EAGAIN или EWOULDBLOCK.

 

Предотвращение неопределённого блокирования файловых дескрипторов при применении управляемого фронтом уведомления

Предположим, что мы отслеживаем множество файловых дескрипторов применяя управляемые фронтом уведомления и что некий готовый файловый дескриптор имеет достаточно большой объём (возможно, некий бесконечный поток) доступного входа. Если, после определения того, что такой файловый дескриптор готов, мы пытаемся принять весь имеющийся ввод с применением неблокируемых чтений, тогда мы рискуем посадить на голодный паёк (выполнить неопределённое блокирование) прочие требующие внимания файловые дескрипторы (т.е. может пройти длительный промежуток времени пока мы снова проверим их готовность и выполним в них ввод/ вывод). Одно из решений данной проблемы состоит в том, что данное приложение сопровождает некий список получивших уведомление о готовности файловых дескрипторов и выполняет некий цикл, который непрерывно выполняет следующие действия:

  1. Отслеживает необходимые файловые дескрипторы с применением epoll_wait() и добавляет готовые дескрипторы в имеющийся список приложения. Если какой- либо дескриптор уже зарегистрирован готовым в данном списке приложения, тогда значение таймаута для этого шага мониторинга должно быть малым или нулевым с тем, чтобы когда нет готовых новых дескрипторов, данное приложение могло быстро проследовать к следующему этапу и и обслужить все файловые дескрипторы, которые уже известны как готовые.

  2. Выполнить ограниченный объём ввода/ вывода на тех зарегистрированных файловых дескрипторах, которые готовы в имеющемся перечне приложения (возможно, выполняя их циклический обход карусельным - round-robin - образом, вместо того чтобы начинать с самого начала имеющегося списка после каждого вызова epoll_wait()). Некий файловый дескриптор может быть удалён из данного перечня приложения в случае отказа соответствующего неблокируемого системного вызова ввода/ вывода или при ошибке EAGAIN либо EWOULDBLOCK.

Хотя это требует дополнительной работы по программированию, такой подход предлагает прочие преимущества помимо предотвращения режима голода файловых дескрипторов. Например, мы можем в приведённый выше цикл дополнительные шаги, такие как обработка таймеров и приём сигналов с помощью sigwaitinfo() (или аналогичных вызовов).

Необходимо также уделять внимание неопределённому блокированию при использовании управляемого сигналами ввода/ вывода, так как он также представляет некий механизм управляемого фронтом уведомления. Напротив, рассмотрение режима голодания не требуется в приложениях, использующих механизм уведомления с применением переключения уровнем. Это обусловлено тем, что мы можем применять при управляемом уровнем уведомлении файловых дескрипторов с блокированием и использовать цикл, который непрерывно проверяет дескрипторы на предмет готовности, а затем выполняет некий ввод/ вывод для готовых дескрипторов прежде чем снова проверит готовность файловых дескрипторов.

63.5 Ожидание сигналов и файловых дескрипторов

Порой некому процессу необходимо одновременно дождаться ввода/ вывода чтобы получить возможность на одном из наборов файловых дескрипторов или доставки некого сигнала. Мы можем попробовать выполнить такую операцию с помощью 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 механизм signalfd. Применяя данный механизм, мы можем принимать сигналы через некоторый файловый дескриптор, который отслеживается (помимо прочих файловых дескрипторов) с применением select(), poll() или epoll_wait().

63.5.1 Системный вызов pselect()

Системный вызов 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.

[Замечание]Замечание

Первоначально библиотечная функция pselect() была представлена glibc, однако эта реализация не предоставляла автоматических гарантий для правильного выполнения операций данного вызова. Такие гарантии могут быть представлены только некоторой реализацией ядра pselect().

 

Листинг 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) (рус.яз.)

63.5.2 Хитрость конвейеризации самого себя

Так как pselect() не имеет широко распространённой реализации, переносимые приложения должны придерживаться других стратегий избежания условий соперничества при одновременном ожидании сигналов и вызова select() на некотором множестве файловых дескрипторов. Одно общее решение состоит в следующем:

  1. Создать некий конвейер и пометить его стороны чтения и записи как неблокируемые.

  2. Помимо всех прочих представляющих интерес файловых дескрипторов, включите сторону считывания данного конвейера в свой набор readfds, относящийся к select().

  3. Установите некий обработчик для того сигнала, который представляет интерес. Когда этот обработчик сигнала вызывается, он записывает некие байтовые данные в данный конвейер. Относительно данного обработчика отметим следующие моменты:

    • Та сторона записи данного конвейера, которая была отмечена как неблокируемая на самом первом шаге для предотвращения той возможности, сигналы возникают настолько быстро, что повторяющиеся вызовы данного обработчика сигналов заполняют весь конвейер, что имеет результатом то, что данный обработчик сигнала

      write() (а таким образом и сам процесс) блокируется. (Если произойдет отказ записи в некий заполненный конвейер, это не будет иметь значения, так как все предыдущие записи уже указали доставку данного сигнала.)

    • Данный обработчик сигнала установлен после создания рассматриваемого конвейера с тем, чтобы предотвратить такое условие состязательности которое может возникнуть если некий сигнал был доставлен до создания данного конвейера.

    • Внутри данного обработчика сигнала применение write() является безопасным, так как это одна из функций async-signal-safe (безопасного асинхронного сигнала), перечисленных в Таблице 21-1

  4. Поместите вызов select() в некий цикл с тем, чтобы он перезапускался если выполняется прерывание неким обработчиком сигнала. (Предоставленный таким образом перезапуск строго говоря не обязателен; он просто означает, что мы можем проверить возникновение некоторого сигнала путём инспектирования readfds вместо того чтобы проверять наличие некоторой возвращаемой ошибки EINTR.)

  5. В случае успешного завершения данного вызова select(), мы можем определить возник ли данный сигнал проверив установлен ли тот файловый дескриптор, который относится к нашей стороне считывания данного конвейера в readfds.

  6. Всякий раз когда мы получаем некий сигнал, считываем все имеющиеся в данном конвейере байты. Так как может возникнуть множество сигналов, примените некий цикл, который считывает байты пока не будет получен отказ данного (неблокируемого) 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
 	   

63.6 Выводы

В данной главе мы исследовали различные альтернативы стандартным моделям выполнения ввода/ вывода: мультиплексированный ввод/ вывод (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] это статья, сопоставляющая производительность select(), poll() и epoll.

Особый интерес представляет интернет ресурс с адресом http://www.kegel.com/c10k.html. Написанная Деном Кегелом и имеющее название "Проблема C10K", данная веб страница исследует ту проблему, с которой сталкиваются разработчики веб серверов, проектируемые для одновременного обслуживания десятков тысяч клиентов. Эта веб страница содержит множество ссылок на соответствующую информацию.

63.7 Упражнения

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 данной структуры.