io_getevents 和 io_pgetevents 系统调用及示例

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

1. 函数介绍

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

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

  1. 提交请求:你告诉内核:“请帮我从文件描述符 fd 读取数据到 buffer”,然后你的程序立即返回,可以去做其他事情。
  2. 内核处理:内核在后台执行这个读取操作。
  3. 获取结果:过一段时间后,你再询问内核:“之前那个读取操作完成了吗?”。如果完成了,内核会告诉你结果(读取了多少字节,是否出错等)。

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. 函数原型

// 需要定义宏来启用 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);

注意

  1. 这些是底层系统调用。标准 C 库(glibc)可能不直接提供用户友好的包装函数,或者支持不完整(io_pgetevents 较新,可能需要较新版本 glibc)。
  2. 通常需要通过 syscall() 函数并传入系统调用号来调用它们。
  3. 需要包含 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)

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

  • EFAULTevents 或 timeout 指向了无效的内存地址。
  • EINTR: 系统调用被信号中断(对于 io_getevents)。对于 io_pgetevents,如果 sigmask 为 NULL,也可能发生。
  • EINVALmin_nr 大于 nr,或者 ctx_id 无效。
  • ENOMEM: 内核内存不足。
  • EBADFctx_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_setupio_submitio_getevents 来执行基本的异步 I/O 操作。由于 io_pgetevents 的使用方式类似且需要处理信号,此处主要演示 io_getevents

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

#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. 编译和运行

# 假设代码保存在 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 的补充说明

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

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

// 假设已定义 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:增强版本,在等待期间可以原子性地设置信号掩码,提供更精细的信号控制。
  • 工作流程
    1. io_setup 创建上下文。
    2. 构造 iocb 请求并用 io_submit 提交。
    3. 使用 io_getevents/io_pgetevents 等待和获取完成事件。
    4. io_destroy 销毁上下文。
  • 优势:允许程序在 I/O 操作进行的同时执行其他任务,提高并发性能。
  • 局限性
    • 传统 Linux AIO 对于 buffered 文件 I/O 支持不佳,可能退化为同步。
    • API 相对复杂,直接使用系统调用较为繁琐。
  • 现代替代:对于新的高性能异步 I/O 应用,强烈推荐使用 io_uring,它提供了更强大、更易用、性能更好的异步 I/O 接口。

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

此条目发表在未分类分类目录。将固定链接加入收藏夹。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注