3. liburing 库入门:安装、编译与核心 API 概览

liburing 是 io_uring 的官方用户态辅助库,旨在消除开发者直接使用 io_uring 系统调用和 mmap 内存映射的繁琐细节。它封装了提交队列(SQ)和完成队列(CQ)的管理,提供了更安全、更简洁的 C 语言接口。本章将带你完成 liburing 的安装与编译,并深入解析其最核心的四个 API。

3.1 liburing 的安装与编译

liburing 通常作为独立的开源项目发布,你可以从 git.kernel.dk 获取源码,或通过系统包管理器安装。

3.1.1 源码编译安装(推荐)

源码安装能确保你获得最新特性,并方便交叉编译。以下是标准步骤:

  1. 获取源码:git clone https://github.com/axboe/liburing.git
  2. 进入目录:cd liburing
  3. 配置与编译:./configure && make
  4. 安装: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:开启警告和优化,这是生产级代码的标配。
注意: 运行编译后的程序需要 Linux 内核版本 >= 5.1。若内核版本过低,io_uring_queue_init 会返回 ENOSYS(功能未实现)。

3.2 核心 API 概览

liburing 将 io_uring 的交互抽象为四个核心步骤:初始化队列、获取提交队列条目(SQE)、提交请求、等待完成队列条目(CQE)。下图展示了这一完整的数据流:

liburing 核心 API 数据流 liburing 核心 API 数据流 用户程序 io_uring_queue_init io_uring_get_sqe io_uring_submit io_uring_wait_cqe 内核 IO 处理 流程:初始化 → 获取 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;
}
关键点回顾: 该示例严格遵循了“初始化 → 获取 SQE → 填充 → 提交 → 等待 CQE → 处理 → 标记消费”的闭环。这是所有 liburing 程序的基础骨架。

3.5 核心 API 对比总结

API 角色 阻塞性 关键返回值
io_uring_queue_init 初始化 否(系统调用) 0 成功,负值错误码
io_uring_get_sqe 获取槽位 否 非 NULL 成功,NULL 队列满
io_uring_submit 提交请求 否(可能短时系统调用) 提交数量,负值错误码
io_uring_wait_cqe 等待结果 是(阻塞直到有 CQE) 0 成功,负值错误码