Зачем io_uring
Async I/O в Linux исторически - боль:
select/poll- O(n) на каждый вызов, syscall на каждый eventepoll- быстрее, но всё равно syscall на каждое add/wait/operation; сами read/write остаются blocking или non-blocking-with-EAGAINaio(POSIX async I/O) - ограничен (only direct I/O без буфера), "тёмные углы" в kernel, никогда не получил mainstream использование
В 2019 Jens Axboe запушил io_uring в Linux 5.1: shared-memory ring-buffer между userspace и kernel. App кладёт submission entry в ring, kernel обрабатывает, completion отдаёт в другой ring. Один syscall может subm'ить тысячи операций.
С 5.6 поддержка большинства syscall'ов (read, write, recv, send, accept, connect, openat, statx, fsync, splice, и др).
Производительность:
- 3-5x throughput vs epoll+blocking I/O на mixed workload
- 0 syscall'ов в hot path при SQPOLL
- Linear scale до миллионов IOPS на NVMe
Applications: высоконагруженные сервера (proxy, load balancer),
базы данных (PostgreSQL планирует, RocksDB), HFT, file copy
(liburing cp).
Архитектура - два ring'а
┌─────────────┐ SQE ┌─────────────┐
│ app │ ────► │ submission │ ──┐
│ │ │ queue (SQ) │ │
│ │ └──────────────┘ │
│ │ ┌──────────────┐ ▼
│ │ ◄──── │ completion │ kernel processes
│ │ CQE │ queue (CQ) │ async
│ │ └──────────────┘ │
└─────────────┘ │
▲ │
└───────────────────────────────────┘
enqueue CQE
- SQ (Submission Queue) - app пишет SQE (Submission Queue Entry) с описанием операции (op-code, fd, offset, buffer, length, ...)
- CQ (Completion Queue) - kernel пишет CQE (result, user_data correlation)
- Обе очереди - mmap'нутые (см. mmap) shared между app и kernel, нет копирования
Размер ring'а - степень двойки до 32K (configured при io_uring_setup).
Базовый flow
io_uring_setup(entries, ¶ms)→fdmmap(fd, ...)ringы в userspace- App кладёт SQE в SQ, увеличивает SQ tail
io_uring_enter(fd, num_to_submit, ...)(или ничего, если SQPOLL)- Kernel обрабатывает - результаты в CQ
- App читает CQE, увеличивает CQ head, освобождает буферы
Ручная работа сложна, обычно используют liburing:
#include <liburing.h>
struct io_uring ring;
io_uring_queue_init(QUEUE_DEPTH, &ring, 0);
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_read(sqe, fd, buf, sizeof(buf), 0);
io_uring_sqe_set_data(sqe, my_request_ptr);
io_uring_submit(&ring);
struct io_uring_cqe *cqe;
io_uring_wait_cqe(&ring, &cqe);
printf("read %d bytes\n", cqe->res);io_uring_cqe_seen(&ring, cqe);
SQPOLL - syscall-less submit
В SQPOLL mode kernel запускает kernel-thread, который пуллит SQ:
io_uring_queue_init_params(QD, &ring, &(struct io_uring_params){.flags = IORING_SETUP_SQPOLL,
.sq_thread_idle = 1000, // ms idle перед park
});
Теперь app может submit'ить вообще без io_uring_enter - просто
заполнять SQE и обновлять SQ tail. Kernel-thread увидит, начнёт
обрабатывать.
Минусы:
- Kernel-thread жрёт CPU когда busy-pollит
- Требует CAP_SYS_NICE (раньше CAP_SYS_ADMIN)
- На многопроцессорных машинах нужно pin thread к нужному CPU
Используется в HFT, ultra-low-latency сервисах.
Fixed buffers и fixed files
По умолчанию каждый submit делает get_user_pages на буфер - дорого.
Fixed buffers: pre-register пул буферов один раз:
io_uring_register_buffers(&ring, iovecs, num);
io_uring_prep_read_fixed(sqe, fd, buf_idx, len, offset, buf_index);
Аналогично fixed files: io_uring_register_files - пул FD,
меньше overhead на dup/close.
Дают +20-30% throughput на heavy workload.
Multi-shot и provided buffers
Linux 5.18+ - multi-shot accept: один SQE accept'ит много соединений (CQE per accept). Полезно для серверов:
io_uring_prep_multishot_accept(sqe, listen_fd, NULL, NULL, 0);
Provided buffers: app даёт kernel пул буферов, recv выбирает свободный → меньше fragmentation памяти.
Linux 5.19+ - registered ring fd (faster mmap-less ringы).
io_uring vs альтернативы
| Свойство | epoll + non-block | aio | io_uring |
|---|---|---|---|
| Syscall на event | да | да | нет (с SQPOLL) |
| Buffered I/O | да (sync) | нет, только O_DIRECT | да |
| sockets | да | плохо | да |
| files | sync | да (O_DIRECT) | да (любые) |
| Сложность API | средняя | высокая | средняя (с liburing) |
| Many ops в одном вызове | нет | нет | до thousands |
| Latency 99p | средняя | низкая | очень низкая |
Реальный win io_uring:
- Mixed I/O: socket + disk + open в одном цикле
- High concurrency: 100K+ in-flight ops
- NVMe: 3M+ IOPS реально достижимы
Real-world adopters: ScyllaDB, Cloudflare proxies, Netty (Java), tokio-uring (Rust), libevent4.
Security history - CVE'и
io_uring - сложная kernel-feature, было много CVE:
- CVE-2021-3782 - DoS через async cancellation
- CVE-2022-1786 - heap-out-of-bounds в request queueing, privesc до root
- CVE-2023-2598 - fixed buffers race, info-leak
- CVE-2024-0582 - UAF при retry'ах
Многие production-системы (Google, Docker default seccomp profile)
блокируют io_uring syscall'ы. Docker с июня 2023 включил
io_uring_* в default-deny seccomp - в контейнерах не работает по
умолчанию.
ChromeOS, Android, Snap - тоже block. Если ты не контролируешь workload (multi-tenant), оставь disabled.
Disable kernel-wide:
sysctl -w kernel.io_uring_disabled=2 # 5.13+
Через seccomp:
SCMP_ACT_ERRNO(EPERM) для io_uring_setup, io_uring_enter, io_uring_register
Когда что-то пошло не так
- EPERM на
io_uring_setup- sysctlkernel.io_uring_disabledили seccomp блокирует. Проверь Docker security-opts. -EAGAINв CQE - kernel не успел, retry submit. Поднять queue-depth, проверить SQ overflow.-ECANCELEDмассово - таймауты ловят неудачно. ПроверьIORING_OP_LINK_TIMEOUT.- High CPU в SQPOLL - thread не park'ится. Уменьши
sq_thread_idle, или explicitly callio_uring_enter(IORING_ENTER_SQ_WAKEUP). - Verifier-style errors на новых ядрах - feature support меняется.
io_uring_get_probe()показывает supported ops. - Memory leak - забыл
io_uring_cqe_seen(). CQE не освобождён - ring заполняется, всё стопится. - Performance хуже epoll - неправильный workload. На мало- concurrent или CPU-bound io_uring не ускоряет, добавляет complexity.
Где почитать
- https://kernel.dk/io_uring.pdf - оригинальная whitepaper Axboe
- https://unixism.net/loti/ - "Lord of the io_uring" tutorial
- https://github.com/axboe/liburing - reference impl + examples
- https://man7.org/linux/man-pages/man7/io_uring.7.html - manpage