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

Ввод/вывод данных

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

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

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

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

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

Синхронный и асинхронный ввод/вывод

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

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

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

Большинство устройств поддерживают возможность прямого доступа к памяти (Direct Memory Access, DMA) для передачи данных между устройством и ОЗУ компьютера, не требуя участия процессора в операции, и генерируют прерывание по завершении передачи данных. Синхронный режим ввода/вывода, который внутренне является асинхронным, поддерживается только на уровне приложений Windows.

В Win32 асинхронный ввод/вывод называется перекрывающимся вводом/выводом (overlapped I/O) , сравнение синхронного и перекрывающегося режимов ввода/вывода приводится на рисунке ниже:

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

    Событие Win32: операция, ожидающая это событие, будет выполнена по завершении ввода/вывода.

    Вызов пользовательской функции с помощью механизма асинхронного вызова процедур (Asynchronous Procedure Call, APC) : поток выполнения должен находиться в состоянии ожидания извещения (alertable wait).

    Прием извещений через порты завершения ввода/вывода (I/O Completion Ports, IOCP) : это обычно наиболее эффективный механизм. Мы подробно исследуем его далее.

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

Порты завершения ввода/вывода

Windows поддерживает эффективный механизм извещений о завершении асинхронных операций ввода/вывода под названием порт завершения ввода/вывода (I/O Completion Ports, IOCP). В приложениях для.NET он доступен посредством метода ThreadPool.BindHandle() . Этот механизм используется внутренними реализациями некоторых типов в.NET, выполняющих операции ввода/вывода: FileStream, Socket, SerialPort, HttpListener, PipeStream и некоторые каналы.NET Remoting.

Механизм IOCP, показанный на рисунке выше, связывается с несколькими дескрипторами ввода/вывода (сокетами, файлами и специализированными объектами драйверов устройств), открытыми в асинхронном режиме, и с определенным потоком выполнения. Как только операция ввода/вывода, связанная с таким дескриптором, завершится, Windows добавит извещение в соответствующий порт IOCP и передаст для обработки связанному с ним потоку выполнения.

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

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

Чтобы установить связь с указанным портом IOCP, пользовательские потоки выполнения вызывают функцию GetCompletionStatus и ожидают ее завершения. В каждый конкретный момент времени поток выполнения может быть связан только с одним портом IOCP.

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

Механизм IOCP способен определить, что какой-то из «занятых» потоков фактически выполняет синхронный ввод/вывод, и запустить дополнительный поток, возможно превысив максимальное значение параллелизма. Извещения можно также посылать вручную, без выполнения ввода/вывода, вызовом функции PostQueuedCompletionStatus .

В следующем коде демонстрируется пример использования ThreadPool.BindHandle() с файловым дескриптором Win32:

Using System; using System.Threading; using Microsoft.Win32.SafeHandles; using System.Runtime.InteropServices; public class Extensions { internal static extern SafeFileHandle CreateFile(string lpFileName, EFileAccess dwDesiredAccess, EFileShare dwShareMode, IntPtr lpSecurityAttributes, ECreationDisposition dwCreationDisposition, EFileAttributes dwFlagsAndAttributes, IntPtr hTemplateFile); static unsafe extern bool WriteFile(SafeFileHandle hFile, byte lpBuffer, uint nNumberOfBytesToWrite, out uint lpNumberOfBytesWritten, System.Threading.NativeOverlapped* lpOverlapped); enum EFileShare: uint { None = 0x00000000, Read = 0x00000001, Write = 0x00000002, Delete = 0x00000004 } enum ECreationDisposition: uint { New = 1, CreateAlways = 2, OpenExisting = 3, OpenAlways = 4, TruncateExisting = 5 } enum EFileAttributes: uint { // ... Некоторые флаги не показаны Normal = 0x00000080, Overlapped = 0x40000000, NoBuffering = 0x20000000, } enum EFileAccess: uint { // ... Некоторые флаги не показаны GenericRead = 0x80000000, GenericWrite = 0x40000000, } static long _numBytesWritten; // Тормоз для потока записи static AutoResetEvent _waterMarkFullEvent; static int _pendingIosCount; const int MaxPendingIos = 10; // Процедура завершения, вызывается потоками ввода/вывода static unsafe void WriteComplete(uint errorCode, uint numBytes, NativeOverlapped* pOVERLAP) { _numBytesWritten += numBytes; Overlapped ovl = Overlapped.Unpack(pOVERLAP); Overlapped.Free(pOVERLAP); // Известить поток записи, что количество ожидающих операций ввода/вывода // уменьшилось до допустимого предела if (Interlocked.Decrement(ref _pendingIosCount) = MaxPendingIos) { _waterMarkFullEvent.WaitOne(); } } } } }

Сначала рассмотрим метод TestIOCP. Здесь вызывается функция CreateFile(), которая является функцией механизма P/Invoke, используемой для открытия или создания файла или устройства. Для выполнения операций ввода/вывода в асинхронном режиме, необходимо передать функции флаг EFileAttributes.Overlapped. В случае успеха функция CreateFile() возвращает файловый дескриптор Win32, который мы связываем с портом завершения ввода/вывода вызовом ThreadPool.BindHandle(). Далее создается объект события, используемый для временного блокирования потока, инициировавшего операцию ввода/вывода, если таких операций оказывается слишком много (предел устанавливается константой MaxPendingIos).

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

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

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

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

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

Пул потоков в.NET

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

    Рабочие потоки могут обрабатывать асинхронные вызовы пользовательских делегатов (например, BeginInvoke или ThreadPool.QueueUserWorkItem).

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

    Потоки ожидания могут обеспечивать ожидание наступления зарегистрированных событий, позволяя организовать ожидание сразу нескольких событий в одном потоке (с помощью WaitForMultipleObjects), вплоть до верхнего предела Windows (maximum wait objects = 64). Прием ожидания событий используется для организации асинхронного ввода/вывода без применение портов завершения.

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

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

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

Копирование памяти

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

Неуправляемая память

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

Если применяемые вами функции или библиотеки позволяют явно указывать буфер в памяти или передавать им свою функцию обратного вызова для выделения буфера, распределите управляемый буфер и закрепите его в памяти, чтобы к нему можно было обращаться и по указателю, и по управляемой ссылке. Если буфер достаточно велик (> 85 000 байт), он будет создан в куче больших объектов (Large Object Heap) , поэтому старайтесь повторно использовать уже имеющиеся буферы. Если повторное использование буфера осложнено неопределенностью срока жизни объекта, применяйте пулы памяти.

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

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

Экспортирование части буфера

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

Чтобы избежать ненужного копирования памяти, организуйте прием смещения и длины везде, где принимаете параметр byte. Используйте параметр длины вместо свойства Length массива, а значение смещения добавляйте к текущим индексам.

Чтение вразброс и запись со слиянием

Чтение вразброс и запись со слиянием - это возможность, поддерживаемая ОС Windows, выполнять чтение в несмежные области или записывать данные из несмежных областей, как если бы они занимали непрерывный участок памяти. Данная функциональность в Win32 API предоставляется в виде функций ReadFileScatter и WriteFileGather . Библиотека сокетов Windows также поддерживает возможность чтения вразброс и записи со слиянием, предоставляя собственные функции: WSASend, WSARecv и другие.

Чтение вразброс и запись со слиянием могут пригодиться в следующих ситуациях:

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

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

В сравнении с функциями ReadFileScatter и WriteFileGather, требующими, чтобы каждый буфер в точности соответствовал размеру одной страницы, а дескриптор был открыт в асинхронном и небуферизованном режиме (что является еще большим ограничением), функции чтения вразброс и записи со слиянием на основе сокетов выглядят более практичными, потому что не имеют этих ограничений. Фреймворк.NET Framework поддерживает чтение вразброс и запись со слиянием для сокетов посредством перегруженных методов Socket.Send() и Socket.Receive() , не экспортируя при этом универсальные функции чтения/записи.

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

Файловый ввод/вывод

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

Управление кешированием

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

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

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

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

В.NET эти параметры поддерживаются (кроме последнего) с помощью перегруженного конструктора FileStream, принимающего параметр типа перечисления FileOptions.

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

Небуферизованный ввод/вывод

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

    Флаг FILE_FLAG_NO_BUFFERING отключает кеширование операций чтения и записи, но никак не влияет на кеширование, выполняемое контроллером диска. Это позволяет избежать копирования (из пользовательского буфера в кеш) и «загрязнения» кеша (заполнения кеша ненужными данными и вытеснение нужных). Однако небуферизованные операции чтения и записи должны придерживаться требований, касающихся выравнивания.

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

    Флаг FILE_FLAG_WRITE_THROUGH указывает диспетчеру кеша, что он должен сразу же выталкивать из кеша записываемые данные (если флаг FILE_FLAG_NO_BUFFERING не установлен) и сообщает контроллеру диска, что он должен выполнять запись на физический носитель немедленно, не сохраняя данные в промежуточном аппаратном кеше.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Модуль АЦП/ЦАП
16/32 каналов, 16 бит, 2 МГц, USB, Ethernet

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

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

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

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

Примечание

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

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

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

Из книги Давайте создадим компилятор! автора Креншоу Джек

Из книги Программирование на языке Пролог автора Клоксин У.

Из книги Язык программирования С# 2005 и платформа.NET 2.0. автора Троелсен Эндрю

Из книги Руководство администратора баз данных Informix. автора Кустов Виктор

Из книги Microsoft Visual C++ и MFC. Программирование для Windows 95 и Windows NT автора Фролов Александр Вячеславович

2.2.3.2 Асинхронный ввод-вывод Для ускорения операций ввода-вывода сервер использует собственный пакет асинхронного ввода-вывода (AIO) или пакет асинхронного ввода-вывода ядра ОС (KAIO), если он доступен. Пользовательские запросы на ввод-вывод обрабатываются асинхронно,

Из книги Основы объектно-ориентированного программирования автора Мейер Бертран

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

Из книги Системное программирование в среде Windows автора Харт Джонсон М

Ввод и вывод Два класса библиотеки KERNEL обеспечивают основные средства ввода и вывода: FILE и STD_FILES.Среди операций, определенных для объекта f типа FILE, есть следующие:create f.make ("name") -- Связывает f с файлом по имени name.f.open_write -- Открытие f для записиf.open_read -- Открытие f для

Из книги Программирование на языке Ruby [Идеология языка, теория и практика применения] автора Фултон Хэл

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

Из книги Программирование на языке Пролог для искусственного интеллекта автора Братко Иван

10.1.7. Простой ввод/вывод Вы уже знакомы с некоторыми методами ввода/вывода из модуля Kernel; мы вызывали их без указания вызывающего объекта. К ним относятся функции gets и puts, а также print, printf и p (последний вызывает метод объекта inspect, чтобы распечатать его в понятном для нас

Из книги Язык программирования Си для персонального компьютера автора Бочков C. О.

Из книги Linux программирование в примерах автора Роббинс Арнольд

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

Из книги Основы программирования на Java автора Сухов С. А.

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

Из книги QT 4: программирование GUI на С++ автора Бланшет Жасмин

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

Из книги Идеальный программист. Как стать профессионалом разработки ПО автора Мартин Роберт С.

Из книги автора

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

Из книги автора

Ввод и вывод Также мне кажется очень важным, чтобы мои результаты подпитывались соответствующим «вводом». Написание программного кода – творческая работа. Обычно мои творческие способности в наибольшей степени проявляются тогда, когда я сталкиваюсь с творческим

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

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

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

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

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