Асинхронный ввод/вывод (I/O) — один из краеугольных камней производительности современных серверов и систем. За последние двадцать лет Linux прошёл путь от select() и epoll() до совершенно новой модели — io_uring, способной сократить системные вызовы и достичь почти «zero-syscall» исполнения.
Эта статья объясняет:
как эволюционировали I/O-механизмы в Linux;
чем модель готовности отличается от модели завершения;
как устроен io_uring под капотом;
как на нём строить сетевые и дисковые подсистемы с минимальной латентностью;
какие подводные камни и ограничения есть в продакшене.
🧩 1. От select к io_uring: краткая эволюция
1.1 Классика: select, poll, epoll
Эти интерфейсы реализуют модель готовности (readiness model) — ядро сообщает, что дескриптор готов к чтению или записи, а само чтение/запись делает приложение.
epoll масштабируется по числу соединений, но каждое действие (accept, read, write) остаётся отдельным системным вызовом.
Для сетевых серверов это хорошо, но для дисков — не асинхронно: read() с SSD всё равно может блокировать.
1.2 POSIX AIO и libaio
POSIX-вызовы aio_read, aio_write — теоретически асинхронные, но в Linux glibc реализует их через пул потоков. Настоящий асинхронный доступ к блочным устройствам появился позже в виде Linux Native AIO (libaio), однако он ограничен: требует O_DIRECT, не работает с буферизованными файлами и не подходит для сетей.
1.3 Появление io_uring
В 2019 году (ядро 5.1) в Linux появился io_uring — новая модель ввода/вывода, основанная на очередях событий и модели завершения (completion model). Она решает ключевую проблему: минимизирует количество системных вызовов и избегает копирования данных между пространствами.
⚙️ 2. Как работает io_uring
io_uring строится вокруг двух кольцевых буферов, разделяемых между ядром и процессом:
Submission Queue (SQ) — куда приложение помещает запросы (
read,write,accept,recvmsgи др.);Completion Queue (CQ) — откуда оно получает результаты.
Механизм использует всего три системных вызова:
io_uring_setup()— инициализация кольца;io_uring_register()— регистрация буферов, файлов, таймеров и т.д.;io_uring_enter()— передача пакета запросов в ядро (может быть batched).
2.1 Архитектура без лишних системных вызовов
Очереди мапятся (mmap) в память пользователя, поэтому запись запроса и чтение результата не требуют read()/write() — только атомарные записи в разделяемые структуры.
Это снимает лишние переключения контекста и ускоряет работу в десятки раз при высоких нагрузках.
🧠 3. Ключевые возможности и режимы
3.1 SQPOLL
Включает «polling thread» в ядре: он постоянно проверяет SQ на новые задачи.
Если поток активен, приложение вообще не делает системных вызовов — ядро само снимает SQE.
Настраивается через IORING_SETUP_SQPOLL и параметр sq_thread_idle (миллисекунды простоя до «сна»).
Полезно для сетевых серверов и высокочастотных операций.
3.2 IOPOLL
Режим активного ожидания завершения операций на уровне блочного устройства (например, NVMe).
Максимальная производительность, но нагрузка на CPU.
Используется с O_DIRECT-доступом и NVMe-устройствами.
3.3 Cooperative / Deferred taskrun
Позволяют избежать избыточных IPI-сигналов и переключений контекста между ядром и юзер-пространством.
Флаги IORING_SETUP_COOP_TASKRUN и IORING_SETUP_DEFER_TASKRUN снижают «шум» на CPU в системах с множеством потоков.
🔩 4. Механизмы повышения производительности
4.1 Registered и fixed-ресурсы
Можно заранее «прикрепить»:
Registered buffers — ядро знает, где физически лежат буферы, и не тратит время на их пинning/копирование;
Registered files (direct descriptors) — минует общую файловую таблицу, сокращая операции
fgetи блокировки.
Это уменьшает задержки и ускоряет I/O на десятки процентов.
4.2 Multi-shot операции
Позволяют одной операцией обслуживать несколько событий.
Например, accept_multishot или recv_multishot обрабатывают множество подключений/пакетов без повторной постановки запроса.
В CQE появится флаг IORING_CQE_F_MORE, если событие не последнее.
4.3 Linked операции
Флаг IOSQE_IO_LINK позволяет объединить операции в цепочку: например, read → process → write → timeout.
Если одно звено неуспешно, можно оборвать всю последовательность (IOSQE_IO_HARDLINK).
Это превращает io_uring в мини-планировщик I/O-тасков внутри ядра.
🌐 5. Сетевой I/O: от epoll к io_uring
Раньше типичный сервер выглядел так:
Раньше типичный сервер выглядел так:
epoll_wait()
-> accept()
-> recv()
-> send()Каждая операция — syscall. В io_uring всё иначе:
io_uring_prep_multishot_accept()
io_uring_prep_recv_multishot()
io_uring_prep_send_zc()5.1 Преимущества:
Multi-shot accept/recv → меньше системных вызовов;
Provided buffers → ядро само выбирает буфер из заранее подготовленного пула;
Zero-copy send (send_zc) → данные передаются напрямую из пользовательского буфера в сетевой стек без копирования;
Poll как multi-shot → можно полностью заменить
epoll.
Результат — десятки тысяч соединений при низкой латентности и минимальной нагрузке на ядро.
💾 6. Асинхронный доступ к дискам
Для дисковых операций io_uring работает с обоими типа��и файлов:
Буферизованные (
RWF_NOWAIT+IOSQE_ASYNC) — если страница в кеше, операция немедленная;Прямой доступ (
O_DIRECT) — по-настоящему асинхронно.
6.1 Комбинации:
preadv2(..., RWF_NOWAIT)— не блокирует, если страница не в кеше;io_uring_prep_splice()— передача данных между fd внутри ядра (например,pipe → socket);io_uring_prep_fsync()— асинхронная синхронизация данных;uring_cmd— прямой passthrough к NVMe и драйверам устройств.
6.2 Режим IOPOLL
Если устройство поддерживает polling-I/O (NVMe), можно полностью избавиться от IRQ и системных вызовов.
Однако CPU-затраты возрастут — используйте только на «горячих» путях.
🔬 7. Тюнинг и эксплуатация
8.1 Размеры колец
SQ и CQ задаются при инициализации (
sq_entries,cq_entries).
Закладывайте запас под пиковые bursts: например, 4096–8192.
8.2 SQPOLL
Контролируйте
sq_thread_idle— не позволяйте polling-треду засыпать слишком быстро;Следите за флагом
SQ_NEED_WAKEUP: если активен — вызывайтеio_uring_enter()сIORING_ENTER_SQ_WAKEUP.
8.3 Registered буферы
Для частых операций (
recv/write) — используйтеio_uring_register_buffers().
Это сокращает overhead на сотни тысяч операций в секунду.
8.4 NUMA и CPU-Affinity
Один
ringна ядро — лучший компромисс между параллелизмом и кэш-локализацией.Для SQPOLL можно закрепить поток через
IORING_REGISTER_IOWQ_AFF.
📎 Заключение
io_uring — один из самых амбициозных и успешных шагов Linux в сторону высокопроизводительного I/O.
Он объединяет преимущества epoll, AIO и libaio, убирая их слабые стороны.
Для разработчиков серверов, баз данных и сетевых систем io_uring — это ключ к миллионам операций в секунду без боли потоков и блокировок.
Но важно помнить: это мощный инструмент, а не серебряная пуля. Проверяйте поддержку ядра, планируйте fallback и следите за безопасностью.
В правильных руках io_uring действительно превращает Linux-систему в реактивную машину.
