文章目录
- 本篇摘要
- 一· 五种IO模型介绍:
-
- **1. 阻塞IO(Blocking IO, BIO)**
- **2. 非阻塞IO(Non-blocking IO, NIO)**
- **3. IO多路复用(Multiplexed IO)**
- **4. 信号驱动IO(Signal Driven IO, SIGIO)**
- **5. 异步IO(Asynchronous IO, AIO)**
- `相关概念认识`
-
- 同步通信 vs 异步通信
- 阻塞 vs 非阻塞
- 对比
- **一句话总结**
- 其他高级IO拓展(了解)
-
- 小结:
- 二.非阻塞IO之fcntl
-
- `fcntl` 函数基本信息
- `fcntl` 函数的 5 种主要功能
- **通过 `fcntl` 设置非阻塞模式**
- `基于fcntl函数简单测试`
-
- 测试思路
- 效果展示
- 源码展示
- 三. 多路转接之select
-
- `select相关函数介绍`
- select代码编写测试(基于tcp实现echo回显)
-
- 设计思路
- 测试效果
- 源码汇总
- **select缺点**
- 四.多路转接之poll
-
- poll相关函数介绍
-
- **函数原型**
- **核心参数**
- **返回值**
- **核心事件宏(`events`/`revents`)**
- **使用步骤**
- 操作流程:
- poll代码编写测试(基于tcp实现echo回显)
-
- 设计思路
- 测试效果
- 源码汇总
- **`特点与适用场景`**
-
- poll的优点
- poll的缺点
- <font color="#DC3545" size="5" face="楷体"> **典型场景(相比epoll落后)**
- **与 `select` 的对比**
- 五.本篇小结

本篇摘要
- 本篇将介绍五种IO模型,以及对其中多路转接IO的select,poll进行介绍,使用,总结等。
一· 五种IO模型介绍:
1. 阻塞IO(Blocking IO, BIO)

-
核心特点:应用程序发起IO操作(如读/写)后,线程会被“卡住”,直到IO完成(数据到达/发送完毕)。
-
工作流程:

-
总结:简单但低效,适合单连接、低并发场景(如早期客户端程序),所有套接字默认都是阻塞IO。
2. 非阻塞IO(Non-blocking IO, NIO)

-
核心特点:应用程序发起IO操作后,函数立即返回(不会阻塞线程),但需主动轮询检查数据是否就绪,非阻塞IO:如果内核还未将数据准备好,系统调用仍然会直接返回,并且返叵EWOULDBLOCK错误码。
-
工作流程:

-
总结:避免线程阻塞,但需频繁轮询(浪费CPU),适合短连接或对延迟敏感的场景。
3. IO多路复用(Multiplexed IO)

-
核心特点:通过一个系统调用监控多个文件描述符(FD),当任意一个FD就绪(数据可读/写)时,内核通知应用程序处理。
-
代表实现:select(老)、poll(改进)、epoll(高性能,Linux专属)。
-
工作流程:

-
总结:用“单线程管多个连接”,大幅提升并发能力(如Nginx、Redis用epoll)。
4. 信号驱动IO(Signal Driven IO, SIGIO)

-
核心特点:应用程序通过信号(Signal) 通知内核“监控某个FD”,当数据就绪时,内核发送信号触发应用程序处理。
-
工作流程:

-
总结:异步通知,但信号可能丢失或延迟,实际应用较少(仅适合特定场景)。
5. 异步IO(Asynchronous IO, AIO)

-
核心特点:应用程序发起IO操作后,内核全程完成数据拷贝,完成后主动通知应用程序(无需线程干预)。
-
工作流程:

