3. liburing 库入门:安装、编译与核心 API 概览
liburing 是 io_uring 的官方用户态辅助库,旨在消除开发者直接使用 io_uring 系统调用和 mmap 内存映射的繁琐细节。它封装了提交队列(SQ)和完成队列(CQ)的管理,提供了更安全、更简洁的 C 语言接口。本章将带你完成 liburing 的安装与编译,并深入解析其最核心的四个 API。
3.1 liburing 的安装与编译
liburing 通常作为独立的开源项目发布,你可以从 git.kernel.dk 获取源码,或通过系统包管理器安装。
3.1.1 源码编译安装(推荐)
源码安装能确保你获得最新特性,并方便交叉编译。以下是标准步骤:
- 获取源码:
git clone https://github.com/axboe/liburing.git - 进入目录:
cd liburing - 配置与编译:
./configure && make - 安装:
sudo make install
/usr/local/include/,库文件位于 /usr/local/lib/。若链接时找不到库,请确保 LD_LIBRARY_PATH 包含该路径,或使用 -L/usr/local/lib 编译选项。
3.1.2 包管理器安装(快速体验)
| 发行版 | 安装命令 |
|---|---|
| Ubuntu / Debian | sudo apt-get install liburing-dev |
| Fedora / RHEL | sudo dnf install liburing-devel |
| Arch Linux | sudo pacman -S liburing |
3.1.3 编译你的第一个 liburing 程序
编写一个简单的测试文件 test_uring.c,然后使用以下命令编译:
gcc -o test_uring test_uring.c -luring -Wall -O2
关键点:
-luring:链接 liburing 动态库。-Wall -O2:开启警告和优化,这是生产级代码的标配。
io_uring_queue_init 会返回 ENOSYS(功能未实现)。
3.2 核心 API 概览
liburing 将 io_uring 的交互抽象为四个核心步骤:初始化队列、获取提交队列条目(SQE)、提交请求、等待完成队列条目(CQE)。下图展示了这一完整的数据流:
3.3 核心 API 详解
3.3.1 io_uring_queue_init:初始化 io_uring 实例
这是所有操作的起点。它负责分配提交队列(SQ)和完成队列(CQ)的内存,并建立与内核的共享映射。
#include <liburing.h>
int io_uring_queue_init(unsigned entries, struct io_uring *ring, unsigned flags);
| 参数 | 说明 |
|---|---|
entries |
队列深度,即 SQ 中最多可容纳的 SQE 数量。通常设为 64、128、256 或 1024。 |
ring |
指向 struct io_uring 的指针,函数内部会填充该结构体。 |
flags |
初始化标志,常用 0(默认)或 IORING_SETUP_IOPOLL(IO 轮询模式)。 |
返回值: 成功返回 0,失败返回负的错误码(如 -ENOMEM、-ENOSYS)。
entries 并非越大越好。过大的队列会占用更多内存,且可能增加延迟。对于大多数网络或存储场景,entries = 128 是一个平衡的起点。
3.3.2 io_uring_get_sqe:获取提交队列条目
在提交请求前,你需要从 SQ 中“取出”一个空闲的 SQE 来填充操作类型、文件描述符、缓冲区等参数。
struct io_uring_sqe *io_uring_get_sqe(struct io_uring *ring);
返回值: 成功返回指向 struct io_uring_sqe 的指针;若 SQ 已满,返回 NULL。
获取 SQE 后,通常使用辅助宏来填充操作:
struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
if (!sqe) {
// 处理队列满的情况
return -1;
}
// 准备一个读操作
io_uring_prep_read(sqe, fd, buf, len, offset);
// 或准备一个写操作
io_uring_prep_write(sqe, fd, buf, len, offset);
io_uring_get_sqe 只是“借用”了 SQ 中的槽位,并未真正提交给内核。你必须调用 io_uring_submit 来触发实际的系统调用。
3.3.3 io_uring_submit:提交请求到内核
此函数将之前通过 io_uring_get_sqe 获取并填充的所有 SQE 一次性提交给内核处理。
int io_uring_submit(struct io_uring *ring);
返回值: 成功返回提交的 SQE 数量;失败返回负的错误码。
提交后,内核会异步处理这些请求,并将完成状态放入 CQ。
3.3.4 io_uring_wait_cqe:等待完成队列条目
这是获取异步操作结果的阻塞接口。它会等待直到至少有一个 CQE 可用。
int io_uring_wait_cqe(struct io_uring *ring, struct io_uring_cqe **cqe_ptr);
| 参数 | 说明 |
|---|---|
ring |
io_uring 实例指针。 |
cqe_ptr |
二级指针,函数会将可用的 CQE 地址写入 *cqe_ptr。 |
处理完 CQE 后,必须调用 io_uring_cqe_seen(ring, cqe) 通知内核该条目已被消费,以便回收 CQ 槽位。
struct io_uring_cqe *cqe;
int ret = io_uring_wait_cqe(ring, &cqe);
if (ret < 0) {
// 处理错误
return ret;
}
// 检查 cqe->res 获取操作结果(如读取的字节数)
if (cqe->res < 0) {
// IO 操作失败,cqe->res 为负的错误码
} else {
// 成功处理数据
}
// 标记该 CQE 已消费
io_uring_cqe_seen(ring, cqe);
3.4 完整示例:一个简单的读文件程序
下面是一个完整的 liburing 程序,它打开一个文件,异步读取内容,然后打印:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <liburing.h>
#define QUEUE_DEPTH 1
#define BUF_SIZE 4096
int main(int argc, char *argv[]) {
struct io_uring ring;
struct io_uring_sqe *sqe;
struct io_uring_cqe *cqe;
char buf[BUF_SIZE] = {0};
int fd, ret;
if (argc < 2) {
fprintf(stderr, "Usage: %s <filename>\n", argv[0]);
return 1;
}
// 1. 初始化 io_uring
ret = io_uring_queue_init(QUEUE_DEPTH, &ring, 0);
if (ret) {
fprintf(stderr, "Unable to setup io_uring: %s\n", strerror(-ret));
return 1;
}
// 2. 打开文件
fd = open(argv[1], O_RDONLY);
if (fd < 0) {
perror("open");
return 1;
}
// 3. 获取 SQE 并填充读请求
sqe = io_uring_get_sqe(&ring);
if (!sqe) {
fprintf(stderr, "Could not get SQE.\n");
return 1;
}
io_uring_prep_read(sqe, fd, buf, BUF_SIZE, 0);
// 4. 提交请求
ret = io_uring_submit(&ring);
if (ret < 0) {
fprintf(stderr, "io_uring_submit failed: %s\n", strerror(-ret));
return 1;
}
// 5. 等待完成
ret = io_uring_wait_cqe(&ring, &cqe);
if (ret < 0) {
fprintf(stderr, "io_uring_wait_cqe failed: %s\n", strerror(-ret));
return 1;
}
// 6. 检查结果
if (cqe->res < 0) {
fprintf(stderr, "Async read failed: %s\n", strerror(-cqe->res));
} else {
printf("Read %d bytes:\n%s\n", cqe->res, buf);
}
// 7. 标记 CQE 已消费
io_uring_cqe_seen(&ring, cqe);
// 8. 清理
close(fd);
io_uring_queue_exit(&ring);
return 0;
}
3.5 核心 API 对比总结
| API | 角色 | 阻塞性 | 关键返回值 |
|---|---|---|---|
io_uring_queue_init |
初始化 | 否(系统调用) | 0 成功,负值错误码 |
io_uring_get_sqe |
获取槽位 | 否 | 非 NULL 成功,NULL 队列满 |
io_uring_submit |
提交请求 | 否(可能短时系统调用) | 提交数量,负值错误码 |
io_uring_wait_cqe |
等待结果 | 是(阻塞直到有 CQE) | 0 成功,负值错误码 |