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

 

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

Основные идеи, понятия и объекты асинхронного ввода/вывода
Средства асинхронного ввода/вывода позволяют прикладным процессам ставить в очередь команды ввода/вывода данных, продолжать работу параллельно с операциями передачи данных и получать асинхронные уведомления о завершении выполнения этих команд. Подобные возможности полезны для многих приложений реального времени, поскольку исключают задержки непредсказуемой длительности (или, по крайней мере, перекладывают эти задержки на другие компоненты). Типичный пример – сохранение данных для постпроцессирования (которое будет проводиться не в реальном времени).
Вообще говоря, операции асинхронного ввода/вывода могут перекрываться во времени не только с работой инициировавшего их процесса, но и между собой, то есть в принципе возможно параллельное выполнение множества команд обмена данными с множеством файлов.
Операция асинхронного чтения или записи данных считается завершенной, когда выполнен соответствующий синхронный ввод или вывод и модифицированы все ассоциированные поля состояния (например, время последнего доступа к файлу или последнего изменения файла).
За исключением параллельной работы с инициировавшим их приложением и интерфейсных отличий, операции асинхронного ввода/вывода ведут себя так же, как и обычные функции read(), write(), lseek() и fsync(). Выдать запрос на асинхронный ввод/вывод – все равно, что создать отдельный поток управления, который неделимым образом осуществит позиционирование в файле и обмен данными.
Параллельные асинхронные и синхронные операции обмена с одним и тем же файлом изменяют его так, как если бы они выполнялись последовательно.
После завершения асинхронной операции ввода/вывода приложению может быть доставлен сигнал, который можно интерпретировать как разрешение на доступ к читаемым данным и повторное использование задействованных в операции буферов и управляющих блоков.
«Жизненный цикл» операции асинхронного ввода/вывода включает два этапа:

  • постановка запроса в очередь;
  • выполнение запроса, осуществление ввода/вывода.

