Linux aio что это

Обновлено: 07.07.2024

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

4 ответа

Сетевой ввод-вывод не является приоритетом для AIO, потому что каждый, кто пишет сетевые серверы POSIX, использует неблокирующий подход, основанный на событиях. Подход Java в старом стиле «миллиарды блокирующих потоков» ужасно отстой.

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

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

Таким образом, в конечном итоге POSIX AIO оказывается в положении, не служащем какой-либо полезной цели. Не используйте это.

А как насчет чтения / записи из сетевых (NFS, Samba) файловых систем? Хорошо. У меня есть несколько больших тупых писателей, которые, если я позволю им перейти в кеш, будут на пике использовать dirty_ratio, блокируя всех остальных. Если я просто использую для них прямой ввод-вывод, это будет слишком медленно. Если бы у меня был только один поток, я мог бы управлять самостоятельно, но было бы сложно поддерживать разные приоритеты ввода-вывода в одном потоке. AIO + CFQ действительно кажутся хорошей комбинацией, если бы AIO работал Я не согласен. Дисковый ввод-вывод обычно буферизуется, но может блокировать. Когда poll () обрабатывает файловый FD, он всегда сообщает, что FD доступен для чтения, даже если он будет заблокирован. Это делает невозможным выполнение неблокирующих операций с файлами на диске равномерно, если только не используются потоки или AIO. @Matt: порядок не важен для сокетов дейтаграмм. @Zan: асинхронный ввод-вывод очень хорош для предварительной буферизации потоковых данных в реальном времени, например медиаплееры. Неправда, что AIO бесполезен в системах, основанных на событиях. Фактически вы можете получить сеть с нулевым копированием с помощью надлежащего AIO, чего нельзя добиться с помощью уведомления на основе событий для recv (). Другие вещи могут сговориться, чтобы сделать это в основном теоретическим ограничением, но я думаю, что отсутствие надлежащего AIO (а-ля OVERLAPPED в Windows) - одна из последних больших дыр в Linux.

Эффективное выполнение ввода-вывода сокетов было решено с помощью kqueue, epoll, портов завершения ввода-вывода и т.п. Выполнение асинхронного файлового ввода-вывода - это своего рода поздний этап (не считая перекрывающегося ввода-вывода Windows и ранней поддержки posix AIO в Solaris).

Если вы ищете возможность ввода-вывода через сокеты, вам, вероятно, лучше использовать один из вышеперечисленных механизмов.

Таким образом, основная цель AIO - решить проблему асинхронного дискового ввода-вывода. Скорее всего, поэтому Mac OS X поддерживает только AIO для обычных файлов, а не сокетов (поскольку kqueue в любом случае делает это намного лучше).

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

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

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

При этом у posix AIO довольно неудобный интерфейс, например:

  • Единственное эффективное и хорошо поддерживаемое средство обратных вызовов событий - это сигналы, что затрудняет использование в библиотеке, поскольку это означает использование номеров сигналов из глобального пространства имен сигналов. Если ваша ОС не поддерживает сигналы в реальном времени, это также означает, что вам нужно перебрать все невыполненные запросы, чтобы выяснить, какой из них на самом деле завершен (например, Mac OS X, а не Linux). Перехват сигналов в многопоточной среде также накладывает некоторые хитрые ограничения. Обычно вы не можете реагировать на событие внутри обработчика сигнала, но вы должны поднять сигнал, записать в канал или использовать signalfd () (в Linux).
  • lio_suspend () имеет те же проблемы, что и select (), он не очень хорошо масштабируется с количеством заданий.
  • lio_listio () в том виде, в котором она реализована, имеет довольно ограниченное количество заданий, которые вы можете передать, и найти этот предел переносимым способом нетривиально. Вы должны вызвать sysconf (_SC_AIO_LISTIO_MAX), что может привести к сбою, и в этом случае вы можете использовать определение AIO_LISTIO_MAX, которое не обязательно определено, но затем вы можете использовать 2, который определен как гарантированно поддерживаемый.

