
io_uring là giao diện lập trình ứng dụng (API) I/O bất đồng bộ dành riêng cho nhân Linux, cho phép tiến trình gửi hàng loạt yêu cầu đọc ghi mà không phải chờ từng lần gọi hệ thống. Thay vì mỗi thao tác I/O là một read() hay write() riêng lẻ, io_uring dùng hai vòng bộ nhớ chia sẻ giữa không gian người dùng và không gian nhân, giúp giảm đáng kể chi phí syscall — một trong những nút thắt cổ chai cũng của các API I/O khác trong hệ điều hành khác.
Đây là cơ chế mà các hệ thống hiệu năng cao như database, message broker, web server hay các ứng dụng lưu trữ như RocksDB, Ceph đang dùng để xử lý hàng triệu thao tác I/O mỗi giây.
Vì sao syscall lại là nút thắt cổ chai
Trên Linux, mỗi lần gọi read() hay write() đều phải chuyển từ không gian người dùng sang không gian nhân. Chi phí của lần chuyển mode này không nhỏ: mỗi syscall trung bình tốn khoảng vài trăm nanosecond chỉ để vào ra, chưa kể sao chép bộ đệm giữa hai không gian.
Khi ứng dụng cần xử lý hàng nghìn thao tác nhỏ mỗi giây — ví dụ một cơ sở dữ liệu đọc hàng trăm trang dữ liệu từ đĩa — tổng thời gian chỉ đi vào syscall có thể chiếm tới một nửa thời gian CPU. Cơ chế aio_read() và aio_write() của POSIX ra đời để giải quyết vấn đề này, nhưng nó dùng cơ chế chia sẻ biến điều kiện và thread, phát sinh chi phí thay vì loại bỏ chi phí.

Kiến trúc của io_uring: hai vòng bộ nhớ chia sẻ
Điểm đặc thù của io_uring nằm ở chỗ nó không giao tiếp với nhân chỉ qua syscall. Thay vào đó, người dùng và nhân chia sẻ trực tiếp các vòng bộ nhớ, và syscall chỉ còn chức năng báo hiệu rằng có việc mới.
Cấu trúc gồm hai vòng chính:
- Submission Queue (SQ) — vòng gửi yêu cầu, nằm ở phía người dùng. Mỗi yêu cầu I/O là một entry gọi là SQE (Submission Queue Entry), mô tả đúng thao tác mà bạn định gọi hệ thống.
- Completion Queue (CQ) — vòng nhận kết quả, nằm ở phía nhân. Mỗi yêu cầu đã xử lý xong sẽ sinh ra một CQE (Completion Queue Entry) chứa kết quả trả về.
Cả hai vòng đều được ánh xạ vào không gian người dùng bằng mmap(). Người dùng ghi SQE vào đuôi vòng SQ, nhân đọc từ đầu vòng và xử lý. Sau đó nhân ghi CQE vào đuôi vòng CQ, người dùng đọc từ đầu vòng. Đây là mô hình producer-consumer vòng tròn, không cần sao chép bộ đệm qua lại giữa hai không gian.
| Khái niệm | Vai trò | Phía nào ghi |
|---|---|---|
| SQE | Mô tả một thao tác I/O cần thực hiện | Người dùng |
| CQE | Kết quả của một SQE đã xử lý | Nhân |
| opcode | Loại thao tác, ví dụ IORING_OP_READV |
Người dùng |
| user_data | Mã định danh do người dùng tự gán, nhân trả lại nguyên vẹn | Người dùng |
| res | Kết quả trả về, giá trị âm là mã lỗi dạng -errno |
Nhân |
Một chi tiết quan trọng: io_uring không dùng biến errno để báo lỗi như API I/O cổ điển. Trường res trong CQE chứa giá trị mà lệnh hệ thống tương ứng sẽ trả về nếu thành công, hoặc mã lỗi âm nếu thất bại — ví dụ res = 1024 nghĩa là đọc được 1024 byte, còn res = -EINVAL nghĩa là tham số không hợp lệ.