После завершения каждого из этапов приложение получает возвращаемое значение и статус ошибки, только по завершении второго этапа они извлекаются особым образом, но возвращаемое значение имеет естественный смысл – такое же значение (число переданных байт) вернула бы обычная синхронная операция ввода/вывода.
Пока операция не завершена, ее статус ошибки имеет значение EINPROGRESS. Теоретически, приложение может дожидаться завершения асинхронной операции, периодически опрашивая статус ошибки, пока не получит значение, отличное от EINPROGRESS.
Из общих соображений следует, что если есть возможность поставить запрос в очередь, должны предоставляться средства для последующего удаления его из очереди (изменились обстоятельства, запрос стал не нужен). Стандарт POSIX-2001 удовлетворяет этому требованию.
Полезно иметь возможность за один вызов поставить в очередь целый список предварительно сформированных запросов, специфицирующих операции ввода/вывода, вообще говоря, с разными файлами. Стандарт POSIX-2001 позволяет и это.
Отметим, что потенциальное распараллеливание работы приложения и функций ввода/вывода дает реальный выигрыш в эффективности только при наличии аппаратной поддержки параллелизма. Для многих систем, функционирующих в режиме реального времени, такая поддержка имеется, так что приложение, пользуясь средствами асинхронного ввода/вывода, может мобильным образом полностью загрузить устройства ввода/вывода, не жертвуя при этом скоростью вычислений.
Наиболее важным объектом, обслуживающим асинхронный ввод/вывод, является управляющий блок, оформленный в стандарте POSIX-2001 как структура типа aiocb со следующими полями.
int aio_fildes; // Файловый дескриптор
off_t aio_offset; // Позиция в файле
volatile void *aio_buf; // Адрес буфера
size_t aio_nbytes; // Число передаваемых
// байт
int aio_reqprio; // Величина понижения
// приоритета
struct sigevent aio_sigevent; // Номер и
// значение
// сигнала
int aio_lio_opcode; // Запрошенная
// операция
Значением элемента aio_fildes является дескриптор файла, над которым выполняется асинхронная операция.
Элемент aio_offset задает (абсолютную) позицию в файле, начиная с которой будет производиться ввод/вывод. Если для файлового дескриптора установлен флаг O_APPEND или заданное дескриптором устройство не поддерживает позиционирования, операции записи добавляют данные в конец.
Поля aio_buf и aio_nbytes имеют тот же смысл, что и соответствующие аргументы функций read() и write().
Если реализация поддерживает приоритетное планирование, очередь запросов на асинхронный ввод/вывод упорядочивается в соответствии с текущими приоритетами вызывающих процессов. Элемент aio_reqprio позволяет понизить (но не повысить) приоритет запроса. Его значение должно находиться в диапазоне от нуля до AIO_PRIO_DELTA_MAX включительно; оно вычитается из приоритета процесса.
Элемент aio_sigevent, имеющий структурный тип sigevent, определяет способ уведомления вызывающего процесса о завершении операции ввода/вывода. Если значение aio_sigevent.sigev_notify равно SIGEV_NONE, никаких сигналов по завершении генерироваться не будет, но возвращаемое значение и статус ошибки будут сформированы должным образом.
Элемент aio_lio_opcode используется только в списках запросов и специфицирует операцию чтения или записи.
Адрес управляющего блока играет роль идентификатора, позволяющего получить доступ к возвращаемому значению и статусу ошибки.
Управляющий блок и буфера данных, ассоциированные с операцией асинхронного ввода/вывода, используются системой тогда и только тогда, когда статус ошибки имеет значение EINPROGRESS. Разумеется, приложение не должно модифицировать в это время структуру aiocb.
Функции асинхронного ввода/вывода
Основными операциями асинхронного ввода/вывода являются чтение и запись данных (
#include <aio.h>
int aio_read (struct aiocb *aiocbp);
int aio_write (struct aiocb *aiocbp);
Функция aio_read() инициирует запросначтение aiocbp->aio_nbytes байт из файла, ассоциированного с дескриптором aiocbp->aio_fildes, начиная с абсолютной позиции aiocbp->aio_offset, в буфер с адресом aiocbp->aio_buf. Возврат из функции произойдет тогда, когда запрос будет принят к исполнению или поставлен в очередь; при этом нормальный результат равен нулю, а значение -1 (в совокупности с errno) свидетельствует об ошибке (например, исчерпаны ресурсы и запрос не удалось поставить в очередь).
Функция aio_write() осуществляет соответствующие действия для записи данных. Если для файлового дескриптора aiocbp->aio_fildes установлен флаг O_APPEND, запись будет производиться в конец файла.
Функция lio_listio()) позволяет за один вызов инициировать (поставить в очередь) список запросов на чтение и/или запись данных.
#include <aio.h>
int lio_listio (
int mode, struct aiocb *restrict const
listio [restrict], int nent,
struct sigevent *restrict sigev);
Элементы массива listio [] специфицируют отдельные запросы. В полях aio_lio_opcode указуемых структур типа aiocb могут располагаться значения LIO_READ (запрос на чтение), LIO_WRITE (запрос на запись) или LIO_NOP (пустой запрос). Остальные элементы указуемых структур интерпретируются в соответствии со смыслом запроса (можно считать, что адреса структур передаются в качестве аргументов функциям aio_read() или aio_write()). Число элементов в массиве listio [] задается аргументом nent.
Значение аргумента mode определяет способ обработки списка запросовсинхронный (LIO_WAIT) или асинхронный (LIO_NOWAIT). В первом случае возврат из вызова lio_listio() произойдет только после завершения всех заказанных операций ввода/вывода; при этом аргумент sigev игнорируется. Во втором случае возврат из lio_listio() произойдет немедленно, а по завершении всех операций ввода/вывода в соответствии со значением аргумента sigev будет сгенерировано асинхронное уведомление.
Нормальный результат выполнения функции lio_listio() равен нулю, но для двух описанных случаев он имеет разный смысл – успешное завершение всех операций ввода/вывода или только успешная постановка запросов в очередь соответственно. Во втором случае часть операций ввода/вывода может завершиться неудачей, и с каждой из них придется разбираться индивидуально. Подобное разбирательство позволяют осуществить функции aio_return() и aio_error()).
#include <aio.h>

ssize_t aio_return (
struct aiocb *aiocbp);