Большинство платформ posix на данный момент поддерживают posix AIO (Linux, BSD, Solaris, AIX, tru64). Windows поддерживает его через перекрывающийся файловый ввод-вывод. Насколько я понимаю, только Solaris, Windows и Linux действительно поддерживают асинхронный режим. файловый ввод-вывод вплоть до драйвера, тогда как другие операционные системы эмулируют асинхронный режим. Ввод-вывод с потоками ядра. Linux является исключением, его реализация posix AIO в glibc эмулирует асинхронные операции с потоками пользовательского уровня, тогда как его собственный интерфейс асинхронного ввода-вывода (io_submit () и т. Д.) Действительно асинхронен на всем пути вплоть до драйвера, если драйвер поддерживает его. .

Я считаю, что среди операционных систем довольно распространено не поддерживать posix AIO для любого fd, а ограничивать его обычными файлами.

Принцип AIO модели сетевого программирования нового поколения Java и внедрение системы Linux AIO

Предисловие

Поскольку реализация AIO должна полностью вызывать ОС для участия, IO требуется поддержка операционной системы, а параллелизм также требует поддержки операционной системы, поэтому производительность различных операционных систем будет более очевидной. В этой статье также представлены новые функции AIO в Linux 2.6 и более поздних версиях (поскольку это соответствует Java AIO).

Java AIO

1 Фундаментальный

Пока что Java поддерживает 3 модели сетевого программирования: BIO, NIO, AIO:

Анализ сценариев приложений BIO, NIO, AIO:

  • Метод BIO подходит для относительно небольшого числа соединений и фиксированной архитектуры. Этот метод требует относительно больших ресурсов сервера, а параллелизм ограничен приложениями. Единственный вариант до JDK1.4, но программа интуитивно понятна и проста для понимания.
  • Метод NIO подходит для архитектур с большим количеством подключений и относительно коротких (легкая работа) подключений, таких как серверы чата. Параллелизм ограничен приложениями, а программирование более сложным. JDK1.4 начал поддерживать.
  • Метод AIO используется в архитектурах с большим количеством подключений и относительно длительным подключением (тяжелая операция), таких как сервер фотоальбомов, который полностью вызывает ОС для участия в параллельных операциях, а программирование является более сложным. JDK7 начал поддерживать.

2 Введение в AIO


jdk обеспечивает поддержку неблокирующей мультиплексированной синхронной модели io в версии nio 1.4, но она реализована на основе менее эффективного выбора / опроса в Windows.

Поддержка aio в jdk1.7 дает два преимущества:

  • Вы можете использовать iocp в Windows.
  • Упростил сеть в виде модели. По сравнению с моделью неблокирующего мультиплексирования, асинхронный io легче понять и легче разработать.


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

Новые классы и интерфейсы java aio в JDK7 в основном включают:


В дополнение к методу обратного вызова CompletionHandler, aio также поддерживает возврат объектов Future и использование Future для установки операций обратного вызова.

Linux AIO

1 Введение в Linux AIO


Асинхронный ввод-вывод Linux - довольно новое усовершенствование, представленное в ядре Linux. Это стандартная функция ядра 2.6, но мы также можем найти ее в патче ядра 2.4. Основная идея AIO заключается в том, чтобы позволить процессу инициировать множество операций ввода-вывода без блокировки или ожидания завершения какой-либо операции. Позже или при получении уведомления о завершении операции ввода-вывода процесс может получить результаты операции ввода-вывода.

2 Модель ввода-вывода Linux


Прежде чем углубиться в AIO API, давайте сначала рассмотрим различные модели ввода-вывода, доступные в Linux. Это не исчерпывающее введение, но мы попытаемся представить некоторые из наиболее часто используемых моделей, чтобы объяснить разницу между ними и асинхронным вводом-выводом. На рисунке 1 показаны синхронные и асинхронные модели, а также модели с блокировкой и без блокировки.

Простая матрица базовых моделей ввода-вывода Linux:


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

Java AIO Linux AIO _QQ20160514-0.jpg

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

С точки зрения приложения вызов чтения будет длиться долго. Фактически, когда ядро ​​выполняет операции чтения и другую работу, приложение блокируется.

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

