我们来深入学习 io_getevents
和 io_pgetevents
系统调用,从 Linux 编程小白的角度出发。
1. 函数介绍
为了提高性能,特别是对于高并发的服务器程序,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. 函数原型
c
// 需要定义宏来启用 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);
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. 示例代码
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>
#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[num_reads];
struct iocb *iocb_ptrs[num_reads];
char buffers[num_reads][chunk_size];
struct io_event events[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[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[i], fd, buffers[i], chunk_size, i * chunk_size, i + 1);
iocb_ptrs[i] = &iocbs[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[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[ev->data - 1] 中的数据
// printf(" First byte: %c\n", buffers[ev->data - 1][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. 编译和运行
bash
# 假设代码保存在 aio_getevents_example.c 中
gcc -o aio_getevents_example aio_getevents_example.c
# 运行程序
./aio_getevents_example
10. 预期输出
--- 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
的补充说明
c
// 假设已定义 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
。