io_setup/io_submit系统调用及示例

我们来深入学习 io_setup 和 io_submit 系统调用,从 Linux 编程小白的角度出发。

1. 函数介绍

在 Linux 系统编程中,进行文件 I/O 操作(如 read, write)通常是同步的。这意味着当你的程序调用 read(fd, buffer, size) 时,程序会一直等待,直到内核从磁盘(或网络、设备等)读取完数据并放入 buffer 中,然后 read 函数才返回。如果数据读取很慢(例如从机械硬盘读取大量数据),你的程序就会在这段时间内卡住,无法执行其他任务。

data-ad-format="fluid" data-ad-layout-key="-7k+ex-4a-9w+4a">

为了提高性能,特别是对于高并发的服务器程序,Linux 提供了异步 I/O (Asynchronous I/O, AIO) 机制。核心思想是:

提交请求:你告诉内核:“请帮我从文件描述符 fd 读取数据到 buffer”,然后你的程序立即返回,可以去做其他事情。

内核处理:内核在后台执行这个读取操作。

获取结果:过一段时间后,你再询问内核:“之前那个读取操作完成了吗?”。如果完成了,内核会告诉你结果(读取了多少字节,是否出错等)。

io_setup 和 io_submit 就是这个异步 I/O 机制的第一步和第二步。

  • io_setup: 创建一个异步 I/O 上下文 (context)。你可以把它想象成一个“工作队列”或“任务列表”的管理器。所有你提交给这个上下文的异步 I/O 请求都会被它管理。

  • io_submit: 提交一个或多个异步 I/O 请求(读、写等)到一个已创建的上下文中。内核会接收这些请求,并在后台开始执行它们。

简单来说:

  • io_setup:创建一个“异步任务管理器”。

  • io_submit:把“异步任务”(读/写文件)交给这个“管理器”去执行。

2. 函数原型

1
2
3
4
5
6
7
8
9
10
11
12
// 需要定义宏来启用 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);

注意:

这些是底层系统调用。标准 C 库(glibc)可能不直接提供用户友好的包装函数。

通常需要通过 syscall() 函数并传入系统调用号来调用它们。

需要包含 linux/aio_abi.h 头文件来获取相关结构体和类型定义。

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 结构体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 这是 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 的用法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
#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&#91;num_ops];
struct iocb *iocb_ptrs&#91;num_ops];
char write_buffers&#91;2]&#91;chunk_size];
char read_buffers&#91;2]&#91;chunk_size];
struct io_event events&#91;num_ops];
int ret, i;

printf("--- Demonstrating io_setup and io_submit (Linux AIO) ---\n");

// 1. 初始化要写入的数据
memset(write_buffers&#91;0], 'A', chunk_size);
memset(write_buffers&#91;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&#91;0], fd, write_buffers&#91;0], chunk_size, 0);
prep_pwrite(&iocbs&#91;1], fd, write_buffers&#91;1], chunk_size, chunk_size);
iocb_ptrs&#91;0] = &iocbs&#91;0];
iocb_ptrs&#91;1] = &iocbs&#91;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&#91;i].res < 0) {
fprintf(stderr, "Write error: %s\n", strerror(-events&#91;i].res));
} else {
printf("Write %d completed: %lld bytes written.\n", i+1, (long long)events&#91;i].res);
}
}
}

// 7. 准备读取请求
prep_pread(&iocbs&#91;2], fd, read_buffers&#91;0], chunk_size, 0);
prep_pread(&iocbs&#91;3], fd, read_buffers&#91;1], chunk_size, chunk_size);
iocb_ptrs&#91;0] = &iocbs&#91;2]; // 重用指针数组
iocb_ptrs&#91;1] = &iocbs&#91;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&#91;i].res < 0) {
fprintf(stderr, "Read error: %s\n", strerror(-events&#91;i].res));
} else {
printf("Read %d completed: %lld bytes read.\n", i+1, (long long)events&#91;i].res);
// 简单验证读取的数据
char expected_char = (i == 0) ? 'A' : 'B';
if (((char*)(events&#91;i].data))&#91;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. 编译和运行

1
2
3
4
5
6
# 假设代码保存在 aio_setup_submit_example.c 中
gcc -o aio_setup_submit_example aio_setup_submit_example.c

# 运行程序
./aio_setup_submit_example

11. 预期输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
--- 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:描述单个异步请求(操作类型、文件、缓冲区、偏移等)。

工作流程:

调用 io_setup 创建上下文。

为每个 I/O 操作填充一个 iocb 结构体。

调用 io_submit 提交这些 iocb。

(稍后)调用 io_getevents 获取完成状态。

(最后)调用 io_destroy 销毁上下文。

局限性:

  • 传统 Linux AIO 对 buffered 文件 I/O 支持不佳,性能可能不理想。

  • API 相对底层和复杂。

现代替代:对于新的高性能异步 I/O 应用,强烈推荐使用 io_uring,它提供了更强大、更易用、性能更好的异步 I/O 接口。

对于 Linux 编程新手,理解 io_setup 和 io_submit 是学习异步 I/O 的重要一步,尽管在实践中可能更倾向于使用更高级的封装或 io_uring。

data-ad-format="auto" data-full-width-responsive="true">