Неблокирующая реализация заключается в том, что команда ввода-вывода не может быть удовлетворена немедленно, и приложению необходимо вызывать много раз, чтобы дождаться завершения операции. Это может быть неэффективным, потому что во многих случаях, когда ядро ​​выполняет эту команду, приложение должно ждать, пока данные станут доступны, или попытаться выполнить другие задачи. Как показано на рисунке 3, этот метод может вызывать задержки в операциях ввода-вывода, потому что существует определенный интервал между тем, когда данные становятся доступными в ядре, и когда пользователь вызывает read для возврата данных, что снизит общую пропускную способность данных. .

Java AIO Linux AIO _QQ20160514-1.jpg

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

Основная проблема с вызовом select в том, что он не очень эффективен. Хотя это удобная модель для асинхронных уведомлений, она не рекомендуется для высокопроизводительных операций ввода-вывода.

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

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

3 Мотивация для асинхронного ввода-вывода (AIO)


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

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

подводить итоги


Использование асинхронного ввода-вывода (AIO) может помочь нам создавать приложения с более быстрым и эффективным вводом-выводом. Если наше приложение может перекрывать обработку и операции ввода-вывода, то AIO может помочь нам создавать приложения, которые могут более эффективно использовать доступные ресурсы ЦП.

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


Недавно внимание автора привлекла статья на LWN о новом интерфейсе ядра для опроса (polling). В ней обсуждается новый механизм опроса в Linux AIO API (интерфейс для асинхронной работы с файлами), который добавили в ядро версии 4.18. Идея довольно интересная: автор патча предлагает использовать Linux AIO API для работы с сетью.

Но постойте! Ведь Linux AIO был создан для работы с асинхронным вводом-выводом с диска / на диск! Файлы на диске — это не то же самое, что сетевые соединения. Возможно ли вообще использовать Linux AIO API для работы с сетью?

Оказывается, да, возможно! В этой статье объясняется, как использовать сильные стороны Linux AIO API для создания более быстрых и лучших сетевых серверов.

Но давайте начнём с разъяснения, что представляет собой Linux AIO.

Linux AIO предоставляет интерфейс асинхронного ввода-вывода с диска / на диск для пользовательского ПО.

Исторически на Linux все дисковые операции блокировались. Если вы вызываете open() , read() , write() или fsync() , то поток останавливается до тех пор, пока метаданные не появятся в дисковом кеше. Обычно это не вызывает проблем. Если у вас не много операций ввода-вывода и достаточно памяти, системные вызовы постепенно заполнят кеш, и всё будет работать достаточно быстро.

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

Для решения этой проблемы приложения могут использовать три способа:

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

Теперь, когда мы знаем о слабых сторонах Linux AIO API, давайте рассмотрим его сильные стороны.

Простая программа с использованием Linux AIO

Для того чтобы использовать Linux AIO, вам сначала придётся самостоятельно определить все пять необходимых системных вызовов — glibc их не предоставляет.

  1. Сначала нужно вызвать io_setup() для инициализации структуры aio_context . Ядро вернёт нам непрозрачный (opaque) указатель на структуру.
  2. После этого можно вызвать io_submit() , чтобы добавить в очередь на обработку вектор «контрольных блоков ввода-вывода» в виде структуры struct iocb.
  3. Теперь, наконец, мы можем вызвать io_getevents() и ждать от неё ответа в виде вектора структур struct io_event — результатов работы каждого из блоков iocb.


Структура iocb , которая передаётся в функцию io_submit , достаточно крупная и предназначена для работы с диском. Вот её упрощённая версия:


Полная структура io_event , которую возвращает io_getevents :


Пример. Простая программа, которая читает файл /etc/passwd с помощью Linux AIO API:


Полные исходники, конечно, доступны на GitHub. Вот вывод strace этой программы:


Всё прошло хорошо, но чтение с диска не было асинхронным: вызов io_submit заблокировался и выполнил всю работу, функция io_getevents выполнилась мгновенно. Мы могли попробовать читать асинхронно, но это требует флага O_DIRECT, с которым дисковые операции идут в обход кеша.

Давайте лучше проиллюстрируем то, как io_submit блокируется на обычных файлах. Вот аналогичный пример, который показывает вывод strace в результате чтения блока объёмом 1 Гб из /dev/zero :


Ядро потратило 738 мс на вызов io_submit и только 15 нс — на io_getevents . Подобным образом оно ведёт себя и с сетевыми соединениями — вся работа делается io_submit .



