io_uring, впервые представленный в Linux 5.1, существенно изменил подход к асинхронному вводу/выводу. В отличие от традиционных механизмов вроде epoll или AIO, io_uring использует разделяемые кольцевые буферы для обмена запросами и результатами между пользовательским пространством и ядром. Это позволяет уменьшить количество системных вызовов, минимизировать переключения контекста и добиться пропускной способности в асширения и покажем, как построить высокопроизводительный сетевой сервис на его основе.В этой статье мы глубоко рассмотрим архитектуру io_uring, объясним его ключевые расширения и покажем, как построить высокопроизводительный сетевой сервис на его основе.
1. Как работает io_uring
io_uring состоит из двух кольцевых буферов: очереди отправки (Submission Queue или SQ) и очереди завершения (Completion Queue или CQ). Приложение заполняет структуры Submission Queue Entry (SQE) и увеличивает индекс хвоста SQ, а ядро по своему усмотрению обрабатывает эти запросы и помещает результаты в Completion Queue Entry (CQE), обновляя хвост CQ. Наличие разделяемых mmap‑буферов позволяет приложению избегать дорогостоящих системных вызовов при каждом запросе I/O.
Основные системн
io_uring_setup()
— выделяет кольцевые буферы и инициализирует кольцо.io_uring_enter()
— сообщает ядру о новых SQE и, при необходимости, блокируется в ожидании CQE.io_uring_register()
— регистрирует файлы, буферы и BPF‑программы для ускорения работы.
Внутри ядра io_uring использует worker‑потоки и может работать как в режимах poll, так и без них. В версиях 6.0+ появились делегированные задачи (IORING_SETUP_COOP_TASKRUN
) и single‑issuer rings
, позволяющие снизить блокировки в многопоточном окружении.
2. Расширения и флаги кольца
Для тонкой настройки io_uring существует множество флагов и расширений. Наиболее интересные:
IORING_SETUP_SQPOLL
— ядро создаёт фоновый поток, который самостоятельно считывает SQE, не требуя системного вызоваio_uring_enter()
для каждого запроса. Это снижает накладные расходы, но требует закрепления кольца за конкретным ядром (IORING_SETUP_SQ_AFF
).IORING_SETUP_COOP_TASKRUN
— позволяет выполнять завершённые операции в контексте приложения, уменьшая контекстные переключения.IORING_REGISTER_BUFFERS
иIORING_REGISTER_FILES
— регистрация буферов и файлов снижает стоимость каждого I/O, поскольку ядру не нужно фиксировать страницы памяти и искать файловый дескриптор.Мультишот-операции (
IORING_OP_ACCEPT
,IORING_OP_RECV
сIOSQE_IO_LINK
) позволяют обрабатывать множество событий с одного SQE, что особенно полезно для сетевых серверов.Zero‑copy отправка (функция
send_zc()
иIORING_REGISTER_PBUF_RING
) позволяет ядру передавать указатели на пользовательские буферы сетевому адаптеру без копирования.
Правильное сочетание этих опций позволяет серёзно ускорить высоконагруженные сервисы.
3. Пример эхо‑сервера с liburing
Рассмотрим упрощённый эхо‑сервер, который обрабатывает новые подключения и сообщения б
#include
#include
#include
#define MAX_EVENTS 1024
int main() {
int srv_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr = { .sin_family = AF_INET, .sin_port = htons(12345), .sin_addr.s_addr = INADDR_ANY };
bind(srv_fd, (struct sockaddr*)&addr, sizeof(addr));
listen(srv_fd, SOMAXCONN);
struct io_uring ring;
io_uring_queue_init(MAX_EVENTS, &ring, 0);
// Подготовка accept с multishot
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_multishot_accept(sqe, srv_fd, NULL, NULL, 0);
sqe->user_data = srv_fd;
io_uring_submit(&ring);
while (1) {
struct io_uring_cqe *cqe;
int ret = io_uring_wait_cqe(&ring, &cqe);
if (ret < 0) break;
int fd = cqe->res;
if (cqe->user_data == srv_fd) {
// Новый клиент
struct io_uring_sqe *sq = io_uring_get_sqe(&ring);
io_uring_prep_recv_multishot(sq, fd, NULL, 0, 0);
sq->user_data = fd;
io_uring_submit(&ring);
} else {
// Пришли данные — читаем и отсылаем обратно
char buf[4096];
int n = recv(fd, buf, sizeof(buf), 0);
if (n <= 0) {
close(fd);
} else {
struct io_uring_sqe *sq2 = io_uring_get_sqe(&ring);
io_uring_prep_send(sq2, fd, buf, n, 0);
io_uring_submit(&ring);
}
}
io_uring_cqe_seen(&ring, cqe);
}
return 0;
}
Здесь мы используем io_uring_prep_multishot_accept()
и io_uring_prep_recv_multishot()
, чтобы повторно использовать один SQE для обработки множества соединений и сообщений. В реальном приложении следует регистрировать буферы (io_uring_register_buffers()
) и файлы, а также обрабатывать ошибки.
4. Оптимизация и best practices
Чтобы извлечь максимум из io_uring, учитывайте следующие рекомендации:
Регистрация ресурсов. Используйте
io_uring_register_buffers()
для буферов иio_uring_register_files()
для файлов, чтобы уменьшить накладные расходы. Для сетевых приложений эффективным может бытьio_uring_register_pbuf_ring()
.Привязка SQPOLL‑потока. Если вы включаете
IORING_SETUP_SQPOLL
, задайте CPU‑аффинити сIORING_SETUP_SQ_AFF
. Это уменьшит межпроцессорные синхронизации.Использование multishot. Вместо постановки нового accept/recv после каждого события используйте мультишот‑операции — это экономит SQE.
Zero‑copy отправка. Функция
send_zc()
вместе сSO_ZEROCOPY
позволяет передавать данные в сеть без копирования; с io_uring это особенно эффективно.Минимизация системных вызовов. Подавайте много SQE перед одним
io_uring_submit()
, а при чтении используйтеio_uring_peek_cqe()
для перебора всех готовых CQE за один вызов.Наблюдаемость. Интегрируйте трассировку с помощью
perf
,bcc/BPF
илиftrace
, чтобы следить за латентностью и пропусками запросов. Библиотека libbpf упрощает загрузку программ eBPF для мониторинга io_uring.
Следование этим практикам позволит вашему приложению масштабироваться на десятки ядер и обрабатывать миллионы запросов в секунду.
5. Отладка и мониторинг
Несмотря на простоту API, io_uring скрывает много сложностей. Для диагностики используйте:
strace
иperf trace
для контроля системных вызововio_uring_enter
иio_uring_setup
.bpftool
иbpftrace
— позволяют написать BPF‑скрипты и отслеживать задержки, например, точки входаio_uring_queue_async_work
.cat /proc/sys/fs/io-uring/*
— проверка системных параметров, таких как максимальные размеры колец.Флаги ядра
CONFIG_IO_URING_DEBUG
— включение дополнительных сообщений вdmesg
.Индикаторы переполнения. При чтении CQE проверяйте
cqe->flags & IORING_CQE_F_MORE
, что означает, что ещё есть события, иIORING_CQE_F_BUFFER
для определения использованного буфера.
Помните, что io_uring развивается: новые ядра приносят новые флаги и опкоды. Регулярно обновляйте ядро и liburing для доступа к свежим функциям.
Итоги
— это серьёзный шаг вперёд в развитии асинхронного ввода/вывода в Linux. Благодаря разделяемым кольцам, многообразию опций и поддержке zero‑copy он позволяет строить высокопроизводительные файловые и сетевые приложения без необходимости писать сложные kernel‑модули. Однако его эффективное использование требует глубокого понимания работы ядра, аккуратной регистрации ресурсов и внимательного отношения к профилированию.
Для профессиональных системных разработчиков изучение io_uring — это возможность получить значительный прирост производительности Linux I/O и подготовиться к будущим инновациям.
Kerts89
а как с нагрузкой на CPU