异步I/O允许用户空间程序在提交一个I/O请求后立即返回,无需等待操作完成。当操作在后台完成后,内核会通知用户程序。在Linux系统中,主要存在两种AIO机制:传统的Linux AIO和革命性的高性能框架io_uring。
1. 传统 Linux AIO (Legacy AIO)
传统的Linux AIO(由libaio库支持)是早期为满足数据库等场景的高性能I/O需求而设计的。但其自身存在一些设计和性能上的限制,导致其在普通文件I/O及某些设备驱动中的应用并不广泛。
- 用户空间操作:通常使用
io_submit()提交请求,再通过io_getevents()来检查完成事件。
- 驱动程序实现:驱动程序需要实现
file_operations中的aio_read和aio_write等方法,并处理I/O请求块struct kiocb。
2. io_uring (现代AIO)
io_uring是Linux内核引入的一种革命性的高性能异步I/O框架,旨在彻底解决传统AIO的限制,提供真正的非阻塞异步操作。其核心在于通过内核和用户空间共享的两个环形队列(提交队列SQ和完成队列CQ)来实现极低的开销。
- 用户空间操作:
- 应用程序将请求描述放入提交队列(SQ)。
- 调用
io_uring_enter()系统调用(或类似的库函数封装)来通知内核处理。
- 应用程序从完成队列(CQ)中消费已完成的事件。
- 驱动程序实现:驱动需要集成到io_uring框架中,这通常涉及更底层的块设备层或文件系统集成。对于普通的字符设备驱动,集成io_uring相对复杂,但能带来巨大的性能提升。
在学习路径上,通常建议先掌握阻塞I/O和poll/select/epoll多路复用机制,再学习异步通知(信号驱动I/O)。传统Linux AIO在现今的驱动开发中已较少使用,而io_uring无疑代表了Linux高性能I/O的未来方向。
io_uring的核心思想是利用用户空间和内核空间共享的两个环形缓冲区(Ring Buffers)来管理I/O请求和完成通知,从而最大限度地避免每次操作都进行昂贵的系统调用。
io_uring接口围绕两个通过共享内存映射到用户空间的关键环形队列运行:
| 组件 |
英文全称 |
简称 |
作用 |
| 提交队列 |
Submission Queue |
SQ |
用户空间向内核提交I/O请求的地方。 |
| 完成队列 |
Completion Queue |
CQ |
内核通知用户空间I/O操作已完成的地方。 |
io_uring的原生系统调用接口较为复杂,在实际开发中,我们通常使用liburing库来简化操作。下面是一个使用liburing异步读取文件的简明示例,展示了SQE提交流程、user_data传递请求上下文以及CQE消费的完整流程。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <liburing.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#define QD 64 // 队列深度 (Queue Depth)
#define BLOCK_SIZE 1024
// 结构体用于存储每个 I/O 请求的上下文信息
struct read_data {
off_t offset;
size_t length;
char *buffer;
};
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: %s <filename>\n", argv[0]);
return 1;
}
const char *filename = argv[1];
struct io_uring ring;
int ret;
int fd;
struct stat st;
// 1. 初始化 io_uring 实例
ret = io_uring_queue_init(QD, &ring, 0);
if (ret < 0) {
fprintf(stderr, "io_uring_queue_init failed: %s\n", strerror(-ret));
return 1;
}
// 2. 打开文件
fd = open(filename, O_RDONLY);
if (fd < 0) {
perror("open failed");
io_uring_queue_exit(&ring);
return 1;
}
// 获取文件大小
if (fstat(fd, &st) < 0) {
perror("fstat failed");
close(fd);
io_uring_queue_exit(&ring);
return 1;
}
// 计算需要多少个块来读取整个文件
int num_blocks = (st.st_size + BLOCK_SIZE - 1) / BLOCK_SIZE;
printf("Reading file '%s' (Size: %ld bytes) in %d blocks.\n",
filename, st.st_size, num_blocks);
// 3. 提交所有读取请求
for (int i = 0; i < num_blocks; ++i) {
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring); // 获取一个 SQE
if (!sqe) {
fprintf(stderr, "io_uring_get_sqe failed\n");
break;
}
// 分配并设置请求上下文
struct read_data *data = malloc(sizeof(*data));
data->offset = (off_t)i * BLOCK_SIZE;
data->length = (i == num_blocks - 1) ? (st.st_size % BLOCK_SIZE) : BLOCK_SIZE;
if (data->length == 0) data->length = BLOCK_SIZE; // 确保最后一小块大于0
data->buffer = malloc(data->length);
if (!data->buffer) {
perror("malloc buffer failed");
free(data);
break;
}
// 设置 SQE:这是一个异步读取请求
io_uring_prep_read(sqe, fd, data->buffer, data->length, data->offset);
// 将请求上下文指针设置到 SQE 的 user_data 字段,以便完成时能取回
io_uring_sqe_set_data(sqe, data);
}
// 4. 提交请求到内核
// io_uring_submit() 执行一次系统调用,将所有准备好的 SQE 提交给内核。
ret = io_uring_submit(&ring);
if (ret < 0) {
fprintf(stderr, "io_uring_submit failed: %s\n", strerror(-ret));
close(fd);
io_uring_queue_exit(&ring);
return 1;
}
printf("%d requests submitted.\n", ret);
// 5. 等待所有请求完成并消费结果
struct io_uring_cqe *cqe;
int completed = 0;
while (completed < num_blocks) {
// 等待一个或多个完成事件 (可能会阻塞,直到有事件完成)
ret = io_uring_wait_cqe(&ring, &cqe);
if (ret < 0) {
fprintf(stderr, "io_uring_wait_cqe failed: %s\n", strerror(-ret));
break;
}
// 从 CQE 中取回请求上下文
struct read_data *data = io_uring_cqe_get_data(cqe);
// 检查结果
if (cqe->res < 0) {
fprintf(stderr, "Read failed for offset %ld: %s\n",
data->offset, strerror(-cqe->res));
} else if (cqe->res != data->length) {
// 简单处理:实际读取字节数与请求不符
fprintf(stderr, "Short read for offset %ld: expected %zu, got %d\n",
data->offset, data->length, cqe->res);
} else {
// 成功:现在 data->buffer 中包含了从文件中读取的数据
// 在实际应用中,你会在成功的回调中处理这些数据
// printf("Successfully read %d bytes at offset %ld.\n", cqe->res, data->offset);
}
// 清理内存
free(data->buffer);
free(data);
// 标记该 CQE 已被消费,并释放 CQE
io_uring_cqe_seen(&ring, cqe);
completed++;
}
// 6. 清理
close(fd);
io_uring_queue_exit(&ring);
printf("All %d reads completed and cleaned up.\n", completed);
return 0;
}
此示例清晰地演示了io_uring的基础应用模式,它是理解和运用这一高性能框架的绝佳起点。