Фото Helix84 CC/BY-SA/3.0

Linux AIO и сеть

Реализация io_submit достаточно консервативна: если переданный дескриптор файла не был открыт с флагом O_DIRECT, то функция просто блокируется и выполняет указанное действие. В случае с сетевыми соединениями это означает, что:

  • для блокирующих соединений IOCV_CMD_PREAD будет ждать ответного пакета;
  • для неблокирующих соединений IOCB_CMD_PREAD вернёт код -11 (EAGAIN).

Важно отметить, что запросы iocb выполняются ядром последовательно.

Несмотря на то, что Linux AIO не поможет нам с асинхронными операциями, его можно использовать для объединения системных вызовов в пакеты (batches).

Один буфер
Несколько буферов
Один файловый дескриптор
read()
readv()
Несколько файловых дескрипторов
io_submit + IOCB_CMD_PREAD
io_submit + IOCB_CMD_PREADV

Для иллюстрации группировки системных вызовов в пакеты с помощью io_submit давайте напишем небольшую программу, которая пересылает данные из одного TCP-соединения в другое. В простейшей форме (без Linux AIO) она выглядит примерно так:


Тот же функционал мы можем выразить через Linux AIO. Код в этом случае будет таким:


Этот код добавляет два задания в io_submit : сначала запрос на запись в sd2 , а потом запрос на чтение из sd1. После выполнения чтения код исправляет размер буфера записи и повторяет цикл сначала. Есть одна хитрость: первый раз запись происходит с буфером размера 0. Это необходимо потому, что у нас есть возможность объединить write + read в одном вызове io_submit (но не read + write).

Быстрее ли этот код, чем обычные read() / write() ? Пока нет. Обе версии используют два системных вызова: read + write и io_submit + io_getevents. Но, к счастью, код можно улучшить.

Избавляемся от io_getevents

Во время выполнения io_setup() ядро выделяет несколько страниц памяти для процесса. Вот как этот блок памяти выглядит в /proc//maps:


Блок памяти [aio] (12 Кб в данном случае) был выделен io_setup . Он используется для кольцевого буфера, где хранятся события. В большинстве случаев нет причин для вызова io_getevents — данные о завершении событий можно получить из кольцевого буфера без необходимости перехода в режим ядра. Вот исправленная версия кода:


Полная версия кода доступна на GitHub. Интерфейс этого кольцевого буфера плохо документирован, автор адаптировал код из проекта axboe/fio.

После этого изменения наша версия кода с использованием Linux AIO требует только одного системного вызова в цикле, что делает её чуть быстрее, чем оригинальный код с использованием read + write.



Фото Train Photos CC/BY-SA/2.0

Альтернатива epoll

С добавлением IOCB_CMD_POLL в ядро версии 4.18 стало возможным использование io_submit в качестве замены select/poll/epoll. Например, этот код будет ожидать данных от сетевого соединения:


Полный код. Вот его вывод strace:


Как видите, в этот раз асинхронность сработала: io_submit выполнилась мгновенно, а io_getevents заблокировалась на одну секунду в ожидании данных. Это можно использовать вместо системного вызова epoll_wait() .

Более того, работа с epoll обычно требует использования системных вызовов epoll_ctl. А разработчики приложений стараются избегать частых вызовов этой функции — чтобы понять причины, достаточно прочитать в мануале о флагах EPOLLONESHOT и EPOLLET. Используя io_submit для опроса соединений, можно избежать этих сложностей и дополнительных системных вызовов. Просто добавьте соединения в вектор iocb, вызовите io_submit один раз и ожидайте выполнения. Всё очень просто.

Резюме

В этом посте мы рассмотрели Linux AIO API. Этот API изначально задумывался для работы с диском, но он работает также и с сетевыми соединениями. Однако, в отличие от обычных вызовов read() + write(), использование io_submit позволяет группировать системные вызовы и таким образом увеличивать производительность.

Начиная с ядра версии 4.18 io_submit и io_getevents в случае с сетевыми соединениями могут быть использованы для событий вида POLLIN и POLLOUT. Это является альтернативой epoll() .

Могу себе представить сетевой сервис, который использует только io_submit и io_getevents вместо стандартного набора read, write, epoll_ctl и epoll_wait. В этом случае группировка системных вызовов в io_submit может дать большое преимущество, такой сервер был бы значительно быстрее.

