Введение.
Не так давно я реализовывал систему асинхронного 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, либо писать собственнные обертки вокруг системных вызовов, что усложняется тем, что номера системных вызовов отличаются от архитектуры к архитектуре. Обертки могут выглядеть таким образом:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#if defined(__i386__) | |
# define SYS_io_setup 245 | |
# define SYS_io_destroy 246 | |
# define SYS_io_getevents 247 | |
# define SYS_io_submit 248 | |
# define SYS_io_cancel 249 | |
#elif defined(__x86_64__) | |
# define SYS_io_setup 206 | |
# define SYS_io_destroy 207 | |
# define SYS_io_getevents 208 | |
# define SYS_io_submit 209 | |
# define SYS_io_cancel 210 | |
#endif | |
/* Linux specific io calls */ | |
static int | |
io_setup (guint nr_reqs, aio_context_t *ctx) | |
{ | |
return syscall (SYS_io_setup, nr_reqs, ctx); | |
} | |
static int | |
io_destroy (aio_context_t ctx) | |
{ | |
return syscall (SYS_io_destroy, ctx); | |
} | |
static int | |
io_getevents (aio_context_t ctx, long min_nr, long nr, struct io_event *events, struct timespec *tmo) | |
{ | |
return syscall (SYS_io_getevents, ctx, min_nr, nr, events, tmo); | |
} | |
static int | |
io_submit (aio_context_t ctx, long n, struct iocb **paiocb) | |
{ | |
return syscall (SYS_io_submit, ctx, n, paiocb); | |
} | |
static int | |
io_cancel (aio_context_t ctx, struct iocb *iocb, struct io_event *result) | |
{ | |
return syscall (SYS_io_cancel, ctx, iocb, result); | |
} |
Следующее ограничение - дескрипторы для ядерного aio должны быть открыты с флагом O_DIRECT, что накладывает ограничения на операции с дескриптором (без этого флага aio работать будет, но может быть блокирующим):
- Смещения должны быть выровнены по границе 512 байт
- Размер буфера чтения/записи должен быть кратен 512 байтам
- Буфер в памяти должен быть выровнен по границе 512 байт (например, при помощи posix_memalign)
Получение нотификаций.
Нотификации можно получать двумя способами: через сигналы (дорого) и через eventfd (правильно). Eventfd - это такой дескриптор, через который ядро уведомляет приложение о событиях, которые случились внутри ядра. Завершение AIO является характерным примером такого события. Сам по себе eventfd выплевывает 8-ми байтные целые числа. Такое число значит, сколько событий произошло с последнего вызова eventfd. Eventfd может обрабатываться обычными операциями поллинга, например, через libevent. Далее, получив число таких событий, можно звать io_getevents. Данный вызов заполняет массив структур io_cbdata, выглядящих следующим образом:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
struct io_event { | |
guint64 data; /* the data field from the iocb */ | |
guint64 obj; /* what iocb this event came from */ | |
gint64 res; /* result code for this event */ | |
gint64 res2; /* secondary result */ | |
}; |
Основная ценность данной структуры в поле data, позволяющем передать произвольный указатель при aio запросе, а вторая - поле res, означающее результат. Этот результат - это либо число байт, обработанных в ходе запроса, либо код ошибки (при отрицательном res).
Отправка запросов.
Отправка aio запроса - отдельная тема для разговора, так как опять же структура для этого не описана в libc. Поэтому я использовал фрагмент кода из libaio для описания этой структуры:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
typedef enum io_iocb_cmd { | |
IO_CMD_PREAD = 0, | |
IO_CMD_PWRITE = 1, | |
IO_CMD_FSYNC = 2, | |
IO_CMD_FDSYNC = 3, | |
IO_CMD_POLL = 5, | |
IO_CMD_NOOP = 6, | |
} io_iocb_cmd_t; | |
#if defined(__LITTLE_ENDIAN) | |
#define PADDED(x,y) x, y | |
#elif defined(__BIG_ENDIAN) | |
#define PADDED(x,y) y, x | |
#else | |
#error edit for your odd byteorder. | |
#endif | |
/* | |
* we always use a 64bit off_t when communicating | |
* with userland. its up to libraries to do the | |
* proper padding and aio_error abstraction | |
*/ | |
struct iocb { | |
/* these are internal to the kernel/libc. */ | |
guint64 aio_data; /* data to be returned in event's data */ | |
guint32 PADDED(aio_key, aio_reserved1); | |
/* the kernel sets aio_key to the req # */ | |
/* common fields */ | |
guint16 aio_lio_opcode; /* see IOCB_CMD_ above */ | |
gint16 aio_reqprio; | |
guint32 aio_fildes; | |
guint64 aio_buf; | |
guint64 aio_nbytes; | |
gint64 aio_offset; | |
/* extra parameters */ | |
guint64 aio_reserved2; /* TODO: use this for a (struct sigevent *) */ | |
/* flags for the "struct iocb" */ | |
guint32 aio_flags; | |
/* | |
* if the IOCB_FLAG_RESFD flag of "aio_flags" is set, this is an | |
* eventfd to signal AIO readiness to | |
*/ | |
guint32 aio_resfd; | |
}; |
Использовать эту структуру для aio запроса следует так:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
struct io_cbdata *cbdata; | |
struct iocb *iocb[1]; | |
iocb[0] = alloca (sizeof (struct iocb)); | |
memset (iocb[0], 0, sizeof (struct iocb)); | |
iocb[0]->aio_fildes = fd; | |
iocb[0]->aio_lio_opcode = IO_CMD_PREAD; | |
iocb[0]->aio_reqprio = 0; | |
iocb[0]->aio_buf = (uint64_t)((uintptr_t)buf); | |
iocb[0]->aio_nbytes = len; | |
iocb[0]->aio_offset = offset; | |
iocb[0]->aio_flags |= (1 << 0) /* IOCB_FLAG_RESFD */; | |
iocb[0]->aio_resfd = event_fd; | |
iocb[0]->aio_data = (uint64_t)((uintptr_t)cbdata); | |
/* Iocb is copied to kernel internally, so it is safe to put it on stack */ | |
if (io_submit (io_ctx, 1, iocb) == 1) { | |
return len; | |
} | |
else { | |
if (errno == EAGAIN || errno == ENOSYS) { | |
/* Fall back to sync read */ | |
goto blocking; | |
} | |
} |
Таким образом, можно отправлять не единичные, а множественные запросы aio, которые потом обрабатывать.
Заключение.
AIO в Linux использовать можно и нужно тогда, когда необходимо обеспечить два требования: нотификация о завершении операции и неблокирующий ввод/вывод. Это полезно тогда, когда событийно ориентированной программе необходимо интенсивно выполнять IO с файлам, при этом не блокируясь на read и write, но при этом получая информации о завершении таких операций. Кроме этого, использование O_DIRECT, дает возможность получать нотификацию ровно тогда, когда данные уже либо записались на диск, либо находятся в буфере диска (если таковой есть). Это позволяет избежать использование буферов системы и обеспечить равномерное использование диска (то есть, исключается поведение, когда вначале запись проходит мгновенно, а потом, при переполнении системных буферов, внезапно начинает тормозить, так как эти буферы начинают интенсивно сбрасываться на диск). Полную версию API для использования aio в Linux я включил в rspamd:aio_event.c
aio_event.h
Надеюсь, кому-то это поможет не наступить на те грабли, по которым я изрядно понаступал сам. Кроме того, можно ознакомиться с кодом libaio или же использовать ее интерфейсы (весьма спорные и мало облегчающие разработку, к сожалению).
No comments:
Post a Comment