- PVSM.RU - https://www.pvsm.ru -
io_uring, впервые представленный в Linux 5.1, существенно изменил подход к асинхронному вводу/выводу. В отличие от традиционных механизмов вроде epoll или AIO, io_uring использует разделяемые кольцевые буферы для обмена запросами и результатами между пользовательским пространством и ядром. Это позволяет уменьшить количество системных вызовов, минимизировать переключения контекста и добиться пропускной способности в асширения и покажем, как построить высокопроизводительный сетевой сервис на его основе.В этой статье мы глубоко рассмотрим архитектуру 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, позволяющие снизить блокировки в многопоточном окружении.
Для тонкой настройки 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) позволяет ядру передавать указатели на пользовательские буферы сетевому адаптеру без копирования.
Правильное сочетание этих опций позволяет серёзно ускорить высоконагруженные сервисы.
Рассмотрим упрощённый эхо‑сервер, который обрабатывает новые подключения и сообщения б
#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()) и файлы, а также обрабатывать ошибки.
Чтобы извлечь максимум из 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.
Следование этим практикам позволит вашему приложению масштабироваться на десятки ядер и обрабатывать миллионы запросов в секунду.
Несмотря на простоту 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 и подготовиться к будущим инновациям.
Автор: Pro_DGTL
Источник [1]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/linux/429641
Ссылки в тексте:
[1] Источник: https://habr.com/ru/articles/943128/?utm_source=habrahabr&utm_medium=rss&utm_campaign=943128
Нажмите здесь для печати.