-
总结:完全异步,“发起即忘”,效率最高(适合高并发、大文件传输等场景,如Windows的IOCP)。
因此对应信号驱动IO以及异步IO区别:信号驱动是数据准备好了,告诉用户自己调用接口从内核态拷贝到用户态,而异步是直接让内核拿到数据后拷贝到用户态完成才进行通知用户。
相关概念认识
同步通信 vs 异步通信
核心区别:
- 同步:调用者自己发起IO操作,全程等待结果返回(自己干完才停)。
- 异步:调用者委托他人执行IO操作,完成后收到通知(交给别人干,自己等通知)。
本质差异:
- 同步:IO操作由调用方“亲自执行”,需阻塞等待。
- 异步:IO操作由其他组件执行,调用方仅接收结果通知。
阻塞 vs 非阻塞
关注点:
- 程序在“等待调用结果时的状态”。
区别:
- 阻塞:等待期间,当前线程被“挂起”(卡住不动),直到结果返回才能继续。
- 非阻塞:等待期间,线程不被“卡住”,可继续执行其他任务(不干等)。
对比
- 同步/异步:描述“IO操作由谁执行”(自己干 vs 别人干)。
- 阻塞/非阻塞:描述“等待时线程是否被挂起”(卡住 vs 不卡住)。
- 二者独立:同步操作可能阻塞或非阻塞(如同步非阻塞需轮询),异步操作通常搭配非阻塞(避免卡死)。
一句话总结
从“干等”(阻塞)到“轮询”(非阻塞),再到“监控多个”(多路复用),最后到“内核代劳”(异步),IO模型逐步解决“线程效率”和“并发能力”的矛盾。
其他高级IO拓展(了解)
非阻塞 IO:
- 调用时不阻塞线程,立即返回结果(可能未完成),需主动轮询或配合其他机制(如 I/O 多路复用)获取最终结果。
记录锁(文件锁):
- 对文件/数据加锁,确保多进程/线程并发访问时的数据一致性(如防止同时写入)。
系统 V 流机制(STREAMS):
- UNIX 系统中的一种设备驱动模型,支持数据的异步处理和模块化组件(现代应用较少,可简单理解为“流式数据处理框架”)。
I/O 多路转接(I/O Multiplexing):
- 通过 select/poll/epoll 等机制,单个线程监控多个文件描述符的 IO 状态,有事件时才触发处理,避免轮询浪费资源(常用于高并发服务端)。
readv/writev 函数(分散/聚集 I/O):
- 允许一次系统调用读写多个内存缓冲区(分散写入、聚集读取),减少多次拷贝开销,提高大块数据传输效率。
存储映射 IO(mmap):
- 将文件直接映射到进程内存空间,通过内存操作访问文件(避免频繁读写系统调用),适合大文件随机访问和进程间共享内存。
小结:
这些高级 IO 主要解决 效率、并发控制、资源利用 问题,常结合使用(如 epoll + 非阻塞 IO + mmap),适用于高并发、高性能场景。
二.非阻塞IO之fcntl
fcntl 函数基本信息
fcntl 函数的 5 种主要功能
通过 fcntl 设置非阻塞模式
- 使用 F_GETFL 获取当前文件状态标志(返回一个位图)。
- 通过按位或运算(|)添加 O_NONBLOCK 标志。
- 使用 F_SETFL 将修改后的标志写回文件描述符。
int flags = fcntl(fd, F_GETFL, 0); // 获取当前标志
flags |= O_NONBLOCK; // 添加非阻塞标志
fcntl(fd, F_SETFL, flags); // 设置回文件描述符
}
基于fcntl函数简单测试
测试思路
- 将标准输入(键盘)设置为非阻塞模式,通过无限循环持续尝试读取输入:有数据时输出内容;无数据时因EAGAIN/EWOULDBLOCK提示“没有数据”并休眠1秒;若被信号中断(EINTR)则提示后继续循环;其他错误则提示错误,最终实现非阻塞读取标准输入的功能。
效果展示

- 标准输入流没输入就打印没有数据,否则打印输入的数据。
源码展示
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
using std::cout;
using std::endl;
void set_nonblock(int fd)
{
int flags = fcntl(fd, F_GETFL, 0);
flags |= O_NONBLOCK;
fcntl(fd, F_SETFL, flags);
}
int main()
{
set_nonblock(0);
while (1)
{
char buf[1024];
int n = read(0, buf, sizeof(buf));
if (n > 0)
{
buf[n – 1] = 0;
cout << buf << endl;
}
else if (n < 0)
{ //无论是读取失败还对方的接收缓冲区没有数据最后都是n==-1因此三种情况区分!
cout<<n<<endl;
if (errno == EINTR)
{
cout << "被打断了" << endl;
continue;
}
else if (errno == EAGAIN || errno == EWOULDBLOCK)
{ sleep(1);
cout << "没有数据" << endl;
//虽然IO效率没有提高,可以在等待的时间做自己想做的事情!
continue;
}
else
{
cout << "出错了" << endl;
continue;
}
}
}
return 0;
}
三. 多路转接之select
select相关函数介绍
- nfds:需监视的最大文件描述符值+1。
- readfds/writefds/exceptfds:分别对应可读、可写、异常文件描述符的集合。
- timeout:设置等待时间的结构体(struct timeval)。
- 集合以位图形式存储,从右到左第几位代表对应fd值,用户通过以下函数操作:void FD_CLR(int fd, fd_set *set); // 清除fd在集合中
int FD_ISSET(int fd, fd_set *set); // 检查fd是否在集合中
void FD_SET(int fd, fd_set *set); // 将fd添加到集合中
void FD_ZERO(fd_set *set); // 清空集合 - 若不关心某类事件,对应参数可传nullptr。

