我们来深入学习 io_setup
和 io_submit
系统调用,从 Linux 编程小白的角度出发。
1. 函数介绍
为了提高性能,特别是对于高并发的服务器程序,Linux 提供了异步 I/O (Asynchronous I/O, AIO) 机制。核心思想是:
io_setup
和 io_submit
就是这个异步 I/O 机制的第一步 和第二步。
io_setup
: 创建一个异步 I/O 上下文 (context)。你可以把它想象成一个"工作队列"或"任务列表"的管理器。所有你提交给这个上下文的异步 I/O 请求都会被它管理。io_submit
: 提交一个或多个异步 I/O 请求(读、写等)到一个已创建的上下文中。内核会接收这些请求,并在后台开始执行它们。
简单来说:
io_setup
:创建一个"异步任务管理器"。io_submit
:把"异步任务"(读/写文件)交给这个"管理器"去执行。
2. 函数原型
c
// 需要定义宏来启用 AIO
#define _GNU_SOURCE
#include <linux/aio_abi.h> // 包含 AIO 相关结构体和常量 (io_context_t, iocb)
#include <sys/syscall.h> // 包含 syscall 函数和系统调用号
#include <unistd.h> // 包含 syscall 函数
// io_setup 系统调用
long syscall(SYS_io_setup, unsigned nr_events, io_context_t *ctxp);
// io_submit 系统调用
long syscall(SYS_io_submit, io_context_t ctx_id, long nr, struct iocb **iocbpp);
3. 功能
io_setup
: 初始化一个异步 I/O 上下文,该上下文能够处理最多nr_events
个并发的异步 I/O 操作,并将上下文的标识符(句柄)存储在ctxp
指向的变量中。io_submit
: 将nr
个异步 I/O 请求(由iocbpp
数组指向)提交到由ctx_id
标识的异步 I/O 上下文中。内核会尝试立即开始处理这些请求。
4. 参数详解
io_setup(unsigned nr_events, io_context_t *ctxp)
nr_events
:unsigned
类型。- 指定这个异步 I/O 上下文最多可以同时处理多少个未完成的异步 I/O 请求(事件)。这相当于预分配了资源来跟踪这些请求。
- 内核可能会将这个值向上舍入到内部优化所需的大小。
ctxp
:io_context_t *
类型。- 一个指向
io_context_t
类型变量的指针。io_setup
调用成功后,会将新创建的异步 I/O 上下文的标识符 (或句柄)写入到这个变量中。后续的io_submit
,io_getevents
等操作都需要使用这个ctx_id
。
io_submit(io_context_t ctx_id, long nr, struct iocb **iocbpp)
ctx_id
:io_context_t
类型。- 由
io_setup
返回的异步 I/O 上下文的标识符。
nr
:long
类型。- 指定要提交的异步 I/O 请求数量。这个值应该与
iocbpp
数组的大小相对应。
iocbpp
:struct iocb **
类型。- 一个指针数组,数组中的每个元素都指向一个
struct iocb
结构体。struct iocb
(I/O Control Block)描述了一个单独的异步 I/O 请求。 - 数组的大小至少为
nr
。
5. struct iocb
结构体
这是描述单个异步 I/O 请求的核心结构体。你需要为每个你想要提交的异步操作(如一次读取或写入)填充一个 iocb
结构体。
c
// 这是 linux/aio_abi.h 中定义的简化版结构
struct iocb {
__u64 aio_data; // 用户定义的数据,通常用于匹配请求和完成事件
__u32 aio_key, aio_reserved1;
__u16 aio_lio_opcode; // 操作类型:IOCB_CMD_PREAD, IOCB_CMD_PWRITE 等
__s16 aio_reqprio; // 请求优先级 (通常保留为 0)
__u32 aio_fildes; // 文件描述符
__u64 aio_buf; // 缓冲区地址 (用户空间指针)
__u64 aio_nbytes; // 传输字节数
__s64 aio_offset; // 文件偏移量
// ... 还有其他字段,用于更高级的功能
};
关键字段:
aio_lio_opcode
: 指定操作类型。IOCB_CMD_PREAD
: 异步预读(Positioned Read)。IOCB_CMD_PWRITE
: 异步预写(Positioned Write)。IOCB_CMD_FSYNC
: 异步同步文件数据和元数据。IOCB_CMD_FDSYNC
: 异步同步文件数据。
aio_fildes
: 进行 I/O 操作的文件描述符。aio_buf
: 用户空间缓冲区的地址。aio_nbytes
: 要读取或写入的字节数。aio_offset
: 文件中的偏移量(类似pread
/pwrite
)。aio_data
: 用户自定义数据。当这个请求完成后,对应的完成事件 (io_event
) 会包含这个值,方便你识别是哪个请求完成了。
6. 返回值
io_setup
:- 成功: 返回 0。
- 失败 : 返回 -1,并设置
errno
。
io_submit
:- 成功 : 返回实际提交成功的请求数 (一个非负整数,可能小于或等于
nr
)。 - 失败 : 返回 -1,并设置
errno
。如果返回值是 0 到nr
之间的正数m
,则表示只有前m
个请求被成功提交,后面的请求提交失败。
- 成功 : 返回实际提交成功的请求数 (一个非负整数,可能小于或等于
7. 错误码 (errno
)
io_setup
EAGAIN
: 内核资源不足,无法创建新的上下文,或者达到用户/系统范围内的 AIO 上下文数量限制。EINVAL
:nr_events
为 0。ENOMEM
: 内存不足。
io_submit
EAGAIN
: 资源暂时不可用,例如提交队列已满。EBADF
:ctx_id
无效,或者iocbpp
中某个iocb
的aio_fildes
是无效的文件描述符。EINVAL
:ctx_id
无效,或者iocbpp
中某个iocb
的参数无效(例如aio_lio_opcode
未知)。ENOMEM
: 内存不足。
8. 相似函数或关联函数
io_getevents
: 用于从异步 I/O 上下文中获取已完成的 I/O 事件。io_destroy
: 用于销毁一个异步 I/O 上下文。io_cancel
: 用于尝试取消一个已提交但尚未完成的 I/O 请求。struct io_context_t
: 异步 I/O 上下文的类型。struct iocb
: 描述单个异步 I/O 请求的结构体。struct io_event
: 描述单个已完成 I/O 事件的结构体。io_uring
: Linux 5.1+ 引入的更现代、更高效的异步 I/O 接口。
9. 示例代码
下面的示例演示了如何使用 io_setup
和 io_submit
来执行基本的异步 I/O 操作,并使用 io_getevents
来获取结果。
警告 :Linux 原生 AIO (io_uring
之前的 AIO) 对于文件 I/O 的支持在某些场景下(如 buffered I/O)可能退化为同步操作。对于高性能异步 I/O,现代推荐使用 io_uring
。此处仅为演示 io_setup
和 io_submit
的用法。
c
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include <sys/stat.h>
#include <linux/aio_abi.h>
#include <sys/syscall.h>
// 辅助函数:调用 io_setup 系统调用
static inline int io_setup(unsigned nr_events, io_context_t *ctxp) {
return syscall(__NR_io_setup, nr_events, ctxp);
}
// 辅助函数:调用 io_destroy 系统调用
static inline int io_destroy(io_context_t ctx) {
return syscall(__NR_io_destroy, ctx);
}
// 辅助函数:调用 io_submit 系统调用
static inline int io_submit(io_context_t ctx, long nr, struct iocb **iocbpp) {
return syscall(__NR_io_submit, ctx, nr, iocbpp);
}
// 辅助函数:调用 io_getevents 系统调用
static inline int io_getevents(io_context_t ctx, long min_nr, long nr, struct io_event *events, struct timespec *timeout) {
return syscall(__NR_io_getevents, ctx, min_nr, nr, events, timeout);
}
// 辅助函数:初始化一个异步读取的 iocb 结构
void prep_pread(struct iocb *iocb, int fd, void *buf, size_t count, __u64 offset) {
// 清零结构体
memset(iocb, 0, sizeof(*iocb));
// 设置操作类型为pread (异步pread)
iocb->aio_lio_opcode = IOCB_CMD_PREAD;
// 设置文件描述符
iocb->aio_fildes = fd;
// 设置缓冲区
iocb->aio_buf = (__u64)(unsigned long)buf;
// 设置读取字节数
iocb->aio_nbytes = count;
// 设置文件偏移量
iocb->aio_offset = offset;
// 设置用户数据 (可选,用于匹配事件)
iocb->aio_data = (__u64)(unsigned long)buf; // 这里简单地用 buf 地址作为标识
}
// 辅助函数:初始化一个异步写入的 iocb 结构
void prep_pwrite(struct iocb *iocb, int fd, const void *buf, size_t count, __u64 offset) {
memset(iocb, 0, sizeof(*iocb));
iocb->aio_lio_opcode = IOCB_CMD_PWRITE;
iocb->aio_fildes = fd;
iocb->aio_buf = (__u64)(unsigned long)buf;
iocb->aio_nbytes = count;
iocb->aio_offset = offset;
iocb->aio_data = (__u64)(unsigned long)buf; // 用 buf 地址作为标识
}
int main() {
const char *filename = "aio_test_file.txt";
const int num_ops = 4; // 2次写入 + 2次读取
const size_t chunk_size = 1024;
int fd;
io_context_t ctx = 0; // 必须初始化为 0
struct iocb iocbs[num_ops];
struct iocb *iocb_ptrs[num_ops];
char write_buffers[2][chunk_size];
char read_buffers[2][chunk_size];
struct io_event events[num_ops];
int ret, i;
printf("--- Demonstrating io_setup and io_submit (Linux AIO) ---\n");
// 1. 初始化要写入的数据
memset(write_buffers[0], 'A', chunk_size);
memset(write_buffers[1], 'B', chunk_size);
// 2. 创建并打开文件 (O_DIRECT 对 AIO 更友好,但为简单起见使用普通模式)
fd = open(filename, O_CREAT | O_TRUNC | O_RDWR, 0644);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
printf("Opened/created file '%s'\n", filename);
// 3. 初始化异步 I/O 上下文
// 我们需要能处理至少 num_ops 个并发请求
ret = io_setup(num_ops, &ctx);
if (ret < 0) {
perror("io_setup");
close(fd);
exit(EXIT_FAILURE);
}
printf("Initialized AIO context (ctx_id=%llu).\n", (unsigned long long)ctx);
// 4. 准备写入请求
prep_pwrite(&iocbs[0], fd, write_buffers[0], chunk_size, 0);
prep_pwrite(&iocbs[1], fd, write_buffers[1], chunk_size, chunk_size);
iocb_ptrs[0] = &iocbs[0];
iocb_ptrs[1] = &iocbs[1];
printf("Prepared 2 write requests.\n");
// 5. 提交写入请求
printf("Submitting write requests...\n");
ret = io_submit(ctx, 2, iocb_ptrs);
if (ret != 2) {
fprintf(stderr, "io_submit (writes) failed: submitted %d, expected %d\n", ret, 2);
if (ret < 0) perror("io_submit");
io_destroy(ctx);
close(fd);
exit(EXIT_FAILURE);
}
printf("Submitted 2 write requests successfully.\n");
// 6. 等待写入完成 (简单起见,等待所有已提交的)
printf("Waiting for write completions...\n");
struct timespec timeout = {5, 0}; // 5秒超时
ret = io_getevents(ctx, 2, 2, events, &timeout);
if (ret < 0) {
perror("io_getevents (writes)");
io_destroy(ctx);
close(fd);
exit(EXIT_FAILURE);
}
if (ret < 2) {
printf("Warning: Only got %d write events, expected 2.\n", ret);
} else {
printf("Received %d write completion events.\n", ret);
for (i = 0; i < ret; ++i) {
if (events[i].res < 0) {
fprintf(stderr, "Write error: %s\n", strerror(-events[i].res));
} else {
printf("Write %d completed: %lld bytes written.\n", i+1, (long long)events[i].res);
}
}
}
// 7. 准备读取请求
prep_pread(&iocbs[2], fd, read_buffers[0], chunk_size, 0);
prep_pread(&iocbs[3], fd, read_buffers[1], chunk_size, chunk_size);
iocb_ptrs[0] = &iocbs[2]; // 重用指针数组
iocb_ptrs[1] = &iocbs[3];
printf("\nPrepared 2 read requests.\n");
// 8. 提交读取请求
printf("Submitting read requests...\n");
ret = io_submit(ctx, 2, iocb_ptrs);
if (ret != 2) {
fprintf(stderr, "io_submit (reads) failed: submitted %d, expected %d\n", ret, 2);
if (ret < 0) perror("io_submit");
io_destroy(ctx);
close(fd);
exit(EXIT_FAILURE);
}
printf("Submitted 2 read requests successfully.\n");
// 9. 等待读取完成
printf("Waiting for read completions...\n");
ret = io_getevents(ctx, 2, 2, events, &timeout);
if (ret < 0) {
perror("io_getevents (reads)");
io_destroy(ctx);
close(fd);
exit(EXIT_FAILURE);
}
if (ret < 2) {
printf("Warning: Only got %d read events, expected 2.\n", ret);
} else {
printf("Received %d read completion events.\n", ret);
for (i = 0; i < ret; ++i) {
if (events[i].res < 0) {
fprintf(stderr, "Read error: %s\n", strerror(-events[i].res));
} else {
printf("Read %d completed: %lld bytes read.\n", i+1, (long long)events[i].res);
// 简单验证读取的数据
char expected_char = (i == 0) ? 'A' : 'B';
if (((char*)(events[i].data))[0] == expected_char) {
printf(" Data verification OK for buffer %d.\n", i+1);
} else {
printf(" Data verification FAILED for buffer %d.\n", i+1);
}
}
}
}
// 10. 清理资源
printf("\n--- Cleaning up ---\n");
ret = io_destroy(ctx);
if (ret < 0) {
perror("io_destroy");
} else {
printf("Destroyed AIO context.\n");
}
close(fd);
printf("Closed file descriptor.\n");
unlink(filename); // 删除测试文件
printf("Deleted test file '%s'.\n", filename);
printf("\n--- Summary ---\n");
printf("1. io_setup(nr_events, &ctx): Creates an AIO context that can handle 'nr_events' concurrent operations.\n");
printf("2. io_submit(ctx_id, nr, iocb_ptrs): Submits 'nr' AIO requests (described by iocb structs) to the context.\n");
printf("3. struct iocb: Describes a single AIO operation (read/write/fsync etc.) with all necessary parameters.\n");
printf("4. These are the first steps in the Linux AIO workflow. Results are fetched with io_getevents().\n");
printf("5. Note: Traditional Linux AIO has limitations. io_uring is the modern, preferred approach.\n");
return 0;
}
10. 编译和运行
bash
# 假设代码保存在 aio_setup_submit_example.c 中
gcc -o aio_setup_submit_example aio_setup_submit_example.c
# 运行程序
./aio_setup_submit_example
11. 预期输出
--- Demonstrating io_setup and io_submit (Linux AIO) ---
Opened/created file 'aio_test_file.txt'
Initialized AIO context (ctx_id=123456789).
Prepared 2 write requests.
Submitting write requests...
Submitted 2 write requests successfully.
Waiting for write completions...
Received 2 write completion events.
Write 1 completed: 1024 bytes written.
Write 2 completed: 1024 bytes written.
Prepared 2 read requests.
Submitting read requests...
Submitted 2 read requests successfully.
Waiting for read completions...
Received 2 read completion events.
Read 1 completed: 1024 bytes read.
Data verification OK for buffer 1.
Read 2 completed: 1024 bytes read.
Data verification OK for buffer 2.
--- Cleaning up ---
Destroyed AIO context.
Closed file descriptor.
Deleted test file 'aio_test_file.txt'.
--- Summary ---
1. io_setup(nr_events, &ctx): Creates an AIO context that can handle 'nr_events' concurrent operations.
2. io_submit(ctx_id, nr, iocb_ptrs): Submits 'nr' AIO requests (described by iocb structs) to the context.
3. struct iocb: Describes a single AIO operation (read/write/fsync etc.) with all necessary parameters.
4. These are the first steps in the Linux AIO workflow. Results are fetched with io_getevents().
5. Note: Traditional Linux AIO has limitations. io_uring is the modern, preferred approach.
12. 总结
io_setup
和 io_submit
是 Linux 异步 I/O (AIO) 机制的入口点。
io_setup
:- 作用:创建一个管理和跟踪异步 I/O 操作的上下文。
- 参数 :指定上下文能处理的最大并发请求数 (
nr_events
) 和用于返回上下文 ID 的指针 (ctxp
)。
io_submit
:- 作用 :将描述异步操作的
iocb
结构体提交到指定的上下文中,让内核开始执行。 - 参数 :上下文 ID (
ctx_id
)、请求数量 (nr
) 和指向iocb
指针数组的指针 (iocbpp
)。
- 作用 :将描述异步操作的
- 核心概念 :
io_context_t
:异步 I/O 上下文的句柄。struct iocb
:描述单个异步请求(操作类型、文件、缓冲区、偏移等)。
- 工作流程 :
1. 调用io_setup
创建上下文。
2. 为每个 I/O 操作填充一个iocb
结构体。
3. 调用io_submit
提交这些iocb
。
4. (稍后)调用io_getevents
获取完成状态。
5. (最后)调用io_destroy
销毁上下文。 - 局限性 :
- 传统 Linux AIO 对 buffered 文件 I/O 支持不佳,性能可能不理想。
- API 相对底层和复杂。
- 现代替代 :对于新的高性能异步 I/O 应用,强烈推荐使用
io_uring
,它提供了更强大、更易用、性能更好的异步 I/O 接口。
对于 Linux 编程新手,理解 io_setup
和 io_submit
是学习异步 I/O 的重要一步,尽管在实践中可能更倾向于使用更高级的封装或 io_uring
。