io_getevents系统调用及示例

我们来深入学习 io_getevents 和 io_pgetevents 系统调用,从 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_submit 系列函数用于提交异步 I/O 请求,而 io_getevents 和 io_pgetevents 则用于获取这些已提交请求的完成状态(事件)。

  • io_getevents: 从指定的异步 I/O 上下文(context)中获取已完成的 I/O 事件。

  • io_pgetevents: 是 io_getevents 的扩展版本,它在获取事件的同时,可以设置一个信号掩码(就像 pselect 或 ppoll 一样),在等待事件期间临时改变进程的信号屏蔽字。

简单来说:

  • io_getevents:问内核:“有哪些我之前提交的异步读写操作已经完成了?”

  • io_pgetevents:和 io_getevents 功能一样,但可以在等待时临时调整对信号的响应。

2. 函数原型

1
2
3
4
5
6
7
8
9
10
11
12
13
// 需要定义宏来启用 AIO 和 io_pgetevents
#define _GNU_SOURCE
#include <linux/aio_abi.h> // 包含 AIO 相关结构体和常量 (io_context_t, io_event, iocb)
#include <sys/syscall.h> // 包含 syscall 函数和系统调用号
#include <unistd.h> // 包含 syscall 函数
#include <signal.h> // 包含 sigset_t 等 (io_pgetevents)

// io_getevents 系统调用
long syscall(SYS_io_getevents, io_context_t ctx_id, long min_nr, long nr, struct io_event *events, struct timespec *timeout);

// io_pgetevents 系统调用
long syscall(SYS_io_pgetevents, io_context_t ctx_id, long min_nr, long nr, struct io_event *events, struct timespec *timeout, const sigset_t *sigmask);

注意:

这些是底层系统调用。标准 C 库(glibc)可能不直接提供用户友好的包装函数,或者支持不完整(io_pgetevents 较新,可能需要较新版本 glibc)。

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

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

3. 功能

  • io_getevents: 尝试从异步 I/O 上下文 ctx_id 中获取至少 min_nr 个、最多 nr 个已完成的 I/O 事件,并将它们存储在 events 指向的数组中。如果没有任何事件完成,它会根据 timeout 参数决定是阻塞等待还是立即返回。

  • io_pgetevents: 功能与 io_getevents 相同,但在等待事件期间,会将调用进程的信号屏蔽字临时设置为 sigmask 指向的掩码。这可以防止在等待过程中被不希望的信号中断。

4. 参数详解

io_getevents(io_context_t ctx_id, long min_nr, long nr, struct io_event *events, struct timespec *timeout)

ctx_id:

  • io_context_t 类型。

  • 一个异步 I/O 上下文的标识符。这个上下文是通过 io_setup 系统调用创建的,用于管理一组异步 I/O 操作。

min_nr:

  • long 类型。

  • 指定函数希望返回的最少事件数量。如果已完成的事件少于 min_nr,函数可能会根据 timeout 选择等待。

nr:

  • long 类型。

  • 指定 events 数组能容纳的最大事件数量。函数返回的事件数不会超过 nr。

events:

  • struct io_event * 类型。

  • 一个指向 struct io_event 数组的指针。函数成功返回时,会将获取到的已完成事件信息填充到这个数组中。

struct io_event 结构体包含:

  • __u64 data;:与请求关联的用户数据(通常是你在 iocb 中设置的 data 字段)。

  • __u64 obj;:指向完成的 iocb 的指针(内核空间地址)。

  • __s64 res;:操作结果。对于读/写操作,这是传输的字节数;对于失败的操作,这是一个负的错误码(如 -EIO)。

  • __s64 res2;:预留字段。

timeout:

  • struct timespec * 类型。

  • 指向一个 timespec 结构体,指定等待事件的超时时间。

  • 如果为 NULL,函数会无限期阻塞,直到至少有 min_nr 个事件完成。

  • 如果 tv_sec 和 tv_nsec 都为 0,函数会立即返回,不进行任何等待,只返回当前已有的事件。

  • 否则,函数最多等待指定的时间。

io_pgetevents(io_context_t ctx_id, long min_nr, long nr, struct io_event *events, struct timespec *timeout, const sigset_t *sigmask)

  • 前五个参数与 io_getevents 完全相同。

sigmask:

  • const sigset_t * 类型。

  • 一个指向信号集的指针。在 io_pgetevents 执行等待(如果需要等待)期间,调用进程的信号屏蔽字会被临时替换为 sigmask 指向的信号集。等待结束后,信号屏蔽字会恢复为原始值。

  • 这使得程序可以在等待 I/O 事件时,精确控制哪些信号可以中断等待。

5. 返回值

两者返回值相同:

  • 成功: 返回实际获取到的事件数量(大于等于 0,小于等于 nr)。

  • 失败: 返回 -1,并设置全局变量 errno 来指示具体的错误原因。

6. 错误码 (errno)