- 结构体含tv_sec(秒)和tv_usec(微秒)字段。
- 若为NULL:阻塞等待;若字段全为0:非阻塞(立即返回)。
- 成功:返回状态改变的文件描述符数量。
- 返回0:超时时间内无描述符状态改变。
- 返回-1:发生错误,错误原因存于errno,涉及错误码如:
- EBADF:无效文件描述符或文件已关闭。
- EINTR:调用被信号中断。
- EINVAL:参数n为负值。
- ENOMEM:内核内存不足。
- 参数 readfds、writefds、exceptfds和 timeout的值可能变为不可预测状态,后续代码不应依赖这些值。
使用流程:

select代码编写测试(基于tcp实现echo回显)
设计思路
- 拿着select进行文件描述符的就绪监测(包括istenfd以及给用户开的普通的fd);当select等待到事件就绪就进行为新的连接开辟fd或者对普通fd的读写操作等;这里只检测读事件默认写都是就络的;但是select会更改输入参数,导致每次都要动态维护需要监测的fd,然后对d集合进行调整;因此这里采取一个数组来动态维护再进行fd集合的填充(新连接来或者有客户退出进行d数组的更新)。
测试效果

- 成功连接,为新的连接开辟新的fd。

- 普通的fd进行的读事件就绪,进行正常读取。

- 客户端退出,对应fd的读事件就绪(里面被写入了类似文件结尾的标志,读取后直接关闭这个fd,移除监测集合中对应的位置)。
源码汇总
点我速看源码
select缺点
- 每次调用需手动设置fd集合,使用不便;
- 需将fd集合从用户态拷贝到内核态,fd数量多时开销大;
- 内核需遍历所有传入的fd,数量多时效率低;
- 支持的文件描述符数量上限较低(依赖系统实现)。
四.多路转接之poll
poll解决了select的部分问题,主要体现在两方面:
poll相关函数介绍
poll 是 Linux 系统中用于 I/O 多路复用的系统调用,用于监控多个文件描述符(FD)的状态变化,避免单线程阻塞等待 I/O。以下是其核心用法总结:
函数原型
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
核心参数
如下:
#include <poll.h>
struct pollfd {
int fd;
short events;
short revents;
};
- 监控的文件描述符列表,每个元素包含三部分信息:
- fd:要监控的文件描述符(如套接字、文件等)。
- events:感兴趣的事件(通过宏定义指定,如 POLLIN、POLLOUT 等)。
- revents:实际发生的事件(由内核填充,调用后检查该字段判断状态)。
nfds_t nfds
- fds 数组的长度(即监控的文件描述符数量)。
int timeout
- 超时时间(单位:毫秒):
- -1:永久阻塞,直到有事件发生或出错。
- 0:立即返回(非阻塞轮询)。
- >0:最多等待指定毫秒数,超时返回。
返回值
- < 0:发生错误(需检查 errno)。
- = 0:超时(无任何事件发生)。
- > 0:就绪的文件描述符数量(fds 中 revents 非零的元素个数)。
核心事件宏(events/revents)

使用步骤
初始化 pollfd 数组: 为每个需要监控的文件描述符创建 struct pollfd 结构体,设置 fd 和 events。
struct pollfd fds[2];
fds[0].fd = stdin_fd; // 标准输入
fds[0].events = POLLIN; // 监控可读事件
fds[1].fd = socket_fd; // 套接字
fds[1].events = POLLIN | POLLOUT; // 监控可读和可写事件
调用 poll 函数:
int ret = poll(fds, 2, 5000); // 监控 2 个 FD,最多等 5 秒
处理返回结果:
- 若 ret > 0,遍历 fds 数组,检查 revents 判断哪些 FD 就绪:for (int i = 0; i < nfds; i++) {
if (fds[i].revents & POLLIN) {
// 处理可读事件
}
if (fds[i].revents & POLLHUP) {
// 处理连接断开
}
} - 若 ret == 0:超时处理。
- 若 ret < 0:错误处理(如 EINTR 表示被信号中断)。
操作流程:

poll代码编写测试(基于tcp实现echo回显)
设计思路
这里只需要把对应的之前用数组维护的fd集合改成维护这个struct pollfd结构体数组,对结构体里面的变量进行初始化,更新,判断等操作即可。
测试效果
同上面没啥区别。
源码汇总
点我速看源码
特点与适用场景
poll的优点
poll的缺点
典型场景(相比epoll落后)
- 网络服务器(监控多个客户端连接)。
- 同时处理文件和网络 I/O 的程序。
与 select 的对比
- poll 使用数组存储 FD,无数量上限(取决于内存),而 select 使用 fd_set(通常限制为 1024)。
- poll 的事件模型更清晰(通过位掩码区分事件),select 需重置 fd_set。
通过合理使用 poll,可以实现高效的 I/O 多路复用,避免单线程阻塞导致的性能瓶颈。
五.本篇小结
- 本篇带大家简略认识五种常见的IO模型以及多路转接复用的select及poll的接口的使用,也看到了有点与缺点,基于poll略胜一筹,但是还是比不上epoll,因此,下篇介绍epoll独特之处。