Quy trình sử dụng cơ bản
Một chu trình sử dụng io_uring điển hình gồm bốn bước:
- Khởi tạo — gọi
io_uring_setup()để tạo vòng, rồi dùngmmap()ánh xạ SQ và CQ vào không gian người dùng. Với nhân từ 5.4 trở lại chỉ cần hai lầnmmap(), các bản cũ hơn cần ba lần. - Ghi yêu cầu — điền thông tin vào một SQE. Có thể ghi nhiều SQE liên tiếp rồi mới báo cho nhân một lần.
- Báo cho nhân — gọi
io_uring_enter()để nhân bắt đầu xử lý. Lệnh này còn có thể yêu cầu nhân chờ tới khi đủ số lượng kết quả rồi mới quay lại. - Đọc kết quả — lấy CQE từ CQ, kiểm tra trường
resvàuser_datađể biết kết quả thuộc yêu cầu nào.
Ví dụ cấu trúc SQE rút gọn, cho thấy các trường thường dùng nhất:
struct io_uring_sqe {
__u8 opcode; /* loại thao tác, ví dụ IORING_OP_READV */
__s32 fd; /* file descriptor */
__u64 off; /* vị trí trong tệp */
__u64 addr; /* con trỏ bộ đệm hoặc mảng iovec */
__u32 len; /* kích thước bộ đệm */
__u64 user_data; /* mã định danh trả lại khi hoàn tất */
__u8 flags; /* các cờ IOSQE */
};
Lấy SQE tiếp theo chỉ cần đọc con trỏ đuôi, che bằng mặt nạ kích thước vòng rồi tăng con trỏ lên một:
struct io_uring_sqe *sqe;
unsigned tail, index;
tail = *sqring->tail;
index = tail & (*sqring->ring_mask);
sqe = &sqring->sqes[index];
/* điền thông tin thao tác I/O vào đây */
describe_io(sqe);
sqring->array[index] = index;
tail++;
atomic_store_explicit(sqring->tail, tail, memory_order_release);
Thao tác cập nhật tail dùng memory_order_release là bắt buộc. Vì vòng bộ nhớ nằm ở cả hai không gian, trình biên dịch và CPU có thể tự ý sắp xếp lại thứ tự đọc ghi. Rào bộ nhớ bảo đảm dữ liệu trong SQE luôn nhìn thấy được từ phía nhân trước khi nhân xử lý.
Submission Queue Polling: bỏ hẳn syscall
Tính năng đáng chú ý nhất của io_uring là chế độ SQ Polling. Thay vì gọi io_uring_enter() mỗi lần có yêu cầu mới, io_uring khởi động một luồng nhân chuyên poll vòng SQ. Người dùng chỉ cần ghi SQE vào vòng, luồng nhân sẽ tự nhận ra.
Chi phí syscall bị loại bỏ hoàn toàn khỏi đường chạy nóng (hot path). Đổi lại, CPU của máy chủ sẽ dàng một lõi cho luồng poll liên tục — nên kỹ thuật này chỉ đáng dùng khi tần suất I/O đủ cao để bù lại chi phí đó.
So sánh với AIO và epoll
| Tiêu chí | io_uring | POSIX AIO | epoll |
|---|---|---|---|
| Mô hình | Vòng bộ nhớ chia sẻ | Thread + biến điều kiện | Một syscall cho nhiều fd |
| Sao chép bộ đệm | Thường không cần | Có | Không áp dụng |
| Băng thông cho tác vụ nhỏ | Rất cao | Thấp | Trung bình |
| Độ phức tạp lập trình | Trung bình | Thấp | Thấp |
| Phạm vi I/O | Tệp, socket, pipe, timer, poll | Chủ yếu tệp | Chỉ mô tả sẵn sàng |
Điểm mấu chốt: epoll chỉ cho bạn biết “socket này sẵn sàng đọc”, còn io_uring trực tiếp thực hiện thao tác đọc đó. Với ứng dụng nặng I/O, việc gộp cả hai lại vào một giao diện giúp loại bỏ phần lớn logic quản lý trạng thái thủ công.
Lưu ý quan trọng khi lập trình
- Không có thứ tự hoàn tất. Yêu cầu có thể xong theo bất kỳ thứ tự nào. Với các thao tác cần đúng thứ tự như gửi trên socket TCP, nên dùng cờ
IOSQE_IO_LINKđể buộc nhân thực thi liên tiếp. - Vòng SQ có thể đầy. Luôn kiểm tra số entry còn trống trước khi ghi thêm.
- Tuổi thọ con trỏ. Khi bật SQ Polling, con trỏ bộ đệm phải sống tới khi thao tác hoàn tất, không được giải phóng sớm. Cờ
IORING_FEAT_SUBMIT_STABLEcho biết hành vi này đã thay đổi ở nhân mới hay chưa.
io_uring hiện đã có trong nhân Linux từ phiên bản 5.1 và ngày càng được dùng rộng rãi. Nếu bạn đang xây dựng công cụ xử lý dữ liệu lớn, database nhúng hay proxy hiệu năng cao, đây là công cụ đáng đầu tư tìm hiểu.
Tài liệu tham khảo: trang hướng dẫn chính thức io_uring(7) trong Linux manual pages, tài liệu API của dự án io_uring_queue_init_params(3) và bài tổng quan The Quest for AIO của Jens Axboe.
