Thursday, May 3, 2012

AIO в Linux.

Введение.

Не так давно я реализовывал систему асинхронного 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, либо писать собственнные обертки вокруг системных вызовов, что усложняется тем, что номера системных вызовов отличаются от архитектуры к архитектуре. Обертки могут выглядеть таким образом:


Следующее ограничение - дескрипторы для ядерного aio должны быть открыты с флагом O_DIRECT, что накладывает ограничения на операции с дескриптором (без этого флага aio работать будет, но может быть блокирующим):
  1. Смещения должны быть выровнены по границе 512 байт
  2. Размер буфера чтения/записи должен быть кратен 512 байтам
  3. Буфер в памяти должен быть выровнен по границе 512 байт (например, при помощи posix_memalign)
При невыполнении этих условий aio операции будут выдавать EINVAL, что, конечно, было бы крайне информативно, если бы этот самый EINVAL не возвращался в куче других случаев. Поэтому эти три пункта должны выполняться неукоснительно.

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

Нотификации можно получать двумя способами: через сигналы (дорого) и через eventfd (правильно). Eventfd - это такой дескриптор, через который ядро уведомляет приложение о событиях, которые случились внутри ядра. Завершение AIO является характерным примером такого события. Сам по себе eventfd выплевывает 8-ми байтные целые числа. Такое число значит, сколько событий произошло с последнего вызова eventfd. Eventfd может обрабатываться обычными операциями поллинга, например, через libevent. Далее, получив число таких событий, можно звать io_getevents. Данный вызов заполняет массив структур io_cbdata, выглядящих следующим образом:


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

No comments:

Post a Comment