К сожалению, даже после недавних улучшений Linux AIO API дискуссия о его полезности продолжается. Хорошо известно, что Линус его ненавидит:

«AIO — это ужасный пример дизайна «на коленке», где основное оправдание: «другие, менее одарённые люди придумали это, поэтому мы вынуждены соблюдать совместимость ради того, чтобы разработчики баз данных (которые редко обладают вкусом) могли использовать это». Но AIO всегда был очень-очень кривым».

Было предпринято несколько попыток создать лучший интерфейс для группировки вызовов и асинхронности, однако им не хватило общего видения. Например, недавнее добавление sendto(MSG_ZEROCOPY) позволяет вести действительно асинхронную передачу данных, но не предусматривает группировки. io_submit предусматривает группировку, но не асинхронность. Ещё хуже — в Linux на данный момент есть три способа доставки асинхронных событий: сигналы, io_getevents и MSG_ERRQUEUE.

В любом случае, отлично, что появляются новые способы ускорить работу сетевых сервисов.

Интерфейс POSIX AIO состоит из следующих функций:

aio_read(3) Ставит запрос на чтение в очередь. Это асинхронный аналог read(2). aio_write(3) Ставит запрос на запись в очередь. Это асинхронный аналог write(2). aio_fsync(3) Ставит запрос синхронизации операций ввода-вывода над файловым дескриптором. Это асинхронный аналог fsync(2) и fdatasync(2). aio_error(3) Возвращает информацию о состоянии поставленного в очередь запроса ввода-вывода. aio_return(3) Возвращает информацию о выполненном запросе ввода-вывода. aio_suspend(3) Приостанавливает вызывающего до тех пор, пока не выполнится один или более указанных запросов ввода-вывода. aio_cancel(3) Пытается отменить ожидающие выполнения запросы ввода-вывода над заданным файловым дескриптором. lio_listio(3) Ставит в очередь сразу несколько запросов ввода-вывода за один вызов функции.

В структуре aiocb («блок управления асинхронным вводом-выводом») задаются параметры, которые управляют операцией ввода-вывода. Аргумент данного типа передаётся во все функции, перечисленные ранее. Данная структура имеет следующий вид:

Поля этой структуры имеют следующее назначение: aio_filedes Файловый дескриптор, над которым будут выполняться операции ввода-вывода. aio_offset Файловое смещение, начиная с которого будут выполняться операции ввода-вывода. aio_buf Буфер, используемый для пересылки данных при операции чтения или записи. aio_nbytes Размер буфера, на который указывает aio_buf. aio_reqprio В этом поле задаётся значение, которое вычитается из приоритета реального времени вызывающей нити, чтобы определить приоритет выполнения данного запроса ввода-вывода (смотрите pthread_setschedparam(3)). Указываемое значение должно быть в диапазоне от 0 и до значения, возвращаемого sysconf(_SC_AIO_PRIO_DELTA_MAX). Данное поле игнорируется при операциях синхронизации файла. aio_sigevent В этом поле задаётся структура, которая указывает как вызывающему должно быть сообщено о завершении анонимной операции ввода-вывода. Возможные значения для aio_sigevent.sigev_notify: SIGEV_NONE, SIGEV_SIGNAL и SIGEV_THREAD. Подробности смотрите в sigevent(7). aio_lio_opcode Задаёт тип операции, которая будет выполнена; используется только в lio_listio(3).

В дополнении к стандартным функциям, перечисленным ранее, в библиотеке GNU C есть следующее расширение программного интерфейса POSIX AIO:

aio_init(3) Позволяет изменить настройки поведения реализации glibc для POSIX AIO.

ОШИБКИ

EINVAL Значение поля aio_reqprio структуры aiocb меньше 0 или больше, чем значение ограничения, возвращаемое вызовом sysconf(_SC_AIO_PRIO_DELTA_MAX).

ВЕРСИИ

Интерфейсы POSIX AIO появились в glibc в версии 2.1.

СООТВЕТСТВИЕ СТАНДАРТАМ

ЗАМЕЧАНИЯ

