io_uring là giao diện I/O bất đồng bộ của Linux dựa trên hai hàng đợi dùng chung giữa userspace và kernel: Submission Queue (SQ) nhận yêu cầu và Completion Queue (CQ) trả kết quả. Lợi ích không đến từ việc thay mọi lời gọi read() bằng một API “nhanh hơn”, mà từ batching, giảm chuyển ngữ cảnh và quản lý nhiều thao tác đang bay với ownership rõ ràng.
Bài viết xây mental model production bằng io_uring_setup(2), io_uring_enter(2) và thư viện helper liburing. API phát triển theo kernel; hãy probe tính năng lúc chạy và kiểm thử trên đúng kernel, filesystem và workload sẽ triển khai.
1. SQE, CQE và vòng đời một thao tác
| Thành phần | Vai trò | Điểm dễ sai |
|---|---|---|
| SQE | Mô tả opcode, file descriptor, buffer, offset và flags | Lấy SQE chưa có nghĩa request đã được submit |
| SQ | Hàng đợi yêu cầu được userspace chuẩn bị cho kernel | Có kích thước hữu hạn; io_uring_get_sqe() có thể trả NULL |
| CQE | Trả kết quả trong res, flags và user_data | Phải xử lý rồi gọi io_uring_cqe_seen() |
| CQ | Hàng đợi completion do kernel tạo | Không drain đủ nhanh có thể gây overflow và backpressure |
Một request đi qua các trạng thái: chuẩn bị SQE → submit → kernel thực thi hoặc chuyển cho worker → tạo CQE → ứng dụng đọc kết quả → giải phóng context/buffer → đánh dấu CQE đã dùng. Chỉ tái sử dụng memory sau completion cuối cùng liên quan đến memory đó.
2. Ví dụ đọc file tối thiểu bằng liburing
# Debian/Ubuntu
sudo apt install liburing-dev
cc -O2 -Wall -Wextra demo.c -luring -o demo
#include <errno.h>
#include <fcntl.h>
#include <liburing.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main(void) {
struct io_uring ring;
struct io_uring_cqe *cqe;
char buffer[4096];
int fd = -1, rc = 1;
int ret = io_uring_queue_init(32, &ring, 0);
if (ret < 0) {
fprintf(stderr, "queue_init: %s\n", strerror(-ret));
return 1;
}
fd = open("input.txt", O_RDONLY | O_CLOEXEC);
if (fd < 0) { perror("open"); goto out_ring; }
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
if (!sqe) { fprintf(stderr, "SQ is full\n"); goto out_fd; }
io_uring_prep_read(sqe, fd, buffer, sizeof buffer, 0);
io_uring_sqe_set_data64(sqe, 1);
ret = io_uring_submit(&ring);
if (ret < 0) {
fprintf(stderr, "submit: %s\n", strerror(-ret));
goto out_fd;
}
ret = io_uring_wait_cqe(&ring, &cqe);
if (ret < 0) {
fprintf(stderr, "wait: %s\n", strerror(-ret));
goto out_fd;
}
if (cqe->res < 0) {
fprintf(stderr, "read: %s\n", strerror(-cqe->res));
} else {
printf("read %d bytes, request=%llu\n",
cqe->res,
(unsigned long long) io_uring_cqe_get_data64(cqe));
rc = 0;
}
io_uring_cqe_seen(&ring, cqe);
out_fd:
close(fd);
out_ring:
io_uring_queue_exit(&ring);
return rc;
}
liburing trả lỗi của nhiều helper dưới dạng số âm -errno, không nhất thiết đặt errno. Kết quả thao tác nằm trong cqe->res: giá trị âm là lỗi, 0 có thể là EOF, số dương là số byte thực tế. Đừng giả định read/write luôn xử lý đủ chiều dài yêu cầu.
3. user_data là khóa tương quan, không phải memory tự quản lý
user_data được sao chép từ SQE sang CQE để ứng dụng tìm context tương ứng. Có thể dùng ID 64 bit hoặc pointer, nhưng pointer không làm object sống lâu hơn. Một pattern an toàn là cấp phát context chứa buffer, trạng thái và request ID; chỉ giải phóng sau khi nhận đủ mọi completion của chuỗi thao tác.
struct request {
uint64_t id;
int fd;
char *buffer;
size_t capacity;
size_t completed;
};
io_uring_sqe_set_data(sqe, request);
/* request và request->buffer phải còn sống đến CQE cuối */
Nếu một buffer được submit cho read rồi bị trả về pool trước CQE, kernel có thể ghi vào vùng nhớ đã được giao cho request khác. Đây là lỗi use-after-free về mặt logic dù process chưa crash.
4. Queue depth là giới hạn tài nguyên, không phải mục tiêu phải lấp đầy
Queue depth 256 không có nghĩa ứng dụng nên luôn giữ 256 I/O đang bay. Con số phù hợp phụ thuộc thiết bị, độ trễ, kích thước request, memory budget và SLO. Tăng depth có thể nâng throughput tới điểm bão hòa nhưng cũng tăng queueing latency và memory đang bị giữ.
Thiết kế backpressure theo số request/byte đang bay:
if (inflight_requests >= max_inflight_requests ||
inflight_bytes + next_size > max_inflight_bytes) {
stop_accepting_or_pause_upstream();
}
drain_completions();
resume_when_below_low_watermark();
Khi io_uring_get_sqe() trả NULL, hãy submit batch đang chuẩn bị, drain completion hoặc quay lại event loop. Không spin vô hạn và không bỏ request im lặng. Theo dõi riêng SQ full, CQ overflow, inflight và thời gian nằm trong hàng đợi.
5. submit thành công chưa phải operation thành công
io_uring_submit() cho biết số SQE đã được submit hoặc lỗi submit. Nó không xác nhận các I/O đã thành công. Mỗi SQE cuối cùng cần được đối chiếu với CQE, kể cả khi request bị cancel hoặc timeout.
- Xử lý short read/short write bằng state machine và offset còn lại.
- Xử lý EOF khác lỗi.
- Không dùng
strerror(cqe->res); lỗi đang âm nên dùngstrerror(-cqe->res). - Kiểm tra flags của CQE cho multishot hoặc buffer selection.
- Không giả định completion đến theo thứ tự submit.
6. Timeout, linked operation và cancellation đều có race
IORING_OP_ASYNC_CANCEL yêu cầu hủy một operation, nhưng operation đích có thể đã hoàn tất, đang hoàn tất hoặc không tìm thấy. Completion của lệnh cancel và completion của operation gốc là hai sự kiện riêng; ứng dụng phải chấp nhận cả hai thứ tự và không giải phóng context hai lần.
Linked timeout có thể buộc một chuỗi fail/cancel theo semantics của flags liên kết, nhưng timeout không thu hồi buffer thay cho ứng dụng. Một state machine nên có trạng thái pending, completed, cancel-requested và retired; cleanup chỉ diễn ra một lần sau khi tất cả CQE dự kiến đã được accounting.
7. Registered files và buffers: tối ưu có chi phí
io_uring_register(2) cho phép kernel giữ tham chiếu dài hạn tới file hoặc mapping memory, giảm overhead theo I/O. Đổi lại, memory có thể bị pin, bảng đăng ký cần cập nhật và lifecycle phức tạp hơn.
- Bắt đầu với file descriptor và buffer thông thường để đúng trước.
- Profile để chứng minh lookup/mapping là bottleneck rồi mới register.
- Đặt memory budget và tính cả thời gian buffer nằm trong kernel.
- Không unregister hoặc unmap khi operation còn tham chiếu.
- Với direct I/O, vẫn tuân thủ alignment của filesystem/device.
Fixed buffer không tự biến I/O buffered thành direct I/O, và O_DIRECT không tự bảo đảm nhanh hơn. Hai quyết định giải quyết các lớp chi phí khác nhau.
8. Không phải operation nào cũng thực sự bất đồng bộ theo cùng cách
Kernel có thể hoàn tất ngay, chạy trực tiếp trong ngữ cảnh submit hoặc chuyển công việc có khả năng block sang io-wq worker. Vì vậy “dùng io_uring” không đồng nghĩa “không có thread” hay “không thể block”. Filesystem, loại file, opcode, cache state và flags đều ảnh hưởng đường thực thi.
Đừng bật IOSQE_ASYNC, SQPOLL hoặc IOPOLL theo thói quen. SQPOLL tiêu thụ một kernel thread để poll submission và có trade-off CPU rõ ràng; IOPOLL dành cho I/O đặc thù và có ràng buộc. Benchmark trên workload thật, đo CPU trên mỗi operation lẫn tail latency.
9. Probe capability thay vì suy từ version string
Kernel, bản backport của distro, container policy và seccomp có thể tạo support matrix khác nhau. Dùng probe của liburing để kiểm tra opcode, đọc feature bits từ lúc khởi tạo và xử lý -EINVAL/-EOPNOTSUPP có chủ đích.
struct io_uring_probe *probe = io_uring_get_probe_ring(&ring);
if (!probe ||
!io_uring_opcode_supported(probe, IORING_OP_ASYNC_CANCEL)) {
enable_fallback_path();
}
io_uring_free_probe(probe);
Fallback có thể là epoll cộng nonblocking I/O hoặc thread pool có giới hạn. Nếu io_uring là điều kiện bắt buộc, fail fast khi startup với thông báo cụ thể thay vì lỗi ngẫu nhiên giữa traffic.
10. Fork, shutdown và ownership của ring
Ring, mapped queue và các request đang bay cần một owner rõ ràng. Không fork sau khi khởi tạo rồi để cả parent lẫn child tùy ý thao tác trên ring. liburing có helper để bảo vệ mapping khỏi fork trong các thiết kế phù hợp, nhưng cách đơn giản thường là tạo ring sau fork trong đúng process sở hữu.
Quy trình shutdown an toàn:
- Dừng nhận công việc mới và truyền backpressure lên upstream.
- Đặt deadline cho drain.
- Yêu cầu cancel cho phần việc còn lại nếu semantics cho phép.
- Drain/account mọi completion cần thiết và giải phóng context đúng một lần.
- Đóng resource ứng dụng, sau đó gọi
io_uring_queue_exit().
11. Security và giới hạn môi trường
io_uring mở một bề mặt kernel lớn và đã từng được runtime/container hạn chế theo policy. Production cần kernel còn được nhà phân phối vá, seccomp/container profile đã kiểm thử và nguyên tắc least privilege. Không tắt cơ chế bảo vệ toàn máy chỉ để một benchmark chạy.
Nếu workload xử lý tenant không tin cậy, giới hạn số ring, entries, registered memory, file descriptor và tổng inflight. Dữ liệu từ network vẫn phải qua validation; async I/O không thay đổi boundary tin cậy.
12. Quan sát đúng thứ cần tối ưu
| Metric | Câu hỏi trả lời |
|---|---|
| In-flight request/bytes | Ứng dụng đang giữ bao nhiêu công việc và memory? |
| Submit batch size | Có thật sự tận dụng batching hay submit từng request? |
| CQE drain latency | Completion có nằm chờ trong userspace? |
| Operation latency p50/p95/p99 | Tail latency tốt hơn hay xấu đi khi tăng depth? |
| Error theo opcode và errno | Lỗi từ support, cancellation, device hay ứng dụng? |
| SQ full/CQ overflow | Ring sizing hoặc event loop có mất cân bằng? |
| CPU/operation và worker activity | Giảm syscall có thực sự giảm chi phí tổng? |
So sánh với baseline synchronous, pread/pwrite, epoll hoặc thread pool bằng cùng dữ liệu và durability semantics. Benchmark cache nóng không đại diện cho storage thật; throughput cao nhưng p99 và memory tăng có thể là regression.
Checklist production
- Probe opcode/feature và có fallback hoặc fail-fast rõ ràng.
- Mỗi request có ID/context, ownership buffer và cleanup path duy nhất.
- Xử lý số âm, EOF, partial I/O và completion ngoài thứ tự.
- Queue depth, inflight bytes và upstream backpressure đều có giới hạn.
- Cancellation/timeout được test ở mọi race order.
- Registered resource chỉ dùng sau khi profile và có memory budget.
- Không bật SQPOLL/IOPOLL nếu chưa đo CPU và latency trên workload thật.
- Shutdown drain/cancel trước khi đóng ring và resource liên quan.
- Kernel, seccomp và container policy được kiểm thử như production.
- Fault test gồm disk full, short I/O, slow device, cancellation storm và CQ drain chậm.
Kết luận
io_uring là nền tảng mạnh để xây pipeline I/O có concurrency cao, nhưng hiệu năng đến từ state machine và quản lý tài nguyên đúng hơn là từ tên API. Bắt đầu với liburing và đường code đơn giản, giữ buffer sống đến completion, giới hạn inflight, drain CQ đều đặn và coi cancel là một giao thức có race. Sau khi correctness, fallback và observability đã vững, registration, batching và các polling mode mới là tối ưu có thể đánh giá bằng số liệu.




Chưa có bình luận. Hãy là người đầu tiên chia sẻ ý kiến.