int aio_error (
const struct aiocb *aiocbp);
Аргументом этих функций служит адрес управляющего блока, который играет в данном случае роль идентификатора запроса. Функция aio_return() выдает значение, которое вернула бы соответствующая функция обычного ввода/вывода (если операция ввода/вывода в момент опроса не была завершена, возвращаемое значение, разумеется, не определено). К идентификатору данного запроса функцию aio_return() можно применить ровно один раз; последующие вызовы aio_return() и aio_error() могут завершиться неудачей. (Конечно, если в той же указуемой структуре типа aiocb сформировать новый запрос и выполнить его, возможность корректного вызова функций aio_return() и aio_error() появляется снова.)
Результатом вызова aio_error() служит значение, которое установила бы в переменную errno соответствующая функция обычного ввода/вывода (или, если операция еще не завершена, результат равен EINPROGRESS). Если операция асинхронного ввода/вывода завершилась успешно, aio_error() вернет нулевой результат. Функцию aio_error() к идентификатору данного запроса можно применять произвольное число раз, и до завершения его выполнения, и после.
Ожидающие обработки запросы на асинхронный ввод/вывод можно аннулировать, воспользовавшись функцией aio_cancel() (
#include <aio.h>
int aio_cancel (int fildes,
struct aiocb *aiocbp);
Функция aio_cancel() пытается аннулировать один (если значение аргумента aiocbp отлично от NULL) или все ожидающие обработки запросы, в которых фигурирует файловый дескриптор fildes. После аннулирования генерируется соответствующее асинхронное уведомление, статус ошибки устанавливается равным ECANCELED, а в качестве возвращаемого значения выдается -1.
Если запрос не удается аннулировать (возможные причины этого, вообще говоря, зависят от реализации), он выполняется стандартным образом.
Результат вызова aio_cancel() равен AIO_CANCELED, если все указанные запросы аннулированы. Если хотя бы один из запросов уже выполняется, он не может быть аннулирован, и тогда в качестве результата возвращается значение AIO_NOTCANCELED (а со статусом запросов, как и для функции lio_listio(), нужно разбираться индивидуально, пользуясь функцией aio_error()). Результат AIO_ALLDONE свидетельствует о том, что выполнение всех указанных запросов не только начато, но и уже завершено. Во всех остальных случаях результат вызова aio_cancel() равен -1.


Согласно стандарту POSIX-2001, списком можно не только инициировать запросы на асинхронный ввод/вывод, но и ожидать их завершения. Для этого служит функция aio_suspend() (
#include <aio.h>
int aio_suspend (
const struct aiocb *const listio [],
int nent,
const struct timespec *timeout);
Поток управления, вызвавший aio_suspend(), приостанавливает выполнение на время, не большее, чем задано аргументом timeout, до тех пор пока не завершится по крайней мере одна из операций асинхронного ввода/вывода, на которую есть ссылка из массива listio (nent – число его элементов), или ожидание не будет прервано доставкой сигнала. Если какое-либо из перечисленных условий истинно в момент вызова, он завершается без задержек.
Нулевой результат свидетельствует о завершении по крайней мере одной из операций ввода/вывода. Какой именно, как обычно, нужно выяснять индивидуально, пользуясь функциями aio_error() и aio_return().
Описываемая далее группа функций) предназначена для согласования состояния буферизованных и хранимых в долговременной памяти данных, обеспечения целостности данных и файлов. Подобные функции необходимы в системах обработки транзакций, чтобы гарантировать определенное состояние долговременной памяти, даже если система аварийно завершит работу. Один элемент из этой группы – функцию msync() – мы уже рассматривали.
#include <unistd.h>
void sync (void);

#include <unistd.h>
int fsync (int fildes);

#include <unistd.h>
int fdatasync (int fildes);

#include <aio.h>
int aio_fsync (int op,
struct aiocb *aiocbp);
Функция sync() имеет глобальный характер – она планирует запись в файловые системы всех измененных данных.
Функция fsync() обеспечивает запись измененных данных в файл, заданный дескриптором fildes. Более того, после успешного возврата из функции (с нулевым результатом) окажутся выполненными все стоявшие в очереди к этому файлу запросы на ввод/вывод с гарантией целостности файла.
Функция fdatasync() аналогична fsync(), но гарантируется лишь целостность данных (см. курс [1]).
Функция aio_fsync() осуществляет «асинхронную синхронизацию» файла. Иными словами, порождается запрос, выполнение которого при значении аргумента op, равном O_DSYNC, эквивалентно вызову fdatasync (aiocbp->aio_fildes), а при op, равном O_SYNC, – fsync (aiocbp->aio_fildes). Как и для других операций асинхронного ввода/вывода, адрес управляющего блока может использоваться в качестве аргумента функций aio_error() и aio_return(), а после завершения генерируется асинхронное уведомление в соответствии со значением элемента aiocbp->aio_sigevent. Все остальные поля указуемой структуры типа aiocb игнорируются.
Проиллюстрируем использование функций асинхронного ввода/вывода программой, подсчитывающей суммарное число строк в совокупности файлов
Отметим списочную форму начального представления и ожидания выполнения запросов на асинхронное чтение. Из технических деталей обратим внимание на пустой указатель в качестве элемента массива указателей на структуры типа aiocb (функция lio_listio() игнорирует такие элементы) и в качестве аргумента timeout функции aio_suspend() (означает бесконечное ожидание).
Второй вариант решения той же задачи (демонстрирует другой способ уведомления о завершении операции асинхронного ввода/вывода – вызов функции. В модифицированном варианте можно отметить применение переменных условия как средства ожидания завершения «вдвойне асинхронной» обработки файлов (по вводу/выводу и по выполнению функций процессирования прочитанных данных в рамках специально создаваемых потоков управления). Когда очередной файл закрывается, то для генерации уведомления о возможном завершении всей обработки вызывается функция pthread_cond_signal(). Обратим внимание на то, что в этот момент вызывающий поток не является владельцем мьютекса, ассоциированного с переменной условия, но в данном случае это и не требуется.

Рекомендательные интерфейсы
Напомним, что рекомендательные интерфейсы – это средство проинформировать операционную систему о поведении мобильного приложения, чтобы ОС могла принять меры для оптимизации его (приложения) обслуживания.
В стандарте POSIX-2001 предусмотрена только оптимизация работы с файлами, которая может затрагивать следующие аспекты осуществляемого приложением ввода/вывода:

  • последовательный доступ;
  • кэширование;
  • передача данных;
  • предварительное резервирование долговременной памяти.

Если операционная система знает, что приложение осуществляет последовательный доступ к файлу, ей целесообразно производить предвыборку данных и отводить долговременную память под файл также последовательно. Очевидна в таком случае и дисциплина работы с кэшем данных. Напротив, при случайном доступе предвыборка данных только повредит, следует читать лишь то, что необходимо.
Приложение может проинформировать ОС и о том, что данные не подлежат повторному использованию. Следовательно, их не нужно кэшировать, предпочтителен прямой обмен с пользовательскими буферами (которые должны быть определенным образом выравнены в оперативной памяти и иметь подходящий размер).
Описанные возможности реализуют функции posix_fadvise(), posix_fallocate(), posix_madvise() и posix_memalign()).
#include <fcntl.h>
int posix_fadvise (int fd,
off_t offset,
size_t len,
int advice);

#include <fcntl.h>
int posix_fallocate (int fd,
off_t offset,
size_t len);

#include <sys/mman.h>
int posix_madvise (void *addr,
size_t len,
int advice);

#include <stdlib.h>
int posix_memalign (void **memptr,
size_t alignment,
size_t size);
Листинг 7.9. Описание функций рекомендательных интерфейсов.
Функция posix_fadvise() информирует реализацию об ожидаемом поведении приложения по отношению к части файла, ассоциированного с открытым дескриптором fd, которая начинается с позиции offset и имеет длину len байт (если значение аргумента len равно нулю, рекомендация распространяется до конца файла).
Ожидаемое поведение специфицирует аргумент advice, который может принимать следующие значения.
POSIX_FADV_NORMAL
Подразумеваемое поведение – отсутствие рекомендаций.
POSIX_FADV_SEQUENTIAL
Предполагается, что приложение будет осуществлять доступ к указанной части файла последовательно, от меньших смещений к большим.
POSIX_FADV_RANDOM
Специфицирует случайный доступ.
POSIX_FADV_WILLNEED
Предполагается, что данные из указанной части файла скоро понадобятся.
POSIX_FADV_DONTNEED
Предполагается, что данные из указанной части файла в ближайшее время не понадобятся.
POSIX_FADV_NOREUSE
Специфицирует однократный доступ.
Нормальный результат функции posix_fadvise() равен нулю; при обнаружении ошибки возвращается ее номер.

Функция posix_fallocate() резервирует долговременную память для указанной части файла. После успешного завершения ее работы запись в эту часть файла не может кончиться неудачей из-за нехватки свободного пространства.
Если сумма (offset + len) превышает текущий размер файла, он будет соответственно увеличен; в противном случае размер не изменится.
Влияние предыдущих вызовов posix_fadvise() на стратегию выделения долговременной памяти зависит от реализации.
Функция posix_madvise() информирует реализацию об ожидаемом поведении приложения по отношению к части адресного пространства вызывающего процесса, которая начинается с адреса addr и имеет длину len байт. Подобная информация может оказаться полезной, если в указанную область памяти отображен файл. (Реализация имеет право требовать, чтобы область начиналась с границы страницы.)
Ожидаемое поведение специфицирует аргумент advice, который может принимать следующие значения.
POSIX_MADV_NORMAL
Подразумеваемое поведение – отсутствие рекомендаций.
POSIX_MADV_SEQUENTIAL
Предполагается, что приложение будет осуществлять доступ к указанной части адресного пространства последовательно, от меньших адресов к большим.
POSIX_MADV_RANDOM
Специфицирует случайный доступ.
POSIX_MADV_WILLNEED
Предполагается, что данные из указанной части адресного пространства скоро понадобятся.
POSIX_MADV_DONTNEED
Предполагается, что данные из указанной части адресного пространства в ближайшее время не понадобятся.
Функция posix_memalign() служит для резервирования области памяти длины size с начальной границей, выровненной в соответствии со значением аргумента alignment. Результирующий адрес записывается по указателю memptr. Значение alignment должно быть кратным величине sizeof (void *), которая является степенью двойки.
Область памяти, зарезервированную посредством вызова posix_memalign(), в дальнейшем можно освободить с помощью функции free().
При оптимизации обмена данными с файлами, наряду с применением рекомендательных интерфейсов, целесообразно учитывать значения следующих конфигурационных констант, определенных в заголовочном файле <limits.h>, которые можно опросить, обращаясь к функции pathconf().
POSIX_ALLOC_SIZE_MIN
Минимальное число байт долговременной памяти, реально выделяемое для любой части файла.
POSIX_REC_MIN_XFER_SIZE
Минимальное рекомендуемое число передаваемых байт при обмене данными с файлами. Рекомендуется также, чтобы и смещение передаваемой порции данных от начала файла было кратным POSIX_REC_MIN_XFER_SIZE.
POSIX_REC_MAX_XFER_SIZE
Максимальное рекомендуемое число передаваемых байт при обмене данными с файлами.
POSIX_REC_INCR_XFER_SIZE
Рекомендуемое приращение числа передаваемых байт при обмене данными с файлами (в диапазоне от POSIX_REC_MIN_XFER_SIZE до POSIX_REC_MAX_XFER_SIZE).
POSIX_REC_XFER_ALIGN
Рекомендуемое значение для выравнивания границы буфера обмена данными с файлами.
Применение функций рекомендательных интерфейсов проиллюстрируем программой, подсчитывающей сумму байт в файле).
Листинг 7.10. Пример программы, использующей функции рекомендательных интерфейсовВ этой программе задействовано несколько оптимизирующих факторов. Во-первых, посредством вызовов posix_fadvise() системе сообщается, что ко всему файлу (от начала до конца) будет осуществляться только однократный последовательный доступ. Во-вторых, память под буфер обмена резервируется с соблюдением рекомендаций на выравнивание и размер. Читателю предлагается самостоятельно определить реальный вклад каждого оптимизирующего фактора.

 

 
На главную | Содержание | < Назад....Вперёд >
С вопросами и предложениями можно обращаться по nicivas@bk.ru. 2013 г.Яндекс.Метрика