lenec ru

← все посты

Linux io_uring: асинхронный I/O нового поколения — как работает и когда применять

14K

Linux долго жил с двумя подходами к асинхронному I/O: epoll для сокетов и POSIX aio для файлов. Оба имеют фундаментальные ограничения. io_uring, появившийся в ядре 5.1 (2019), решает их через единый интерфейс с shared memory между userspace и ядром — без системных вызовов на каждую операцию.

Проблемы epoll и aio

epoll: каждая операция read/write — отдельный syscall; не работает с обычными файлами; нет batching. POSIX aio: только O_DIRECT (без page cache); только файловый I/O; неудобный API. io_uring объединяет оба мира: файлы, сокеты, таймеры — всё через единый асинхронный интерфейс.

Архитектура: SQ, CQ и shared memory

io_uring построен на двух кольцевых буферах в shared memory:

┌───────────────────────────────────────────┐
│           Userspace (приложение)           │
│  ┌────────────────┐  ┌────────────────┐   │
│  │ Submission Queue│  │Completion Queue│   │
│  │  (SQ) — запись │  │  (CQ) — чтение │   │
│  └───────┬────────┘  └───────▲────────┘   │
│          │  shared memory     │            │
├──────────┼────────────────────┼────────────┤
│          ▼                    │            │
│  ┌────────────────┐  ┌───────┴────────┐   │
│  │ SQ — чтение    │  │ CQ — запись    │   │
│  └────────────────┘  └────────────────┘   │
│            Kernel (ядро Linux)             │
└───────────────────────────────────────────┘
  • Submission Queue (SQ) — приложение кладёт запросы, ядро забирает.
  • Completion Queue (CQ) — ядро кладёт результаты, приложение забирает.
  • SQPOLL mode — ядро крутит поток, опрашивающий SQ. Приложение вообще не делает syscall.

liburing: базовые операции

#include <liburing.h>

int main() {
    struct io_uring ring;
    io_uring_queue_init(256, &ring, 0);

    int fd = open("data.bin", O_RDONLY);
    char buf[4096];

    // Готовим запрос на чтение
    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, (void *)42);

    // Отправляем и ждём
    io_uring_submit(&ring);

    struct io_uring_cqe *cqe;
    io_uring_wait_cqe(&ring, &cqe);
    int bytes_read = cqe->res;
    io_uring_cqe_seen(&ring, cqe);

    io_uring_queue_exit(&ring);
    return 0;
}

Linked operations — цепочка accept + read за одну отправку:

sqe = io_uring_get_sqe(&ring);
io_uring_prep_accept(sqe, server_fd, NULL, NULL, 0);

sqe = io_uring_get_sqe(&ring);
io_uring_prep_read(sqe, client_fd, buf, BUF_SIZE, 0);
sqe->flags |= IOSQE_IO_LINK;

io_uring_submit(&ring); // одна отправка — две операции

Бенчмарки: io_uring vs epoll vs aio

Случайное чтение 4KB с NVMe SSD (fio, iodepth=128):

Метод        | IOPS      | Avg lat  | 99p lat  | CPU%
─────────────────────────────────────────────────────
io_uring     | 1,200K    | 105 μs   | 180 μs   | 45%
libaio       | 1,050K    | 120 μs   | 220 μs   | 52%
epoll+read   | 680K      | 185 μs   | 350 μs   | 78%
sync read    | 95K       | 1,340 μs | 2,100 μs | 30%

Для сети (echo-сервер, 10K соединений) разница скромнее: io_uring ~890K RPS vs epoll ~820K RPS. Основной выигрыш — файловый I/O и batching.

Кто использует и ограничения

Продакшен: Tokio (Rust) — tokio-uring для файлов; RocksDB — параллельное чтение SST; ScyllaDB — весь дисковый I/O; NGINX Unit — статические файлы.

Ограничения:

  • Ядро 5.1+, полная поддержка сети — с 5.19+.
  • Безопасность. Отключён в Docker по умолчанию (seccomp). Google отключил в ChromeOS/Android из-за уязвимостей.
  • Сложность отладки. Асинхронность + shared memory = трудно дебажить.

Вывод

io_uring — самое значительное изменение в Linux I/O за десятилетие. Для файлового I/O с высоким параллелизмом даёт 15-40% прироста IOPS при снижении CPU. Если сервис упирается в I/O на современном ядре — io_uring стоит рассмотреть.

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.