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 и подготовиться к будущим инновациям.

Комментарии (3)


  1. Kerts89
    02.09.2025 12:14

    а как с нагрузкой на CPU


  1. Sazonov
    02.09.2025 12:14

    Делайте, пожалуйста, минимальную вычитку того что публикуете. А то выглядит как кривой copy-paste (в единственном примере кода) из чатгпт.


  1. kpn
    02.09.2025 12:14

    Пример кода скопировался криво (& > <).