一、什么是IO
比如调用 recv(),其实是两个阶段:
-
等待数据到达内核(数据准备阶段)
-
将内核的数据拷贝到用户空间(数据复制阶段)
这两个阶段的等待和谁来“干活”,决定了是哪种 I/O 模型。
IO=等+拷贝
同步IO和异步IO
同步IO
同步IO是指在执行IO操作时,调用方(程序或线程)必须等待IO操作完成才能继续执行后续代码。
程序在发起IO请求后会被阻塞,直到数据返回或操作完成。
特点
阻塞:调用IO操作的线程暂停,等待操作完成(如等待网络响应、磁盘读取)。
顺序执行:代码按顺序执行,IO完成后才执行下一步。
简单性:编程模型直观,代码易于理解,但效率可能较低(尤其在高并发场景)。
异步IO
异步IO是指在执行IO操作时,调用方发起请求后不等待操作完成,而是立即返回,继续执行后续代码。IO操作完成后,通过回调、轮询或事件通知告知程序结果。
程序可以同时处理多个IO任务,适合高并发场景。
特点
非阻塞:调用IO操作后,线程立即返回,不等待结果。
并发性:允许程序在等待IO时处理其他任务,提高效率。
复杂性:需要回调函数、事件循环或协程(如 async/await)管理异步任务,代码逻辑更复杂。

二、五种IO模型
举一个例子,有五个人钓鱼。
张三:张三专注盯着鱼漂,鱼漂不动,张三就一直等着,直到鱼上钩才拉杆
李四:李四不时看一眼鱼漂,鱼漂没动就去做其他事(如喝茶),稍后再检查。
王五:赵六给鱼漂装上铃铛,鱼上钩时铃铛响(信号),赵六收到通知去拉杆。
赵六:王五不盯着鱼漂,而是交给一个“鱼漂监控器”(如 select、epoll),鱼上钩时监控器通知王五去拉杆。相当于搞了许多鱼竿。
田七:田七发起钓鱼任务,交给小吴去钓,鱼上钩后小吴直接把鱼交给田七,田七无需关心过程。
张三就相当于阻塞IO,李四相当于非阻塞IO,王五相当于信号驱动IO,赵六相当于多路复用IO,田七相当于异步IO。
阻塞IO

最传统的模型,调用recv()会一直卡住等待,直到数据从内核拷贝到用户空间完成。
用户线程被阻塞,啥也不能干。编程简单,
性能极差,尤其在连接数多的时候。
非阻塞IO

recv()不会阻塞,如果没有数据就立即返回RAGAIN,所以程序必须不停的轮询检查数据有没有准备好(自己反复调用)
用户线程不会阻塞,需要自己写轮询循环,效率低下。
太“积极”,cpu白忙活
信号驱动IO

程序为文件描述符注册信号处理函数,当 IO 事件就绪时,内核发送信号(如 SIGIO),程序通过信号处理函数处理。
特点:无需轮询,但信号处理复杂,实际使用较少。
多路转接/多路复用

select/poll
使用select/poll等系统调用统一监听多个文件描述符,只有当某个fd准备好才通知你,然后你再recv()。
可以监听多个socket,单线程处理多连接。
select/poll每次都要遍历所有的fd,效率低下。
epoll(linux特有的io多路转接)
epoll是select/poll的进化版,使用事件通知+内核数据结构(红黑树、就绪链表)提升性能。
重要有事件就告诉你,不用遍历所有fd。
高效,适合大量并发连接。
异步IO

由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据。
三、基于fcntl,实现一个SetNoBlock函数,将fd设置为非阻塞
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ )
fcntl 函数有 5 种功能
- 复制一个现有的描述符(cmd=F_DUPFD) .
- 获得/设置文件描述符标记(cmd=F_GETFD 或 F_SETFD).
- 获得/设置文件状态标记(cmd=F_GETFL 或 F_SETFL).
- 获得/设置异步 I/O 所有权(cmd=F_GETOWN 或 F_SETOWN).
- 获得/设置记录锁(cmd=F_GETLK,F_SETLK 或 F_SETLKW)
我们此处只是用第三种功能, 获取/设置文件状态标记, 就可以将一个文件描述符设置为非阻塞
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <fcntl.h>
// 0 标准输入,默认就是阻塞的
void SetNonBlock(int fd)
{
int fl = fcntl(fd, F_GETFL);
if (fl < 0)
{
perror("fcntl");
return;
}
fcntl(fd, F_SETFL, fl | O_NONBLOCK); // O_NONBLOCK:将fd设置为非阻塞
}
int main()
{
SetNonBlock(0);
char buffer[1024];
while (true)
{
// Linux中ctrl + d:标识输入结束,read返回值是0,类似读到文件结尾
ssize_t n = read(0, buffer, sizeof(buffer));
if (n > 0)
{
buffer[n - 1] = 0;
std::cout << buffer << std::endl;
}
else if (n < 0) // 非阻塞read,如果底层数据没有准备好,数据读取算不算出错?不算
{
// 1. 读取出错 2. 底层没有数据准备好
if(errno == EAGAIN || errno == EWOULDBLOCK) // 错误码
{
std::cout << "数据没有准备好..." << std::endl;
sleep(1);
// 做你的事情
continue;
}
else if(errno == EINTR)
{
continue;
}
else
{
// 真正的read出错了
}
}
else
{
break;
}
//
// sleep(1);
// std::cout << ".: " << n << std::endl; // C++也有语言级输出缓冲区
}
}
代码是一个使用非阻塞 IO 的示例,通过 fcntl 将标准输入(文件描述符 0)设置为非阻塞模式,并循环读取用户输入。
1104