Желательно обнулять буфер блока управления перед использованием (смотрите memset(3)). Буфер блока управления и буфер, который задаётся в aio_buf, не должны изменяться во время выполнения операции ввода-вывода. Данные буферы должны оставаться рабочими до завершения операции ввода-вывода.

Одновременное выполнение операций чтения или записи через совместно используемую структуру aiocb приводит к непредсказуемым результатам.

Имеющаяся реализация Linux POSIX AIO предоставляется glibc в пользовательском пространстве. Она имеет ряд ограничений, наиболее существенные из которых — затраты на сопровождение нескольких нитей при операциях ввода-вывода и плохое масштабирование. Некогда для реализации асинхронного ввода-вывода велась работа над ядерной реализацией на основе машины состояний (смотрите io_submit(2), io_setup(2), io_cancel(2), io_destroy(2), io_getevents(2)), но эта реализация ещё недостаточно стабильна в тех местах, где POSIX AIO можно было бы полностью реализовать на системных вызовах ядра.

ПРИМЕР

Представленная далее программа открывает все файлы, указанные в параметрах командной строки и ставит в очередь запрос на полученные файловые дескрипторы с помощью aio_read(3). Затем программа входит в цикл, в котором периодически следит за всеми выполняемыми операциями ввода-вывода с помощью aio_error(3). Для каждого запроса ввода-вывода настроено получение уведомления посредством сигнала. После завершения всех запросов ввода-вывода, программа возвращает их состояние с помощью aio_return(3).

Сигнал SIGQUIT (генерируемый нажатием control-\) заставляет программу отменить все невыполненные запросы с помощью aio_cancel(3).

Вот результат работы программы. В этом примере программа ставит в очередь два запроса для стандартного ввода, и они отрабатываются двумя введёнными строками «abc» и «x».

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

Принципы asynchronous io.

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

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

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

AIO в Linux.

В Linux существует два типа aio - posix aio, который эмулируется в glibc созданием потоков (pthreads), и ядерный aio, который работает внутри ядра и не является портабельным на другие ОС (в том числе и на старые ядра Linux). Первый способ работает через сигналы и не слишком производителен из-за накладных расходов по переключению контекста и обработке этих самых сигналов. Зато второй способ хорошо укладывался в модель обработки событий через libevent.

Необходимо отметить, что есть целый ряд ограничений на работу ядерного aio. Во-первых, ядерный aio не имеет интерфейса в libc, поэтому надо либо использовать libaio, либо писать собственнные обертки вокруг системных вызовов, что усложняется тем, что номера системных вызовов отличаются от архитектуры к архитектуре. Обертки могут выглядеть таким образом:

Смещения должны быть выровнены по границе 512 байт Размер буфера чтения/записи должен быть кратен 512 байтам

Получение нотификаций.


Основная ценность данной структуры в поле data, позволяющем передать произвольный указатель при aio запросе, а вторая - поле res, означающее результат. Этот результат - это либо число байт, обработанных в ходе запроса, либо код ошибки (при отрицательном res).

Отправка запросов.

Отправка aio запроса - отдельная тема для разговора, так как опять же структура для этого не описана в libc. Поэтому я использовал фрагмент кода из libaio для описания этой структуры:


Использовать эту структуру для aio запроса следует так:


Таким образом, можно отправлять не единичные, а множественные запросы aio, которые потом обрабатывать.

Заключение.

AIO в Linux использовать можно и нужно тогда, когда необходимо обеспечить два требования: нотификация о завершении операции и неблокирующий ввод/вывод. Это полезно тогда, когда событийно ориентированной программе необходимо интенсивно выполнять IO с файлам, при этом не блокируясь на read и write, но при этом получая информации о завершении таких операций. Кроме этого, использование O_DIRECT, дает возможность получать нотификацию ровно тогда, когда данные уже либо записались на диск, либо находятся в буфере диска (если таковой есть). Это позволяет избежать использование буферов системы и обеспечить равномерное использование диска (то есть, исключается поведение, когда вначале запись проходит мгновенно, а потом, при переполнении системных буферов, внезапно начинает тормозить, так как эти буферы начинают интенсивно сбрасываться на диск). Полную версию API для использования aio в Linux я включил в rspamd:
aio_event.c
aio_event.h

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

Читайте также: