Термины: Ввод-вывод данных синхронный и асинхронный. Синхронный и асинхронный ввод-вывод Что такое асинхронный ввод вывод

Как известно, имеются два основных режима ввода/вывода: режим обмена с опросом готовности устройства ввода/вывода и режим обмена с прерываниями.

В режиме обмена с опросом готовности управление вводом/выводом осуществляет центральный процессор. Центральный процессор посылает устройству управления команду выполнить некоторое действие устройству ввода/вывода. Последнее исполняет команду, транслируя сигналы, понятные центральному устройству и устройству управления в сигналы, понятные устройству ввода/вывода. Но быстродействие устройства ввода/вывода намного меньше быстродействия центрального процессора. Поэтому сигнал готовности приходится очень долго ожидать, постоянно опрашивая соответствующую линию интерфейса на наличие или отсутствие нужного сигнала. Посылать новую команду, не дождавшись сигнала готовности, сообщающего об исполнении предыдущей команды, бессмысленно. В режиме опроса готовности драйвер, управляющий процессом обмена данными с внешним устройством, как раз и выполняет в цикле команду «проверить наличие сигнала готовности». До тех пор пока сигнал готовности не появится, драйвер ничего другого не делает. При этом, естественно, нерационально используется время центрального процессора. Гораздо выгоднее, выдав команду ввода/ вывода, на время забыть об устройстве ввода/вывода и перейти на выполнение другой программы. А появление сигнала готовности трактовать как запрос на прерывание от устройства ввода/вывода. Именно эти сигналы готовности и являются сигналами запроса на прерывание.

Режим обмена с прерываниями по своей сути является режимом асинхронного управления. Для того чтобы не потерять связь с устройством может быть запущен отсчет времени, в течение которого устройство обязательно должно выполнить команду и выдать сигнал запроса на прерывание. Максимальный интервал времени, и течение которого устройство ввода/вывода или его контроллер должны выдать сигнал запроса на прерывание, часто называют уставной тайм-аута. Если это время истекло после выдачи устройству очередной команды, а устройство так и не ответило, то делается вывод о том, что связь с устройством потеряна и управлять им больше нет возможности. Пользователь и/или задача получают соответствующее диагностическое сообщение.

Рис. 4.1. Управление вводом/выводом

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

Секция запуска инициирует операцию ввода/вывода. Эта секция запускается для включения устройства ввода/вывода либо просто для инициации очередной операции ввода/вывода.

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

Секция завершения обычно выключает устройство ввода/вывода либо просто завершает операцию.

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

Рис. 7.1. Два режима выполнения операций ввода-вывода

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

Задача, выдавшая запрос на операцию ввода/вывода, переводится супервизором в состояние ожидания завершения заказанной операции. Когда супервизор получает от секции завершения сообщение о том, что операция завершилась, он переводит задачу в состояние готовности к выполнению, и она продолжает свою работу. Эта ситуация соответствует синхронному вводу/выводу. Синхронный ввод/вывод является стандартным для большинства ОС. Чтобы увеличить ско­рость выполнения приложений, было предложено при необходимости использо­вать асинхронный ввод/вывод.

Простейшим вариантом асинхронного вывода является так называемый буфери­рованный вывод данных на внешнее устройство, при котором данные из приложения передаются не непосредственно на устройство ввода/вывода, а в специальный системный буфер. В этом случае логически операция вывода для приложения считается выполненной сразу же, и задача может не ожидать окон­чания действительного процесса передачи данных на устройство. Процессом реального вывода данных из системного буфера занимается супервизор ввода/ вывода. Естественно, что выделением буфера из системной области памяти за­нимается специальный системный процесс по указанию супервизора ввода/вы­вода. Итак, для рассмотренного случая вывод будет асинхронным, если, во-пер­вых, в запросе на ввод/вывод было указано на необходимость буферирования данных, а во-вторых, если устройство ввода/вывода допускает такие асинхрон­ные операции и это отмечено в UCB. Можно организовать и асинхронный ввод данных. Однако для этого необходи­мо не только выделить область памяти для временного хранения считываемых с устройства данных и связать выделенный буфер с задачей, заказавшей опера­цию, но и сам запрос на операцию ввода/вывода разбить на две части (на два за­проса). В первом запросе указывается операция на считывание данных, подобно тому, как это делается при синхронном вводе/выводе. Однако тип (код) запроса используется другой, и в запросе указывается ещё по крайней мере один допол­нительный параметр – имя (код) того системного объекта, которое получает за­дача в ответ на запрос и которое идентифицирует выделенный буфер. Получив имя буфера (будем этот системный объект условно называть таким образом, хотя в различных ОС для его обозначения используются и другие термины, на­пример – класс), задача продолжает свою работу. Здесь очень важно подчерк­нуть, что в результате запроса на асинхронный ввод данных задача не переводится супервизором ввода/вывода в состояние ожидания завершения операции ввода/ вывода, а остается в состоянии выполнения или в состоянии готовности к вы­полнению. Через некоторое время, выполнив необходимый код, который был оп­ределен программистом, задача выдаёт второй запрос на завершение операции ввода/вывода. В этом втором запросе к тому же устройству, который, естествен­но, имеет другой код (или имя запроса), задача указывает имя системного объек­та (буфера для асинхронного ввода данных) и в случае успешного завершения операции считывания данных тут же получает их из системного буфера. Если же данные ещё не успели до конца переписаться с внешнего устройства в систем­ный буфер, супервизор ввода/вывода переводит задачу в состояние ожидания завершения операции ввода/вывода, и далее всё напоминает обычный синхрон­ный ввод данных.

Обычно асинхронный ввод/вывод предоставляется в большинстве мультипрограммных ОС, особенно если ОС поддерживает мультизадачность с помощью механизма тредов. Однако если асинхронный ввод/вывод в явном виде отсутст­вует, его идеи можно реализовать самому, организовав для вывода данных самостоятельный поток.

Аппаратуру ввода/вывода можно рассматривать как совокупность аппаратурных процессоров , которые способны работать параллельно относительно друг друга, а также относительно центрального процессора (процессоров). На таких «процессорах» выполняются так называемыевнешние процессы. Например, для внешнего устройства (устройства ввода/вывода) внешний процесс может представлять собой совокупность операций, обеспечивающих перевод печатающей головки, продвижение бумаги на одну позицию, смену цвета чернил или печать каких-то символов. Внешние процессы, используя аппаратуру ввода/вывода, взаимодействуют как между собой, так и с обычными «программными» процессами, выполняющимися на центральном процессоре. Важным при этом является то обстоятельство, что скорости выполнения внешних процессов будут существенно (порой, на порядок или больше) отличаться от скорости выполнения обычных («внутренних ») процессов. Для своей нормальной работы внешние и внутрен­ние процессы обязательно должны синхронизироваться. Для сглаживания эф­фекта сильного несоответствия скоростей между внутренними и внешними процессами используют упомянутое выше буферирование. Таким образом, можно говорить о системе параллельных взаимодействующих процессов (см. главу 6).

Буферы являются критическим ресурсом в отношении внутренних (программных) и внешних процессов, которые при параллельном своем развитии информационно взаимодействуют. Через буфер (буферы) данные либо посылаются от некоторого процесса к адресуемому внешнему (операция вывода данных на внешнее устройство), либо от внешнего процесса передаются некоторому программному процессу (операция считывания данных). Введение буферирования как средства информационного взаимодействия выдвигает проблему управле­ния этими системными буферами, которая решается средствами супервизорной части ОС. При этом на супервизор возлагаются задачи не только по выделению и освобождению буферов в системной области памяти, но и синхронизации про­цессов в соответствии с состоянием операций по заполнению или освобождению буферов, а также их ожидания, если свободных буферов в наличии нет, а запрос на ввод/вывод требует буферирования. Обычно супервизор ввода/вывода для решения перечисленных задач использует стандартные средства синхронизации, принятые в данной ОС. Поэтому если ОС имеет развитые средства для решения проблем параллельного выполнения взаимодействующих приложений и задач, то, как правило, она реализует и асинхронный ввод/вывод.

Операциям ввода и вывода присуща более медленная скорость выполнения по сравнению с другими видами обработки. Причиной такого замедления являются следующие факторы:

Задержки, обусловленные затратами времени на поиск нужных дорожек и секторов на устройствах произвольного доступа (диски, компакт-диски).

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

Задержки при передаче данных по сети с использованием файловых, серверов, хранилищ данных и так далее.

Во всех предыдущих примерах операции ввода/вывода выполняются синхронно с потоком, поэтому весь поток вынужден простаивать, пока они не завершатся.

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

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

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

Обзор методов асинхронного ввода/вывода Windows

В Windows выполнение асинхронного ввода/вывода обеспечивается в соответствии с тремя методиками.

Многопоточный ввод/вывод (Multihreaded I/O). Каждый из потоков внутри процесса или набора процессов выполняет обычный синхронный ввод/вывод, но при этом другие потоки могут продолжать свое выполнение.

Перекрывающийся ввод/вывод (Overlapped I/O). Запустив операцию чтения, записи или иную операцию ввода/вывода, поток продолжает свое выполнение. Если потоку для продолжения выполнения требуются результаты ввода/вывода, он ожидает, пока не станет доступным соответствующий дескриптор или не наступит заданное событие. В Windows 9x перекрывающийся ввод/вывод поддерживается только для последовательных устройств, например именованных каналов.

Процедуры завершения (или расширенный ввод/вывод) (Completion routines (extended I/O)). Когда наступает завершение операций ввода/вывода, система вызывает специальную процедуру завершения, выполняющуюся внутри потока. Расширенный ввод/вывод для дисковых файлов в Windows 9x не поддерживается.

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

Перекрывающийся ввод/вывод является предметом рассмотрения следующего раздела, а в приведенных в нем примерах, реализующих преобразование файлов (из ASCII в UNICODE), эта методика применена для иллюстрации возможностей последовательной обработки файлов. С этой целью используется видоизмененный вариант программы 2.4. Вслед за перекрывающимся вводом/выводом рассматривается расширенный ввод/вывод, использующий процедуры завершения.

Примечание

Методы перекрывающегося и расширенного ввода/вывода часто оказываются сложными в реализации, редко обеспечивают какие-либо преимущества в отношении производительности, иногда даже становясь причиной ее ухудшения, а в случае файлового ввода/вывода способны работать лишь под управлением Windows NT. Эти проблемы преодолеваются с помощью потоков, поэтому, вероятно, многие читатели захотят сразу же перейти к разделам, посвященным таймерам ожидания и портам завершения ввода/вывода, возвращаясь к этому разделу по мере необходимости. С другой стороны, элементы асинхронного ввода/вывода присутствуют как в устаревших, так и в новых технологиях, в связи с чем эти методы все-таки стоит изучить.

Так, технология СОМ на платформе NT5 поддерживает асинхронный вызов методов, поэтому указанная методика может пригодиться многим читателям, которые используют или собираются использовать технологию СОМ. Кроме того, много общего с расширенным вводом/выводом имеют операции асинхронного вызова процедур (глава 10), и хотя лично я предпочитаю использовать потоки, другие могут отдать предпочтение именно этому механизму.

Перекрывающийся ввод/вывод

Первое, что необходимо сделать для организации асинхронного ввода/вывода, будь то перекрывающегося или расширенного, - это установить атрибут перекрывания (overlapped attribute) для файлового или иного дескриптора. Для этого при вызове CreateFile или иной функции, в результате которого создается файл, именованный канал или иной дескриптор, следует указать флаг FILE_FLAG_OVERLAPPED.

В случае сокетов (глава 12), независимо от того, были они созданы с использованием функции socket или accept, атрибут перекрывания устанавливается по умолчанию в Winsock 1.1, но должен устанавливаться явным образом в Winsock 2.0. Перекрывающиеся сокеты могут использоваться в асинхронном режиме во всех версиях Windows.

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

Вспомните, что при указании флага FILE_FLAG_OVERLAPPED в составе параметра dwAttrsAndFlags (в случае функции CreateFile) или параметра dwOpen-Mode (в случае функции CreateNamedPipe) соответствующие файл или канал могут использоваться только в режиме перекрывания. С анонимными каналами перекрывающийся ввод/вывод не работает.

Примечание

В документации по функции CreateFile есть упоминание о том, что использование флага FILE_FLAG_NO_BUFFERING улучшает характеристики быстродействия перекрывающегося ввода/вывода. Эксперименты показывают лишь незначительное повышение производительности (примерно на 15%, что может быть проверено путем экспериментирования с программой 14.1), но вы должны убедиться в том, что суммарный размер считываемых данных при выполнении операций ReadFile или WriteFile, кратен размеру сектора диска.

Перекрывающиеся сокеты

Одним из наиболее важных нововведений в Windows Sockets 2.0 (глава 12) является стандартизация перекрывающегося ввода/вывода. В частности, сокеты уже не создаются автоматически как дескрипторы файлов с перекрытием. Функция socket создает неперекрывающийся дескриптор. Чтобы создать перекрывающийся сокет, следует вызвать функцию WSASocket, явно запросив создание перекрывающегося совета путем указания значения WSA_FLAG_OVERLAPPED для параметра dwFlags функции WSASocket.

SOCKET WSAAPI WSASocket(int iAddressFamily, int iSocketType, int iProtocol, LPWSAPROTOCOL_INFO lpProtocolInfo, GROUP g, DWORD dwFlags);

Для создания сокета используйте вместо функции socket функцию WSASocket. Любой сокет, возвращенный функцией accept, будет иметь те же свойства, что и аргумент.

Следствия применения перекрывающегося ввода/вывода

Перекрывающийся ввод/вывод выполняется в асинхронном режиме. Это имеет несколько следствий.

Операции перекрывающегося ввода/вывода не блокируются. Функции ReadFile, WriteFile, TransactNamedPipe и ConnectNamedPipe осуществляют возврат, не дожидаясь завершения операции ввода/вывода.

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

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

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

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

Для преодоления двух последних из перечисленных выше трудностей используются структуры OVERLAPPED.

Структуры OVERLAPPED

С помощью структуры OVERLAPPED (указываемой, например, параметром lpOverlapped функции ReadFile) можно указывать следующую информацию:

Позицию в файле (64 бита), с которой должно начинаться выполнение операции чтения или записи в соответствии с обсуждением, которое содержится в главе 3.

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

Ниже приводится определение структуры OVERLAPPED.

Для задания позиции в файле (указателя) должны использоваться оба поля Offset и OffsetHigh, хотя старшая часть указателя (OffsetHigh) во многих случаях равна 0. Не следует использовать поля Internal и InternalHigh, зарезервированные для системных нужд.

Параметр hEvent - дескриптор события (созданного посредством функции CreateEvent). Это событие может быть как именованным, так и неименованным, но оно должно быть обязательно сбрасываемым вручную (см. главу 8), если используется для перекрывающегося ввода/вывода; причины этого будут вскоре объяснены. По завершении операции ввода/вывода событие переходит в сигнальное состояние.

В другом возможном варианте его использования дескриптор hEvent имеет значение NULL; в этом случае программа может ожидать перехода в сигнальное состояние дескриптора файла, который также может выступать в роли объекта синхронизации (см. приведенные далее предостережения). Система использует для отслеживания завершения операций сигнальные состояния дескриптора файла, если дескриптор hEvent равен NULL, то есть объектом синхронизации в этом случае является дескриптор файла.

Примечание

В целях удобства термин "дескриптор файла" ("file handle"), используемый по отношению к дескрипторам, указываемым при вызове функций ReadFile, WriteFile и так далее, будет применяться нами даже в тех случаях, когда речь идет о дескрипторах именованного канала или устройства, а не файла.

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

Даже если дескриптор файла является синхронным (то есть созданным без флага FILE_FLAG_OVERLAPPED), структура OVERLAPPED может послужить в качестве альтернативы функции SetFilePointer для указания позиции в файле. В этом случае возврат после вызова функции ReadFile или иного вызова не происходит до тех пор, операция ввода/вывода пока не завершится. Этой возможностью мы уже воспользовались в главе 3. Также обратите внимание на то, что незавершенные операции ввода/вывода однозначно идентифицируются комбинацией дескриптора файла и соответствующей структуры OVERLAPPED.

Ниже перечислены некоторые предостережения, которые следует принимать во внимание.

Не допускайте повторного использования структуры OVERLAPPED в то время, когда связанная с ней операция ввода/вывода, если таковая имеется, еще не успела завершиться.

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

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

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

Состояния перекрывающегося ввода/вывода

Возврат из функций ReadFile и WriteFile, а также двух указанных выше функций, относящихся к именованным каналам, в случаях, когда они используются для выполнения перекрывающихся операций ввода вывода, осуществляется немедленно. В большинстве случаев операция ввода/вывода к этому моменту завершена не будет, и возвращаемым значением при чтении и записи будет FALSE. Функция GetLastError возвратит в этой ситуации значение ERROR_IO_PENDING.

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

BOOL GetOverlappedResult(HANDLE hFile, LPOVERLAPPED lpOverlapped, LPWORD lpcbTransfer, BOOL bWait)

Указание конкретной операции ввода/вывода обеспечивается сочетанием дескриптора и структуры OVERLAPPED. Значение TRUE параметра bWait указывает на то, что до завершения операции функция GetOverlappedResult должна находиться в состоянии ожидания; в противном случае возврат из функции должен быть немедленным. В любом случае эта функция будет возвращать значение TRUE только после успешного завершения операции. Если возвращаемым значением функции GetOverlappedResult является FALSE, то функция GetLastError возвратит значение ERROR_IO_INCOMPLETE, что позволяет вызывать эту функцию для опроса завершения ввода/вывода.

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

Отмена выполнения операций перекрывающегося ввода/вывода

Булевская функция CancelIO позволяет отменить выполнение незавершенных операций перекрывающегося ввода/вывода, связанных с указанным дескриптором (у этой функции имеется всего лишь один параметр). Отменяется выполнение всех инициированных вызывающим потоком операций, использующих данный дескриптор. На операции, инициированные другими потоками, вызов этой функции никакого влияния не оказывает. Отмененные операции завершаются С ошибкой ERROR OPERATION ABORTED.

Пример: использование дескриптора файла в качестве объекта синхронизации

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

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

OVERLAPPED ov = { 0, 0, 0, 0, NULL /* События не используются. */ };
hF = CreateFile(…, FILE_FLAG_OVERLAPPED, …);
ReadFile(hF, Buffer, sizeof(Buffer), &nRead, &ov);
/* Выполнение других видов обработки. nRead не обязательно достоверно.*/
/* Ожидать завершения операции чтения. */
WaitForSingleObject(hF, INFINITE);
GetOverlappedResult(hF, &ov, &nRead, FALSE);

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

Программа 2.4 (atou) осуществляла преобразование ASCII-файла к кодировке UNICODE путем последовательной обработки файла, а в главе 5 было показано, как выполнить такую же последовательную обработку с помощью отображения файлов. В программе 14.1 (atouOV) та же самая задача решается с использованием перекрывающегося ввода/вывода и множественных буферов, в которых хранятся записи фиксированного размера.

Рисунок 14.1 иллюстрирует организацию программы с четырьмя буферами фиксированного размера. Программа реализована таким образом, чтобы количество буферов можно было определять при помощи символической константы препроцессора, но в нижеследующем обсуждении мы будем предполагать, что существуют четыре буфера.

Сначала в программе выполняется инициализация всех элементов структур OVERLAPPED, определяющих события и позиции в файлах. Для каждого входного и выходного буферов предусмотрена отдельная структура OVERLAPPED. После этого для каждого из входных буферов инициируется операция перекрывающегося чтения. Далее с помощью функции WaitForMultipleObjects в программе организуется ожидание одиночного события, указывающего на завершение чтения или записи. При завершении операции чтения входной буфер копируется и преобразуется в соответствующий выходной буфер, после чего инициируется операция записи. При завершении записи инициируется следующая операция чтения. Заметьте, что события, связанные с входными и выходными буферами размещаются в единственном массиве, который используется в качестве аргумента при вызове функции WaitForMultipleObjects.

Рис. 14.1. Модель асинхронного обновления файла


Программа 14.1. atouOV: преобразование файла с использованием перекрывающегося ввода/вывода
Преобразование файла из кодировки ASCII в кодировку Unicode с использованием перекрывающегося ввода/вывода. Программа работает только в Windows NT. */

#define MAX_OVRLP 4 /* Количество перекрывающихся операций ввода/вывода.*/
#define REC_SIZE 0x8000 /* 32 Кбайт: Минимальный размер записи, обеспечивающий приемлемую производительность. */

/* Каждый из элементов определенных ниже массивов переменных */
/* и структур соответствует отдельной незавершенной операции */
/* перекрывающегося ввода/вывода. */
DWORD nin, nout, ic, i;
OVERLAPPED OverLapIn, OverLapOut;
/* Необходимость использования сплошного, двумерного массива */
/* диктуется Функцией WaitForMultipleObjects. */
/* Значение 0 первого индекса соответствует чтению, значение 1 – записи.*/
/* В каждом из определенных ниже двух буферных массивов первый индекс */
/* нумерует операции ввода/вывода. */
LARGE_INTEGER CurPosIn, CurPosOut, FileSize;
/* Общее количество записей, подлежащих обработке, вычисляемое */
/* на основе размера входного файла. Запись, находящаяся в конце, */
/* может быть неполной. */
for (ic = 0; ic < MAX_OVRLP; ic++) {
/* Создать события чтения и записи для каждой структуры OVERLAPPED.*/
hEvents = OverLapIn.hEvent /* Событие чтения.*/
hEvents = OverLapOut.hEvent /* Событие записи. */
= CreateEvent(NULL, TRUE, FALSE, NULL);
/* Начальные позиции в файле для каждой структуры OVERLAPPED. */
/* Инициировать перекрывающуюся операцию чтения для данной структуры OVERLAPPED. */
if (CurPosIn.QuadPart < FileSize.QuadPart) ReadFile(hInputFile, AsRec, REC_SIZE, &nin, &OverLapIn);
/* Выполняются все операции чтения. Ожидать завершения события и сразу же сбросить его. События чтения и записи хранятся в массиве событий рядом друг с другом. */
iWaits =0; /* Количество выполненных к данному моменту операций ввода/вывода. */
while (iWaits < 2 * nRecord) {
ic = WaitForMultipleObjects(2 * MAX_OVRLP, hEvents, FALSE, INFINITE) – WAIT_OBJECT_0;
iWaits++; /* Инкрементировать счетчик выполненных операций ввода вывода.*/
ResetEvent(hEvents);
/* Чтение завершено. */
GetOverlappedResult(hInputFile, &OverLapIn, &nin, FALSE);
for (i =0; i < REC_SIZE; i++) UnRec[i] = AsRec[i];
WriteFile(hOutputFile, UnRec, nin * 2, &nout, &OverLapOut);
/* Подготовиться к очередному чтению, которое будет инициировано после того, как завершится начатая выше операция записи. */
OverLapIn.Offset = CurPosIn.LowPart;
OverLapIn.OffsetHigh = CurPosIn.HighPart;
} else if (ic < 2 * MAX_OVRLP) { /* Операция записи завершилась. */
/* Начать чтение. */
ic –= MAX_OVRLP; /* Установить индекс выходного буфера. */
if (!GetOverlappedResult (hOutputFile, &OverLapOut, &nout, FALSE)) ReportError(_T("Ошибка чтения."), 0, TRUE);
CurPosIn.LowPart = OverLapIn.Offset;
CurPosIn.HighPart = OverLapIn.OffsetHigh;
if (CurPosIn.QuadPart < FileSize.QuadPart) {
/* Начать новую операцию чтения. */
ReadFile(hInputFile, AsRec, REC_SIZE, &nin, &OverLapIn);
/* Закрыть все события. */
for (ic = 0; ic < MAX_OVRLP; ic++) {

Программа 14.1 способна работать только под управлением Windows NT. Средства асинхронного ввода/вывода Windows 9x не позволяют использовать дисковые файлы. В приложении В приведены результаты и комментарии, свидетельствующие о сравнительно низкой производительности программы atouOV. Как показали эксперименты, для достижения приемлемой производительности размер буфера должен составлять, по крайней мере, 32 Кбайт, но даже и в этом случае обычный синхронный ввод/вывод работает быстрее. К тому же, производительность этой программы не повышается и в условиях SMP, поскольку в данном примере, в котором обрабатываются всего лишь два файла, ЦП не является критическим ресурсом.

Расширенный ввод/вывод с использованием процедуры завершения

Существует также другой возможный подход к использованию объектов синхронизации. Вместо того чтобы заставлять поток ожидать поступления сигнала завершения от события или дескриптора, система может инициировать вызов определенной пользователем процедуры завершения сразу же по окончании выполнения операции ввода/вывода. Далее процедура завершения может запустить очередную операцию ввода/вывода и выполнить любые необходимые действия по учету использования системных ресурсов. Эта косвенно вызываемая (callback) процедура завершения аналогична асинхронному вызову процедуры, который применялся в главе 10, и требует использования состояний дежурного ожидания (alertable wait states).

Каким образом процедура завершения может быть указана в программе? Среди параметров или структур данных функций ReadFile и WriteFile не остается таких, которые можно было бы использовать для хранения адреса процедуры завершения. Однако существует семейство расширенных функций ввода/вывода, которые обозначаются суффиксом "Ех" и содержат дополнительный параметр, предназначенный для передачи адреса процедуры завершения. Функциями чтения и записи являются, соответственно, ReadFileEx и WriteFileEx. Кроме того, требуется использование одной из указанных ниже функций дежурного ожидания.

Расширенный ввод/вывод иногда называют дежурным вводом/выводом (alertable I/O). О том, как использовать расширенные функции, рассказывается в последующих разделах.

Примечание

Под управлением Windows 9x расширенный ввод/вывод не может работать с дисковыми файлами и коммуникационными портами. В то же время, средства расширенного ввода/вывода Windows 9x способны работать с именованными каналами, почтовыми ящиками, сокетами и последовательными устройствами.

Функции ReadFileEx, WriteFileEx и процедурызавершения

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

Перекрывающиеся сокеты (глава 12) могут использоваться совместно с функциями ReadFileEx и WriteFileEx во всех версиях Windows.

BOOL ReadFileEx(HANDLE hFile, LPVOID lpBuffer, DWORD nNumberOfBytesToRead, LPOVERLAPPED lpOverlapped, LPOVERLAPPED_COMPLETION_ROUTINE lpcr)
BOOL WriteFileEx(HANDLE hFile, LPVOID lpBuffer, DWORD nNumberOfBytesToWrite, LPOVERLAPPED lpOverlapped, LPOVERLAPPED_COMPLETION_ROUTINE lpcr)

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

Каждой из функций необходимо предоставлять структуру OVERLAPPED, но надобность в указании элемента hEvent этой структуры отсутствует; система игнорирует его. Вместе с тем, этот элемент оказывается очень полезным для передачи такой, например, информации, как порядковый номер, используемый для различения отдельных операций ввода/вывода, что демонстрируется в программе 14.2.

Сравнивая с функциями ReadFile и WriteFile, можно заметить, что расширенные функции не требуют параметров для хранения количества переданных байтов. Эта информация передается функции завершения, которая должна включаться в программу.

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

VOID WINAPI FileIOCompletionRoutine (DWORD dwError, DWORD cbTransferred, LPOVERLAPPED lpo)

Как и в случае функции CreateThread, при вызове которой также указывается имя некоторой функции, имя FileIOCompletionRoutine является заменителем, а не фактическим именем процедуры завершения.

Значения параметра dwError ограничены 0 (успешное завершение) и ERROR_HANDLE_EOF (при попытке выполнить чтение с выходом за пределы файла). Структура OVERLAPPED - это та структура, которая использовалась завершившимся вызовом ReadFileEx или WriteFileEx.

Прежде чем процедура завершения будет вызвана системой, должны произойти две вещи:

1. Должна завершиться операция ввода/вывода.

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

Каким образом поток переходит в состояние дежурного ожидания? Он должен выполнить явный вызов одной из функций дежурного ожидания, описанных в следующем разделе. Тем самым поток создает условия, делающие преждевременное выполнение процедуры завершения невозможным. В состоянии дежурного ожидания поток может находиться только на протяжении того времени, пока длится вызов функции дежурного ожидания; после возврата из этой функции поток выходит из указанного состояния.

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

Функции дежурного ожидания

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

DWORD WaitForSingleObjectEx(HANDLE hObject, DWORD dwMilliseconds, BOOL bAlertable)
DWORD WaitForMultipleObjectsEx(DWORD cObjects, LPHANDLE lphObjects, BOOL fWaitAll, DWORD dwMilliseconds, BOOL bAlertable)
DWORD SleepEx(DWORD dwMilliseconds, BOOL bAlertable)

В каждой из функций дежурного ожидания имеется флаг bAlertable, который в случае асинхронного ввода/вывода должен устанавливаться в TRUE. Приведенные выше функции являются расширением знакомых вам функций Wait и Sleep.

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

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

Истекает интервал ожидания.

Все процедуры завершения, находящиеся в очереди потока, прекращают свое выполнение, а значение параметра bAlertable равно TRUE. Процедура завершения помещается в очередь тогда, когда завершается соответствующая ей операция ввода/вывода (рис. 14.2).

Заметьте, что со структурами OVERLAPPED в функциях ReadFileEx и WriteFileEx не связаны никакие события, поэтому ни один из дескрипторов, указываемых при вызове функции ожидания, не связывается непосредственно с какой-либо определенной операцией ввода/вывода. В то же время, функция SleepEx не связана с объектами синхронизации, и поэтому ее проще всего использовать. В случае функции SleepEx в качестве длительности интервала ожидания обычно указывают значение INFINITE, поэтому возврат из этой функции произойдет только после того, как закончится выполнение одной или нескольких процедур завершения, которые в настоящий момент находятся в очереди.

Выполнение процедуры завершения и возврат из функции дежурного ожидания

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

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

Если возврат из функции SleepEx обусловлен выполнением одной или нескольких процедур завершения, находящихся в очереди, то возвращаемым значением функции будет WAIT_TO_COMPLETION, и это же значение будет возвращено функцией GetLastError, вызванной после выполнения возврата одной из функций ожидания.

В заключение отметим два момента:

1. При вызове любой из функций дежурного ожидания в качестве значения параметра интервала ожидания используйте INFINITE. В отсутствие возможности истечения интервала ожидания возврат из функций будет осуществляться лишь после того, как закончится выполнение всех процедур завершения или дескрипторы перейдут в сигнальное состояние.

2. Для передачи информации процедуре завершения общепринято использовать элемент данных hEvent структуры OVERLAPPED, поскольку это поле игнорируется ОС.

Взаимодействие между основным потоком, процедурами завершения и функциями дежурного ожидания иллюстрирует рис. 14.2. В этом примере запускаются три параллельные операции чтения, две из которых завершаются к тому моменту, когда начинается выполнение дежурного ожидания.

Рис. 14.2. Асинхронный ввод/вывод с использованием процедур завершения

Пример: преобразование файла с использованием расширенного ввода/вывода

Программа 14.3 (atouEX) представляет собой переработанную версию программы 14.1. Эти программы иллюстрируют различие между двумя методами асинхронного ввода/вывода. Программа atouEx аналогична программе 14.1, но большая часть кода, предназначенного для упорядочения ресурсов, перемещена в ней в процедуру завершения, а многие переменные сделаны глобальными, чтобы процедура завершения могла иметь к ним доступ. Вместе с тем, в приложении В показано, что в отношении быстродействия программа atouEx вполне может конкурировать с другими методами, в которых не используется отображение файлов, тогда как программа atouOV работает медленнее.

Программа 14.2. atouEx: преобразование файла с использованием расширенного ввода/вывода
Преобразование файла из ASCII в Unicode средствами РАСШИРЕННОГО ВВОДА/ВЫВОДА. */
/* atouEX файл1 файл2 */

#define REC_SIZE 8096 /* Размер блока не имеет столь важного значения в отношении производительности, как в случае atouOV. */
#define UREC_SIZE 2 * REC_SIZE

static VOID WINAPI ReadDone(DWORD, DWORD, LPOVERLAPPED);
static VOID WINAPI WriteDone(DWORD, DWORD, LPOVERLAPPED);

/* Первая структура OVERLAPPED предназначена для чтения, а вторая - для записи. Структуры и буферы распределяются для каждой предстоящей операции. */
OVERLAPPED OverLapIn, OverLapOut ;
CHAR AsRec;
WCHAR UnRec;
HANDLE hInputFile, hOutputFile;

int _tmain(int argc, LPTSTR argv) {
hInputFile = CreateFile(argv, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL);
hOutputFile = CreateFile(argv, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_FLAG_OVERLAPPED, NULL);
FileSize.LowPart = GetFileSize(hInputFile, &FileSize.HighPart);
nRecord = FileSize.QuadPart / REC_SIZE;
if ((FileSize.QuadPart % REC_SIZE) != 0) nRecord++;
for (ic = 0; ic < MAX_OVRLP; ic++) {
OverLapIn.hEvent = (HANDLE)ic; /* Перегрузить событие. */
OverLapOut.hEvent = (HANDLE)ic; /* Поля. */
OverLapIn.Offset = CurPosIn.LowPart;
OverLapIn.OffsetHigh = CurPosIn.HighPart;
if (CurPosIn.QuadPart < FileSize.QuadPart) ReadFileEx(hInputFile, AsRec, REC_SIZE, &OverLapIn , ReadDone);
CurPosIn.QuadPart += (LONGLONG)REC_SIZE;
/* Выполняются все операции чтения. Войти в состояние дежурного ожидания и оставаться в нем до тех пор, пока не будут обработаны все записи.*/
while (nDone < 2 * nRecord) SleepEx(INFINITE, TRUE);
_tprintf(_T("Преобразование из ASCII в Unicode завершено.\n"));

static VOID WINAPI ReadDone(DWORD Code, DWORD nBytes, LPOVERLAPPED pOv) {
/* Чтение завершено. Преобразовать данные и инициировать запись. */
LARGE_INTEGER CurPosIn, CurPosOut;
/* Обработать запись и инициировать операцию записи. */
CurPosIn.LowPart = OverLapIn.Offset;
CurPosIn.HighPart = OverLapIn.OffsetHigh;
CurPosOut.QuadPart = (CurPosIn.QuadPart / REC_SIZE) * UREC_SIZE;
OverLapOut.Offset = CurPosOut.LowPart;
OverLapOut.OffsetHigh = CurPosOut.HighPart;
/* Преобразовать запись из ASCII в Unicode. */
for (i = 0; i < nBytes; i++) UnRec[i] = AsRec[i];
WriteFileEx(hOutputFile, UnRec, nBytes*2, &OverLapOut, WriteDone);
/* Подготовить структуру OVERLAPPED для следующего чтения. */
CurPosIn.QuadPart += REC_SIZE * (LONGLONG)(MAX_OVRLP);
OverLapIn.Offset = CurPosIn.LowPart;
OverLapIn.OffsetHigh = CurPosIn.HighPart;

static VOID WINAPI WriteDone(DWORD Code, DWORD nBytes, LPOVERLAPPED pOv) {
/* Запись завершена. Инициировать следующую операцию чтения. */
CurPosIn.LowPart = OverLapIn.Offset;
CurPosIn.HighPart = OverLapIn.OffsetHigh;
if (CurPosIn.QuadPart < FileSize.QuadPart) {
ReadFileEx(hInputFile, AsRec, REC_SIZE, &OverLapIn, ReadDone);

Асинхронный ввод/вывод сиспользованием нескольких потоков

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

Однако Windows обеспечивает многопоточную поддержку, поэтому становится возможным достижение того же эффекта за счет выполнения синхронных операций ввода/вывода в нескольких, выполняемых независимо потоках. Ранее эти возможности уже были продемонстрированы на примере многопоточных серверов и программы grepMT (глава 7). Кроме того, потоки обеспечивают концептуально последовательный и, предположительно, гораздо более простой способ выполнения асинхронных операций ввода/вывода. В качестве альтернативы методам, используемым в программах 14.1 и 14.2, можно было бы предоставить каждому потоку собственный дескриптор файла, и тогда каждый из потоков мог бы обрабатывать в синхронном режиме каждую четвертую запись.

Такой способ использования потоков продемонстрирован в программе atouMT, которая в книге не приводится, но включена в материал, размещенный на Web-сайте. Программа atouMT не только способна выполняться под управлением любой версии Windows, но и более проста по сравнению с любым из двух вариантов программ асинхронного ввода/вывода, поскольку учет использования ресурсов в этом случае менее сложен. Каждый поток просто поддерживает собственные буферы в собственном стеке и выполняет в цикле последовательность синхронных операций чтения, преобразования и записи. При этом производительность программы остается на достаточно высоком уровне.

Примечание

В программе atouMT.с, которая находится на Web-сайте, содержатся комментарии по поводу нескольких возможных "ловушек", которые могут поджидать вас при организации доступа одновременно нескольких потоков к одному и тому же файлу. В частности, все отдельные дескрипторы файлов должны создаваться с помощью функции CreateHandle, а не функции DuplicateHandle.

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

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

Таймеры ожидания

Windows NT поддерживает таймеры ожидания (waitable timers), являющихся одним из типов объектов ядра, осуществляющих ожидание.

Вы всегда можете создать собственный сигнал синхронизации, создав синхронизирующий поток, который устанавливает событие в результате пробуждения после вызова функции Sleep. В программе serverNP (программа 11.3) сервер также использует синхронизирующий поток для периодической широковещательной рассылки имени своего канала. Поэтому таймеры ожидания обеспечивают хотя и несколько избыточный, но удобный способ организации выполнения задач на периодической основе или в соответствии с определенным расписанием. В частности, таймер ожидания можно настроить таким образом, чтобы сигнал был сгенерирован в строго определенное время.

Таймер ожидания может быть либо синхронизирующим (synchronization timer), либо сбрасываемым вручную уведомляющим (manual-reset notification timer) таймером. Синхронизирующий таймер связывается с функцией косвенного вызова, аналогичной процедуре завершения расширенного ввода/вывода, тогда как для синхронизации по сбрасываемому вручную уведомляющему таймеру используется функция ожидания.

Для начала потребуется создать дескриптор таймера, используя для этого функцию CreateWaitableTimer.

HANDLE CreateWaitableTimer(LPSECURITY_ATTRIBUTES lpTimerAttributes, BOOL bManualReset, LPCTSTR lpTimerName);

Второй параметр, bManualReset, определяет, таймер какого типа должен быть создан - синхронизирующий или уведомляющий. В программе 14.3 используется синхронизирующий таймер, но, изменив комментарии и настройку параметра, вы легко превратите его в уведомляющий таймер. Заметьте, что существует также функция OpenWaitableTimer, которая может использовать необязательное имя, предоставляемое третьим аргументом.

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

BOOL SetWaitableTimer(HANDLE hTimer, const LARGE_INTEGER *pDueTime, LONG IPeriod, PTIMERAPCROUTINE pfnCompletionRoutine, LPVOID lpArgToCompletionRoutine, BOOL fResume);

hTimer - действительный дескриптор таймера, созданного с использованием функции CreateWaitableTimer.

Второй параметр, на который указывает указатель pDueTime, может принимать либо положительные значения, соответствующие абсолютному времени, либо отрицательные, соответствующие относительному времени, причем фактические значения выражаются в единицах времени длительностью 100 наносекунд, а их формат описывается структурой FILETIME. Переменные типа FILETIME были введены в главе 3 и уже использовались нами в главе 6 в программе timep (программа 6.2).

Величина интервала между сигналами, указываемая в третьем параметре, выражается в миллисекундах. Если это значение установлено равным 0, то таймер переводится в сигнальное состояние только один раз. При положительных значениях этого параметра таймер является периодическим и срабатывает периодически до тех пор, пока его действие не будет прекращено вызовом функции CancelWaitableTimer. Отрицательные значения указанного интервала не допускаются.

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

Установив синхронизирующий таймер, вы можете перевести поток в состояние дежурного ожидания путем вызова функции SleepEx, чтобы обеспечить возможность вызова процедуры завершения. В случае сбрасываемого вручную уведомляющего таймера следует организовать ожидание перехода дескриптора таймера в сигнальное состояние. Дескриптор будет оставаться в сигнальном состоянии до следующего вызова функции SetWaitableTimer. Полная версия программы 14.3, находящаяся на Web-сайте, предоставляет вам возможность проводить собственные эксперименты, используя таймер выбранного типа в сочетании с процедурой завершения или ожиданием перехода дескриптора таймера в сигнальное состояние, что в итоге дает четыре различные комбинации.

Последний параметр, fResume, связан с режимами энергосбережения. Для получения более подробной информации по этому вопросу обратитесь к справочной документации.

Функция CancelWaitableTimer используется для отмены действия вызванной перед этим функции SetWaitableTimer, но при этом не изменяет сигнальное состояние таймера. Чтобы это сделать, необходимо в очередной раз вызвать функцию SetWaitableTimer.

Пример: использование таймера ожидания

В программе 14.3 демонстрируется применение таймера ожидания для генерации периодических сигналов.

Программа 14.3. TimeBeep: генерация периодических сигналов
/* Глава 14. TimeBeep.с. Периодическое звуковое оповещение. */
/* Использование: TimeBeep период (в миллисекундах). */

static BOOL WINAPI Handler(DWORD CntrlEvent);
static VOID APIENTRY Beeper(LPVOID, DWORD, DWORD);
volatile static BOOL Exit = FALSE;

int _tmain(int argc, LPTSTR argv) {
/* Перехват нажатия комбинации клавиш для прекращения операции. См. главу 4. */
SetConsoleCtrlHandler(Handler, TRUE);
DueTime.QuadPart = –(LONGLONG)Period * 10000;
/* Параметр DueTime отрицателен для первого периода ожидания и задается относительно текущего времени. Период ожидания измеряется в мс (10 -3 с), a DueTime - в единицах по 100 нc (10 -7 с) для согласования с типом FILETIME. */
hTimer = CreateWaitableTimer(NULL, FALSE /* "Таймер синхронизации" */, NULL);
SetWaitableTimer(hTimer, &DueTime, Period, Beeper, &Count, TRUE);
_tprintf(_T("Count = %d\n"), Count);
/* Значение счетчика увеличивается в процедуре таймера. */
/* Войти в состояние дежурного ожидания. */
_tprintf(_T("Завершение. Счетчик = %d"), Count);

static VOID APIENTRY Beeper(LPVOID lpCount, DWORD dwTimerLowValue, DWORD dwTimerHighValue) {
*(LPDWORD)lpCount = *(LPDWORD)lpCount + 1;
_tprintf(_T("Генерация сигнала номер: %d\n"), *(LPDWORD) lpCount);
Веер(1000 /* Частота. */, 250 /* Длительность (мс). */);

BOOL WINAPI Handler(DWORD CntrlEvent) {
_tprintf(_T("Завершение работы\n"));

Мы ждали его слишком долго

Что может быть глупее, чем ждать?

Б. Гребенщиков

В ходе этой лекции вы изучите

    Использование системного вызова select

    Использование системного вызова poll

    Некоторые аспекты использования select/pollв многопоточных программах

    Стандартные средства асинхронного ввода/вывода

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

Если ваша программа главным образом занимается операциями ввода/вывода, вы можете получить наиболее важные из преимуществ многопоточности в однопоточной программе, используя системный вызов select(3C). В большинствеUnix-системselectявляется системным вызовом, или, во всяком случае, описывается в секции системного руководства 2 (системные вызовы), т.е. ссылка на него должна была бы выглядеть какselect(2), но вSolaris10 соответствующая страница системного руководства размещена в секции 3C(стандартная библиотека языка С).

Устройства ввода/вывода обычно работают гораздо медленнее центрального процессора, поэтому при выполнении операций с ними процессор обычно оказывается вынужден ждать их. Поэтому во всех ОС системные вызовы синхронного ввода/вывода представляют собой блокирующиеся операции.

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

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

Системный вызов select(3C) позволяет ожидать готовности нескольких устройств или сетевых соединений (в действительности, готовности объектов большинства типов, которые могут быть идентифицированы файловым дескриптором). Когда один или несколько из дескрипторов оказываются готовы передать данные,select(3C) возвращает управление программе и передает списки готовых дескрипторов в выходных параметрах.

В качестве параметров select(3C) использует множества (наборы) дескрипторов. В старыхUnix-системах множества были реализованы в виде 1024-разрядных битовых масок. В современныхUnix-системах и в других ОС, реализующихselect, множества реализованы в виде непрозрачного типаfd_set, над которым определены некоторые теоретико-множественные операции, а именно – очистка множества, включение дескриптора в множество, исключение дескриптора из множества и проверка наличия дескриптора в множестве. Препроцессорные директивы для выполнения этих операций описаны на странице руководстваselect(3C).

В 32-разрядных версиях UnixSVR4, в том числе вSolaris,fd_setпо прежнему представляет собой 1024-битовую маску; в 64-разрядных версияхSVR4 это маска разрядности 65536 бит. Размер маски определяет не только максимальное количество файловых дескрипторов в наборе, но и максимальный номер файлового дескриптора в наборе. Размер маски в вашей версии системы можно определить во время компиляции по значению препроцессорного символаFD_SETSIZE. Нумерация файловых дескрипторов вUnixначинается с 0, поэтому максимальный номер дескриптора равенFD_SETSIZE-1.

Таким образом, если вы используете select(3C), вам необходимо установить ограничения на количество дескрипторов вашего процесса. Это может быть сделано шелловской командойulimit(1) перед запуском процесса или системным вызовомsetrlimit(2) уже во время исполнения вашего процесса. Разумеется,setrlimit(2) необходимо вызвать до того, как вы начнете создавать файловые дескрипторы.

Если вам необходимо использовать более 1024 дескрипторов в 32-битной программе, Solaris10 предоставляет переходныйAPI. Для его использования необходимо определить

препроцессорный символ FD_SETSIZEс числовым значением, превышающим 1024, перед включением файла . При этом в файле сработают необходимые препроцессорные директивы и типfd_setбудет определен как большая битовая маска, аselectи другие системные вызовы этого семейства будут переопределены для использования масок такого размера.

В некоторых реализациях fd_setреализован другими средствами, без использования битовых масок. Например,Win32 предоставляетselectв составе так называемогоWinsockAPI. ВWin32fd_setреализован как динамический массив, содержащий значения файловых дескрипторов. Поэтому вам не следует полагаться на знание внутренней структуры типаfd_set.

Так или иначе, изменения размера битовой маски fd_setили внутреннего представления этого типа требуют перекомпиляции всех программ, использующихselect(3C). В будущем, когда архитектурный лимит в 65536 дескрипторов на процесс будет повышен, может потребоваться новая версия реализацииfd_setиselectи новая перекомпиляция программ. Чтобы избежать этого и упростить переход на новую версиюABI, компанияSunMicrosystemsрекомендует отказываться от использованияselect(3C) и использовать вместо него системный вызовpoll(2). Системный вызовpoll(2) рассматривается далее на этой лекции.

Системный вызов select(3C) имеет пять параметров.

intnfds– число, на единицу большее, чем максимальный номер файлового дескриптора во всех множествах, переданных как параметры.

fd_set*readfds– Входной параметр, множество дескрипторов, которые следует проверять на готовность к чтению. Конец файла или закрытие сокета считается частным случаем готовности к чтению. Регулярные файлы всегда считаются готовыми к чтению. Также, если вы хотите проверить слушающий сокетTCPна готовность к выполнениюaccept(3SOCKET), его следует включить в это множество. Также, выходной параметр, множество дескрипторов, готовых к чтению.

fd_set*writefds– Входной параметр, множество дескрипторов, которые следует проверять на готовность к записи. Ошибка при отложенной записи считается частным случаем готовности к записи. Регулярные файлы всегда готовы к записи. Также, если вы хотите проверить завершение операции асинхронногоconnect(3SOCKET), сокет следует включить в это множество. Также, выходной параметр, множество дескрипторов, готовых к записи.

fd_set*errorfds– Входной параметр, множество дескрипторов, которые следует проверять на наличие исключительных состояний. Определение исключительного состояния зависит от типа файлового дескриптора. Для сокетовTCPисключительное состояние возникает при приходе внеполосных данных. Регулярные файлы всегда считаются находящимися в исключительном состоянии. Также, выходной параметр, множество дескрипторов, на которых возникли исключительные состояния.

structtimeval*timeout– тайм-аут, временной интервал, задаваемый с точностью до микросекунд. Если этот параметр равенNULL, тоselect(3C) будет ожидать неограниченное время; если в структуре задан нулевой интервал времени,select(3C) работает в режиме опроса, то есть возвращает управление немедленно, возможно с пустыми наборами дескрипторов.

Вместо всех параметров типа fd_set* можно передать нулевой указатель. Это означает, что соответствующий класс событий нас не интересует.select(3C) возвращает общее количество готовых дескрипторов во всех множествах при нормальном завершении (в том числе при завершении по тайм-ауту), и -1 при ошибке.

В примере 1 приводится использование select(3C) для копирования данных из сетевого соединения на терминал, а с терминала – в сетевое соединение. Эта программа упрощенная, она предполагает, что запись на терминал и в сетевое соединение никогда не будет заблокирована. Поскольку и терминал, и сетевое соединение имеют внутренние буферы, при небольших потоках данных это обычно так и есть.

Пример 1. Двустороннее копирование данных между терминалом и сетевым соединением. Пример взят из книги У.Р. Стивенс, Unix: разработка сетевых приложений. Вместо стандартных системных вызовов используются «обертки», описанные в файле “unp.h”

#include "unp.h"

void str_cli(FILE *fp, int sockfd) {

int maxfdp1, stdineof;

char sendline, recvline;

if (stdineof == 0) FD_SET(fileno(fp), &rset);

FD_SET(sockfd, &rset);

maxfdp1 = max(fileno(fp), sockfd) + 1;

Select(maxfdp1, &rset, NULL, NULL, NULL);

if (FD_ISSET(sockfd, &rset)) { /* socket is readable */

if (Readline(sockfd, recvline, MAXLINE) == 0) {

if (stdineof == 1) return; /* normal termination */

else err_quit("str_cli: server terminated prematurely");

Fputs(recvline, stdout);

if (FD_ISSET(fileno(fp), &rset)) { /* input is readable */

if (Fgets(sendline, MAXLINE, fp) == NULL) {

Shutdown(sockfd, SHUT_WR); /* send FIN */

FD_CLR(fileno(fp), &rset);

Writen(sockfd, sendline, strlen(sendline));

Обратите внимание, что программа примера 1 заново пересоздает множества дескрипторов перед каждым вызовом select(3C). Это необходимо, потому что при нормальном завершенииselect(3C) модифицирует свои параметры.

select(3C) считаетсяMT-Safe, однако при его использовании в многопоточной программе надо иметь в виду следующий момент. Действительно, сам по себеselect(3C) не использует локальных данных и поэтому его вызов из нескольких нитей не должен приводить к проблемам. Однако если несколько нитей работают с пересекающимися наборами файловых дескрипторов, возможен такой сценарий:

    Нить 1 вызывает readиз дескриптораsи получает все данные из его буфера

    Нить 2 вызывает readиз дескриптораsи блокируется.

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

    Нить 1 включает дескриптор sв наборreadfdsи вызываетselect.

    selectв нити 1 возвращаетsкак готовый для чтения

    Нить 2 включает дескриптор sв наборreadfdsи вызываетselect

    selectв нити 2 возвращаетsкак готовый для чтения

    Нить 1 вызывает readиз дескриптораsи получает только часть данных из его буфера

    Нить 2 вызывает readиз дескриптораs, получает данные и записывает их поверх данных, полученных нитью 1

В лекции 10 мы рассмотрим архитектуру приложения, в котором несколько нитей работают с общим пулом файловых дескрипторов – так называемую архитектуру рабочих нитей (workerthreads). При этом нити, разумеется, должны указывать друг другу, с какими именно дескрипторами они сейчас работают.

С точки зрения разработки многопоточных программ, важным недостатком select(3C) – или, возможно, недостаткомPOSIXThreadAPI– является тот факт, что примитивы синхронизацииPOSIXне являются файловыми дескрипторами и не могут использоваться вselect(3C). В то же время, при реальной разработке многопоточных программ, занимающихся вводом/выводом, часто было бы полезно ожидать в одной операции готовности файловых дескрипторов и готовности других нитей собственного процесса.

синхронную модель ввода/вывода. Системные вызовы read(2) , write(2) и их аналоги возвращают управление только того, как данные уже прочитаны или записаны. Часто это приводит к блокировке нити.

Примечание

В действительности, не все так просто. read(2) действительно должен ожидать физического прочтения данных с устройства, но write(2) по умолчанию работает в режиме отложенной записи: он возвращает управление после того, как данные переданы в системный буфер, но, вообще говоря, до того, как данные будут физически переданы устройству. Это, как правило, значительно повышает наблюдаемую производительность программы и позволяет использовать память изпод данных для других целей сразу после возврата write(2) . Но отложенная запись имеет и существенные недостатки. Главный из них - вы узнаете о результате физической операции не сразу по коду возврата write(2) , а лишь через некоторое время после возврата, обычно по коду возврата следующего вызова write(2) . Для некоторых приложений - для мониторов транзакций, для многих программ реального времени и др. - это неприемлемо и они вынуждены выключать отложенную запись. Это делается флагом O_SYNC , который может устанавливаться при открытии файла и изменяться у открытого файла вызовом fcntl(2) .

Синхронизация отдельных операций записи может быть обеспечена вызовом fsync(2) . Для многих приложений, работающих с несколькими устройствами и/или сетевыми соединениями синхронная модель неудобна. Работа в режиме опроса тоже не всегда приемлема. Дело в том, что select(3С) и poll(2) считают дескриптор файла готовым для чтения только после того, как в его буфере физически появятся данные. Но некоторые устройства начинают отдавать данные только после того, как их об этом явно попросят.

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

С точки зрения приложений жесткого реального времени может оказаться существенным еще один аспект проблемы ввода/вывода. Дело в том, что приложения жесткого РВ имеют более высокий приоритет, чем ядро , поэтому исполнение ими системных вызовов - даже неблокирующихся! - может привести к инверсии приоритета .

Решение этих проблем известно давно и называется асинхронный ввод/ вывод . В этом режиме системные вызовы ввода/вывода возвращают управление сразу после формирования запроса к драйверу устройства, как правило, даже до того, как данные будут скопированы в системный буфер . Формирование запроса состоит в установке записи ( IRP , Input/Output Request Packet , пакет запроса ввода вывода) в очередь . Для этого надо лишь ненадолго захватить мутекс, защищающий "хвост" очереди, поэтому проблема инверсии приоритета легко преодолима. Для того, чтобы выяснить, закончился ли вызов, и если закончился, то чем именно, и можно ли использовать память , в которой хранились данные, предоставляется специальный API (см. рис. 8.1)


Рис. 8.1.

Асинхронная модель была основной моделью ввода/вывода в таких ОС, как DEC RT-11 , DEC RSX-11 , VAX/VMS, OpenVMS . Эту модель в той или иной форме поддерживают практически все ОС реального времени . В системах семейства Unix с конца 1980х использовалось несколько несовместимых API для асинхронного ввода/вывода. В 1993 году ANSI/IEEE был принят документ POSIX 1003.1b, описывающий стандартизованный API , который мы и изучим далее в этом разделе.

В Solaris 10 функции асинхронного ввода/вывода включены в библиотеку libaio.so . Для сборки программ, использующих эти функции, необходимо использовать ключ -laio . Для формирования запросов на асинхронный ввод/ вывод используются функции aio_read(3AIO) , aio_write(3AIO) и lio_listio(3AIO) .

Функции aio_read(3AIO) и aio_write(3AIO) имеют единственный параметр , structaiocb *aiocbp . Структура aiocb определена в файле < aio .h> и содержит следующие поля:

  • int aio_fildes - дескриптор файла
  • off_t aio_offset - смещение в файле, начиная с которого будет идти запись или чтение
  • volatile void* aio_buf - буфер, в который следует прочитать данные или в котором лежат данные для записи.
  • size_t aio_nbytes - размер буфера. Как и традиционный read(2) , aio_read(3AIO) может прочитать меньше данных, чем было запрошено, но никогда непрочитает больше.
  • int aio_reqprio - приоритет запроса
  • struct sigevent aio_sigevent - способ оповещения о завершении запроса (рассматривается далее в этом разделе)
  • int aio_lio_opcode - при aio_read(3AIO) и aio_write(3AIO) не используется, используется только функцией lio_listio .

Функция lio_listio(3AIO) позволяет одним системным вызовом сформировать несколько запросов на ввод/ вывод . Эта функция имеет четыре параметра:

  • int mode - может принимать значения LIO_WAIT (функция ждет завершения всех запросов) и LIO_NOWAIT (функция возвращает управление сразу после формирования всех запросов).
  • struct aiocb *list - список указателей на структуры aiocb с описаниями запросов.

    Запросы могут быть как на чтение, так и на запись, это определяется полем aio_lio_opcode . Запросы к одному дескриптору исполняются в том порядке, в каком они указаны в массиве list .

  • int nent - количество записей в массиве list .
  • struct sigevent *sig - способ оповещения о завершении всех запросов. Если mode==LIO_WAIT , этот параметр игнорируется.

Библиотека POSIX AIO предусматривает два способа оповещения программы о завершении запроса, синхронный и асинхронный. Сначала рассмотрим синхронный способ. Функция aio_return(3AIO) возвращает статус запроса. Если запрос уже завершился и завершился успешно, она возвращает размер прочитанных или записанныхданных в байтах. Как и у традиционного read(2) , в случае конца файла aio_return(3AIO) возвращает 0 байт . Если запрос завершился ошибкой или еще не завершился, возвращается -1 и устанавливается errno . Если запрос еще не завершился, код ошибки равен EINPROGRESS .

Функция aio_return(3AIO) разрушающая; если ее вызвать для завершенного запроса, то она уничтожит системный объект , хранящий информацию о статусе запроса. Многократный вызов aio_return(3AIO) по поводу одного и того же запроса, таким образом, невозможен.

Функция aio_error(3AIO) возвращает код ошибки , связанной с запросом. При успешном завершении запроса возвращается 0 , при ошибке - код ошибки , для незавершенных запросов - EINPROGRESS .

Функция aio_suspend(3AIO) блокирует нить до завершения одного из указанных ей запросов асинхронного ввода/вывода либо на указанный интервал времени. Эта функция имеет три параметра:

  • const struct aiocb *const list - массив указателей на описатели запросов.
  • int nent - количество элементов в массиве list .
  • const struct timespec *timeout - тайм-аут с точностью до наносекунд (в действительности, с точностью до разрешения системного таймера ).

Функция возвращает 0 , если хотя бы одна из операций, перечисленныхв списке, завершилась. Если функция завершилась с ошибкой, она возвращает -1 и устанавливает errno . Если функция завершилась по тайм-ауту, она также возвращает -1 и errno==EINPROGRESS .

Пример использования асинхронного ввода/вывода с синхронной проверкой статуса запроса приводится в примере 8.3 .

Const char req="GET / HTTP/1.0\r\n\r\n"; int main() { int s; static struct aiocb readrq; static const struct aiocb *readrqv={&readrq, NULL}; /* Открыть сокет […] */ memset(&readrq, 0, sizeof readrq); readrq.aio_fildes=s; readrq.aio_buf=buf; readrq.aio_nbytes=sizeof buf; if (aio_read(&readrq)) { /* … */ } write(s, req, (sizeof req)-1); while(1) { aio_suspend(readrqv, 1, NULL); size=aio_return(&readrq); if (size>0) { write(1, buf, size); aio_read(&readrq); } else if (size==0) { break; } else if (errno!=EINPROGRESS) { perror("reading from socket"); } } } 8.3. Асинхронный ввод/вывод с синхронной проверкой статуса запроса. Код сокращен, из него исключены открытие сокета и обработка ошибок.

Асинхронное оповещение приложения о завершении операций состоит в генерации сигнала при завершении операции . Чтобы это сделать, необходимо внести соответствующие настройки в поле aio_sigevent описателя запроса. Поле aio_sigevent имеет тип struct sigevent . Эта структура определена в и содержит следующие поля:

  • int sigev_notify - режим нотификации. Допустимые значения - SIGEV_NONE (не посылать подтверждения), SIGEV_SIGNAL (генерировать сигнал при завершении запроса) и SIGEV_THREAD (при завершении запроса запускать указанную функцию в отдельной нити). Solaris 10 также поддерживает тип оповещения SIGEV_PORT , который рассматривается в приложении к этой лекции.
  • int sigev_signo - номер сигнала, который будет сгенерирован при использовании SIGEV_SIGNAL .
  • union sigval sigev_value - параметр, который будет передан обработчику сигнала или функции обработки. При использовании для асинхронного ввода/вывода это обычно указатель на запрос.

    При использовании SIGEV_PORT это должен быть указательна структуру port_event_t , содержащую номер порта и, возможно, дополнительные данные.

  • void (*sigev_notify_function)(union sigval) - функция, которая будет вызвана при использовании SIGEV_THREAD .
  • pthread_attr_t *sigev_notify_attributes - атрибуты нити, в которой будет запущена
  • sigev_notify_function при использовании SIGEV_THREAD .

Далеко не все реализации libaio поддерживают оповещение SIGEV_THREAD . Некоторые Unix-системы используют вместо него нестандартное оповещение SIGEV_CALLBACK . Далее в этой лекции мы будем обсуждать только оповещение сигналом.

В качестве номера сигнала некоторые приложения используют SIGIO или SIGPOLL (в Unix SVR4 это один и тот же сигнал). Часто используют также SIGUSR1 или SIGUSR2 ; это удобно потому, что гарантирует, что аналогичный сигнал не возникнет по другой причине.

В приложениях реального времени используются также номера сигналов в диапазоне от SIGRTMIN до SIGRTMAX . Некоторые реализации выделяют для этой цели специальный номер сигнала SIGAIO или SIGASYNCIO , но в Solaris 10 такого сигнала нет.

Разумеется, перед тем, как исполнять асинхронные запросы с оповещением сигналом, следует установить обработчик этого сигнала. Для оповещения необходимо использовать сигналы, обрабатываемые в режиме SA_SIGINFO . Установить такой обработчик при помощи системных вызовов signal(2) и sigset(2) невозможно, необходимо использовать sigaction(2) . Установка обработчиков при помощи sigaction