两者共享许多相同的错误码:

  • EFAULT: events 或 timeout 指向了无效的内存地址。

  • EINTR: 系统调用被信号中断(对于 io_getevents)。对于 io_pgetevents,如果 sigmask 为 NULL,也可能发生。

  • EINVAL: min_nr 大于 nr,或者 ctx_id 无效。

  • ENOMEM: 内核内存不足。

  • EBADF: ctx_id 不是一个有效的异步 I/O 上下文。

7. 相似函数或关联函数

  • io_setup: 创建一个异步 I/O 上下文。

  • io_destroy: 销毁一个异步 I/O 上下文。

  • io_submit: 向异步 I/O 上下文提交一个或多个 I/O 请求 (iocb)。

  • io_cancel: 尝试取消一个已提交但尚未完成的 I/O 请求。

  • struct io_context_t: 异步 I/O 上下文的类型。

  • struct iocb: 描述单个异步 I/O 请求的结构体。

  • struct io_event: 描述单个已完成 I/O 事件的结构体。

8. 示例代码

下面的示例演示了如何使用 io_setup, io_submit, io_getevents 来执行基本的异步 I/O 操作。由于 io_pgetevents 的使用方式类似且需要处理信号,此处主要演示 io_getevents。

警告:Linux 原生 AIO (io_uring 之前的 AIO) 对于文件 I/O 的支持在某些场景下(如 buffered I/O)可能退化为同步操作。对于高性能异步 I/O,现代推荐使用 io_uring。此处仅为演示 io_getevents 的用法。

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
#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>
#include <sys/time.h> // 包含 gettimeofday

// 辅助函数:调用 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_read(struct iocb *iocb, int fd, void *buf, size_t count, __u64 offset, __u64 data) {
// 清零结构体
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 = data;
}

