io_setup系统调用及示例
我们来深入学习 io_setup
和 io_submit
系统调用,从 Linux 编程小白的角度出发。
1. 函数介绍
在 Linux 系统编程中,进行文件 I/O 操作(如 read
, write
)通常是同步的。这意味着当你的程序调用 read(fd, buffer, size)
时,程序会一直等待,直到内核从磁盘(或网络、设备等)读取完数据并放入 buffer
中,然后 read
函数才返回。如果数据读取很慢(例如从机械硬盘读取大量数据),你的程序就会在这段时间内卡住,无法执行其他任务。
为了提高性能,特别是对于高并发的服务器程序,Linux 提供了异步 I/O (Asynchronous I/O, AIO) 机制。核心思想是:
1. 提交请求:你告诉内核:“请帮我从文件描述符 fd
读取数据到 buffer
”,然后你的程序立即返回,可以去做其他事情。
2. 内核处理:内核在后台执行这个读取操作。
3. 获取结果:过一段时间后,你再询问内核:“之前那个读取操作完成了吗?”。如果完成了,内核会告诉你结果(读取了多少字节,是否出错等)。
io_setup
和 io_submit
就是这个异步 I/O 机制的第一步和第二步。
io_setup
: 创建一个异步 I/O 上下文 (context)。你可以把它想象成一个“工作队列”或“任务列表”的管理器。所有你提交给这个上下文的异步 I/O 请求都会被它管理。io_submit
: 提交一个或多个异步 I/O 请求(读、写等)到一个已创建的上下文中。内核会接收这些请求,并在后台开始执行它们。
简单来说:
io_setup
:创建一个“异步任务管理器”。io_submit
:把“异步任务”(读/写文件)交给这个“管理器”去执行。
2. 函数原型
// 需要定义宏来启用 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);
注意:
1. 这些是底层系统调用。标准 C 库(glibc)可能不直接提供用户友好的包装函数。
2. 通常需要通过 syscall()
函数并传入系统调用号来调用它们。
3. 需要包含 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
结构体。
// 这是 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
的用法。
#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; // 必须初始化为 0struct 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. 编译和运行
# 假设代码保存在 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
。