文章目录
- 0. 引言
- 1. I/O 模型简介
- 1.1 阻塞 I/O(Blocking I/O)
- 1.2 非阻塞 I/O(Non-Blocking I/O)
- 1.3 信号驱动式 I/O(Signal-Driven I/O)
- 1.4 多路复用 I/O(I/O Multiplexing)
- 1.5 异步 I/O(Asynchronous I/O)
- 2. 多路复用 I/O VS 异步 I/O 分析
- 2.1 多路复用 I/O
- 2.2 异步 I/O
- 2.3 多路复用 I/O 与异步 I/O 的比较
- 2.4 总结
- 3. 多路复用 I/O 和异步 I/O示例代码
- 3.1 多路复用 I/O 示例(`select`)
- 3.2 异步 I/O 示例(`aio_read`)
- 3.3 代码说明
- 4. 参考文档
0. 引言
本文将简要介绍 Linux 中的多路复用 I/O 和异步 I/O 模型,并编写示例代码简述。对于网络 I/O,特别是 UDP 和 TCP,多路复用 I/O(如 select
、epoll()
)通常是更好的选择。而异步 I/O(如 libaio
)只建议用在文件和块设备的 I/O 操作。
1. I/O 模型简介
Linux 提供了五种常见的 I/O 模型:阻塞 I/O、非阻塞 I/O、多路复用 I/O、信号驱动 I/O 和异步 I/O。
1.1 阻塞 I/O(Blocking I/O)
阻塞 I/O 是最简单的模型,进程会在 I/O 操作完成之前被挂起。适用于低并发、对性能要求不高的场景。
工作原理:
- 进程发起 I/O 操作,若数据不可用,进程会被阻塞,直到数据准备好。
1.2 非阻塞 I/O(Non-Blocking I/O)
非阻塞 I/O 允许进程继续执行其他任务,当数据不可用时,系统返回一个错误码(如 EAGAIN
)。进程可以选择继续做其他事情,或者再次尝试 I/O 操作。
工作原理:
- 进程发起 I/O 操作后,如果数据不可用,系统返回错误码,进程继续执行其他任务。
1.3 信号驱动式 I/O(Signal-Driven I/O)
通过内核向进程发送信号通知数据准备情况,进程可以继续执行其他任务,直到收到信号时再处理数据。
适用场景:不希望主动轮询的应用,通常用于低延迟要求的系统或设备驱动
1.4 多路复用 I/O(I/O Multiplexing)
多路复用 I/O 允许一个进程同时监控多个 I/O 通道,避免因等待 I/O 完成而导致的阻塞。
工作原理:
- 通过
select()
、poll()
或epoll()
等系统调用,进程可以同时监控多个 I/O 通道,当某个通道就绪时,进程会被通知。
1.5 异步 I/O(Asynchronous I/O)
异步 I/O 的核心思想是,进程发起 I/O 操作后,系统立即返回,I/O 操作在后台完成。当操作完成时,系统会通知进程。
工作原理:
- 进程发起异步 I/O 操作,系统立即返回,I/O 在后台进行,完成时通知进程。
2. 多路复用 I/O VS 异步 I/O 分析
2.1 多路复用 I/O
工作原理:
多路复用 I/O 允许一个进程同时监控多个 I/O 通道。在 Linux 中,常用的系统调用包括 select()
、poll()
和 epoll()
:
- select():适合少量文件描述符,效率较低。
- poll():与
select()
类似,但支持更多文件描述符。 - epoll():高效地处理大量并发连接,采用事件驱动机制。
优劣势:
多路复用允许单个进程处理多个 I/O 操作,减少线程切换的开销。然而,随着文件描述符数量的增加,轮询的开销也会增加。epoll()
通过事件驱动的方式,在大规模并发场景中表现优异。
2.2 异步 I/O
工作原理:
异步 I/O 的关键是,进程发起 I/O 操作后,系统立即返回,后台继续执行 I/O 操作,而进程可以继续执行其他任务。当 I/O 操作完成时,系统通过回调函数、信号或其他机制通知进程。
Linux 中的异步 I/O 接口有两种主要实现方式:
- POSIX AIO:通过
aio_read
和aio_write
提交异步 I/O 操作,操作完成后,进程可以通过aio_error
和aio_return
获取结果。需要注意的是,POSIX AIO 在某些 Linux 发行版中可能是基于用户空间的模拟,而不是真正的异步 I/O,这可能会影响其性能。 - libaio:高效的异步 I/O 接口,直接与内核交互,减少线程上下文切换。
libaio
特别适合磁盘和文件系统的异步块读写操作。
带来的挑战:
- 资源管理:异步 I/O 需要额外的内存和控制块,可能导致内存泄漏或资源管理困难。
- 线程调度:异步 I/O 可能会引入线程切换和上下文切换的开销,尤其在大量小 I/O 操作时,性能可能下降。
- 错误处理:异步 I/O 错误处理较为复杂,特别是回调嵌套或并发操作时,状态同步问题尤为突出。
2.3 多路复用 I/O 与异步 I/O 的比较
-
适用场景:
- 多路复用 I/O:适用于网络 I/O 场景,特别是 UDP 和 TCP。
epoll()
在处理大量并发连接时表现优异。 - 异步 I/O:适用于文件和块设备的 I/O 操作,如磁盘读写。
libaio
在减少上下文切换和提高磁盘 I/O 性能方面效果显著。
- 多路复用 I/O:适用于网络 I/O 场景,特别是 UDP 和 TCP。
-
性能和复杂性:
- 多路复用 I/O:简单易用,适合大多数网络应用场景。
epoll()
在高并发场景下性能优越。 - 异步 I/O:虽然性能潜力大,但实现复杂,错误处理和资源管理较为困难。
- 多路复用 I/O:简单易用,适合大多数网络应用场景。
-
UDP 无连接协议:UDP 本身是无连接协议,内核的非阻塞 I/O 和事件驱动模型已经非常高效。UDP 数据包的发送操作不涉及复杂的连接管理,使用非阻塞 I/O 或事件驱动模型(如
select
、epoll
)即可高效处理并发。
2.4 总结
对于网络 I/O,特别是 UDP 和 TCP,多路复用 I/O(如 select
、epoll()
)通常是更好的选择。而异步 I/O(如 libaio
)只建议用在文件和块设备的 I/O 操作。
3. 多路复用 I/O 和异步 I/O示例代码
3.1 多路复用 I/O 示例(select
)
这个例子演示了如何使用 select
实现多路复用 I/O,监听多个文件描述符(如标准输入和套接字)。
#include <iostream>
#include <unistd.h>
#include <sys/select.h>
#include <fcntl.h>
#include <string.h>
int main() {
fd_set read_fds;
struct timeval timeout;
// 创建标准输入的文件描述符集
FD_ZERO(&read_fds);
FD_SET(STDIN_FILENO, &read_fds);
// 设置 select 的超时,单位是秒和微秒
timeout.tv_sec = 5;
timeout.tv_usec = 0;
int ret = select(STDIN_FILENO + 1, &read_fds, NULL, NULL, &timeout);
if (ret == -1) {
std::cerr << "select failed!" << std::endl;
return -1;
} else if (ret == 0) {
std::cout << "Timeout: No input within 5 seconds" << std::endl;
} else {
if (FD_ISSET(STDIN_FILENO, &read_fds)) {
char buffer[1024];
ssize_t len = read(STDIN_FILENO, buffer, sizeof(buffer) - 1);
if (len > 0) {
buffer[len] = '\0';
std::cout << "Input: " << buffer << std::endl;
}
}
}
return 0;
}
3.2 异步 I/O 示例(aio_read
)
这个例子演示了如何使用异步 I/O 读取文件内容。我们将使用 aio_read
来异步地从文件中读取数据。
#include <iostream>
#include <fstream>
#include <aio.h>
#include <unistd.h>
#include <fcntl.h>
#include <cstring>
#include <errno.h>
#define FILE_NAME "test.txt"
int main() {
// 打开文件
int fd = open(FILE_NAME, O_RDONLY);
if (fd == -1) {
std::cerr << "Failed to open file: " << strerror(errno) << std::endl;
return -1;
}
// 准备异步 I/O 控制块
struct aiocb aio;
memset(&aio, 0, sizeof(aio));
aio.aio_fildes = fd;
aio.aio_buf = malloc(1024); // 分配内存空间用于读取数据
aio.aio_nbytes = 1024;
aio.aio_offset = 0;
// 提交异步读取操作
if (aio_read(&aio) == -1) {
std::cerr << "aio_read failed: " << strerror(errno) << std::endl;
close(fd);
return -1;
}
// 等待异步 I/O 完成
while (aio_error(&aio) == EINPROGRESS) {
// 在此可以执行其他任务
std::cout << "Waiting for I/O to complete..." << std::endl;
sleep(1);
}
// 获取异步 I/O 操作的状态
int ret = aio_return(&aio);
if (ret == -1) {
std::cerr << "aio_return failed: " << strerror(errno) << std::endl;
free((void*)aio.aio_buf);
close(fd);
return -1;
}
// 输出读取的内容
std::cout << "Async read completed, data: " << (char*)aio.aio_buf << std::endl;
// 清理资源
free((void*)aio.aio_buf);
close(fd);
return 0;
}
3.3 代码说明
多路复用 I/O (select
):
FD_SET()
和FD_ZERO()
用于设置和清空文件描述符集。select()
会等待 5 秒钟,如果标准输入文件描述符准备好,可以读取数据。- 如果超时或没有输入,程序会输出“Timeout”。
异步 I/O (aio_read
):
- 使用
open()
打开一个文件并读取数据。 - 设置异步 I/O 控制块
aiocb
,并调用aio_read()
提交异步操作。 - 通过
aio_error()
检查 I/O 操作是否正在进行,直到操作完成。 - 一旦 I/O 完成,使用
aio_return()
获取结果,并输出异步读取的内容。
编译和运行:
g++ -o async_io async_io.cpp -lrt
./async_io
4. 参考文档
一顿饭的事儿,搞懂了Linux5种IO模型