int main() {
const char *filename = "aio_test_file.txt";
const int num_reads = 3;
const size_t chunk_size = 1024;
int fd;
io_context_t ctx = 0; // 必须初始化为 0
struct iocb iocbs&#91;num_reads];
struct iocb *iocb_ptrs&#91;num_reads];
char buffers&#91;num_reads]&#91;chunk_size];
struct io_event events&#91;num_reads];
struct timespec timeout;
int ret, i;
struct timeval start, end;
double elapsed_time;

printf("--- Demonstrating io_getevents (Linux AIO) ---\n");

// 1. 创建一个测试文件
fd = open(filename, O_CREAT | O_TRUNC | O_WRONLY, 0644);
if (fd == -1) {
perror("open (create)");
exit(EXIT_FAILURE);
}
char test_data&#91;1024];
memset(test_data, 'A', sizeof(test_data));
for (int j = 0; j < 10; ++j) { // 写入 10KB 数据
if (write(fd, test_data, sizeof(test_data)) != sizeof(test_data)) {
perror("write");
close(fd);
exit(EXIT_FAILURE);
}
}
close(fd);
printf("Created test file '%s' with 10KB of data.\n", filename);

// 2. 以只读方式打开文件
fd = open(filename, O_RDONLY);
if (fd == -1) {
perror("open (read)");
exit(EXIT_FAILURE);
}

// 3. 初始化异步 I/O 上下文
// 我们需要能处理至少 num_reads 个并发请求
ret = io_setup(num_reads, &ctx);
if (ret < 0) {
perror("io_setup");
close(fd);
exit(EXIT_FAILURE);
}
printf("Initialized AIO context.\n");

// 4. 准备 I/O 请求 (iocb)
for (i = 0; i < num_reads; ++i) {
// 从文件不同偏移读取
prep_read(&iocbs&#91;i], fd, buffers&#91;i], chunk_size, i * chunk_size, i + 1);
iocb_ptrs&#91;i] = &iocbs&#91;i];
printf("Prepared read request %d: offset=%zu, size=%zu\n", i+1, i * chunk_size, chunk_size);
}

// 5. 提交 I/O 请求
gettimeofday(&start, NULL);
printf("Submitting %d asynchronous read requests...\n", num_reads);
ret = io_submit(ctx, num_reads, iocb_ptrs);
if (ret != num_reads) {
fprintf(stderr, "io_submit failed: submitted %d, expected %d\n", ret, num_reads);
if (ret < 0) perror("io_submit");
io_destroy(ctx);
close(fd);
exit(EXIT_FAILURE);
}
gettimeofday(&end, NULL);
elapsed_time = ((end.tv_sec - start.tv_sec) * 1000.0) + ((end.tv_usec - start.tv_usec) / 1000.0);
printf("Submitted all requests in %.2f ms.\n", elapsed_time);

// 6. 等待并获取完成的事件 (使用 io_getevents)
printf("Waiting for completion events using io_getevents...\n");
gettimeofday(&start, NULL);

// 设置超时为 5 秒
timeout.tv_sec = 5;
timeout.tv_nsec = 0;

// 等待所有 num_reads 个事件完成
ret = io_getevents(ctx, num_reads, num_reads, events, &timeout);
gettimeofday(&end, NULL);
elapsed_time = ((end.tv_sec - start.tv_sec) * 1000.0) + ((end.tv_usec - start.tv_usec) / 1000.0);

if (ret < 0) {
perror("io_getevents");
io_destroy(ctx);
close(fd);
exit(EXIT_FAILURE);
}

if (ret < num_reads) {
printf("Warning: Only got %d events out of %d expected within timeout.\n", ret, num_reads);
} else {
printf("Received all %d completion events in %.2f ms.\n", ret, elapsed_time);
}

// 7. 处理完成的事件
printf("\n--- Processing Completion Events ---\n");
for (i = 0; i < ret; ++i) {
struct io_event *ev = &events&#91;i];
printf("Event %d:\n", i+1);
printf(" Request ID (user data): %llu\n", (unsigned long long)ev->data);
// printf(" Request pointer: %llu\n", (unsigned long long)ev->obj); // 内核地址,通常不直接使用
if (ev->res >= 0) {
printf(" Result: Success, %lld bytes read.\n", (long long)ev->res);
// 可以检查 buffers&#91;ev->data - 1] 中的数据
// printf(" First byte: %c\n", buffers&#91;ev->data - 1]&#91;0]);
} else {
printf(" Result: Error, code %lld (%s)\n", (long long)ev->res, strerror(-ev->res));
}
printf("\n");
}

// 8. 清理资源
printf("--- Cleaning up ---\n");
io_destroy(ctx);
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_getevents retrieves completed asynchronous I/O operations.\n");
printf("2. It works with an io_context_t created by io_setup.\n");
printf("3. It waits for events based on min_nr, nr, and timeout.\n");
printf("4. io_pgetevents is similar but allows setting a signal mask during wait.\n");
printf("5. Linux AIO has some limitations; io_uring is the modern, preferred approach.\n");

return 0;
}

9. 编译和运行

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

# 运行程序
./aio_getevents_example

10. 预期输出

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
--- Demonstrating io_getevents (Linux AIO) ---
Created test file 'aio_test_file.txt' with 10KB of data.
Initialized AIO context.
Prepared read request 1: offset=0, size=1024
Prepared read request 2: offset=1024, size=1024
Prepared read request 3: offset=2048, size=1024
Submitting 3 asynchronous read requests...
Submitted all requests in 0.05 ms.
Waiting for completion events using io_getevents...
Received all 3 completion events in 2.15 ms.

--- Processing Completion Events ---
Event 1:
Request ID (user data): 1
Result: Success, 1024 bytes read.

Event 2:
Request ID (user data): 2
Result: Success, 1024 bytes read.

Event 3:
Request ID (user data): 3
Result: Success, 1024 bytes read.

--- Cleaning up ---
Destroyed AIO context.
Closed file descriptor.
Deleted test file 'aio_test_file.txt'.

--- Summary ---
1. io_getevents retrieves completed asynchronous I/O operations.
2. It works with an io_context_t created by io_setup.
3. It waits for events based on min_nr, nr, and timeout.
4. io_pgetevents is similar but allows setting a signal mask during wait.
5. Linux AIO has some limitations; io_uring is the modern, preferred approach.

11. 关于 io_pgetevents 的补充说明

io_pgetevents 的使用场景相对较少,主要是在需要精确控制信号处理的异步 I/O 程序中。例如,你可能希望在等待 I/O 完成时,只允许 SIGUSR1 信号中断,而屏蔽其他所有信号。这时就可以构建一个只包含 SIGUSR1 的 sigset_t,并传递给 io_pgetevents。

其函数原型和调用方式与 io_getevents 类似,只是多了一个 sigmask 参数:

1
2
3
4
5
6
7
8
9
10
11
12
// 假设已定义 syscall 号 __NR_io_pgetevents
long io_pgetevents(io_context_t ctx, long min_nr, long nr, struct io_event *events, struct timespec *timeout, const sigset_t *sigmask);

// 使用示例 (概念性)
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGUSR1);

struct timespec timeout = {5, 0}; // 5秒超时
int ret = syscall(__NR_io_pgetevents, ctx, 1, 1, events, &timeout, &mask);
// 在等待期间,只有 SIGUSR1 能中断此调用

12. 总结

io_getevents 和 io_pgetevents 是 Linux 异步 I/O (AIO) 机制的重要组成部分。

  • 核心作用:从 AIO 上下文中获取已完成的 I/O 操作的结果(事件)。

  • io_getevents:基础版本,用于等待和获取事件。

  • io_pgetevents:增强版本,在等待期间可以原子性地设置信号掩码,提供更精细的信号控制。

工作流程:

  • io_setup 创建上下文。

  • 构造 iocb 请求并用 io_submit 提交。

  • 使用 io_getevents/io_pgetevents 等待和获取完成事件。

  • io_destroy 销毁上下文。

  • 优势:允许程序在 I/O 操作进行的同时执行其他任务,提高并发性能。

局限性:

  • 传统 Linux AIO 对于 buffered 文件 I/O 支持不佳,可能退化为同步。

  • API 相对复杂,直接使用系统调用较为繁琐。

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

对于 Linux 编程新手,理解 io_getevents 的工作原理有助于掌握异步编程的思想,尽管在实践中可能更倾向于使用更高级的封装或 io_uring。

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