目录
- [1 ~> 五种 I/O 模型](#1 ~> 五种 I/O 模型)
-
- [1.1 I/O](#1.1 I/O)
- [1.1 I/O 模型](#1.1 I/O 模型)
- [1.2 阻塞 vs 非阻塞](#1.2 阻塞 vs 非阻塞)
- [1.3 同步I/O vs 异步I/O](#1.3 同步I/O vs 异步I/O)
- [2 ~> 非阻塞I/O](#2 ~> 非阻塞I/O)
-
- [2.1 fcntl](#2.1 fcntl)
- [2.2 非阻塞代码](#2.2 非阻塞代码)
1 ~> 五种 I/O 模型
1.1 I/O
I/O(input / output,输入/输出) 是指计算机核心处理系统(CPU/内存)与外部设备,外设,存储驱动器或网络接口之间的数据传输与通信过程。
IO 的效率其实并不高,当我们调用recv(),只有当接收缓冲区中有数据时recv才能够返回,不然就会阻塞等待数据就绪。
所以,I/O = 等 + 拷贝。为什么效率不高,就是因为要等,而拷贝的时间消耗相比于等待所占的比重就少很多。
所以,高效IO 就是围绕如何在单位时间减少等待的比重。
1.1 I/O 模型
(1)阻塞 I/O
在系统调用完成读取或写入之前,应用程序(进程/线程)会一直阻塞,暂停执行。
(2)非阻塞 I/O
如果底层数据没有准备好,系统调用就会立即返回,需要线程定期轮询检查状态。
(3)信号驱动 I/O
当底层数据准备就绪后,内核会主动向应用程序发送信号(如SIGIO)进行通知。
(4)I/O 多路转接
通过select、poll、epoll接口让单个进程/线程同时监控多个文件描述符,当任意文件描述符就绪时就触发响应,然后应用程序进行I/O。
(5)异步 I/O
内核自动完成等待数据就绪以及将数据拷贝到用户空间内存的全过程,仅在全部操作完成后通知应用程序。
下面我们通过一个钓鱼的例子来加深对5种I/O模型的理解:
- 阻塞I/O:张三在钓鱼时什么也不干,一直盯着鱼鳔看,一旦有鱼上钩了就开始往上钓。
- 非阻塞I/O:李四在钓鱼时,抛竿后就开始刷抖音,然后时不时抬头看一个鱼竿的动静,没鱼上钩就继续刷抖音,有鱼上钩就往上钓。
- 信号驱动I/O:王五在钓鱼时,在鱼竿上面系一个铃铛,然后就也去刷抖音了,但是王五只有在铃铛响之后才会开始往上钓鱼。
- I/O多路转接:赵六钓鱼时直接拉来了一车鱼竿,全部抛竿后,然后过来过去查看鱼竿状态,发现哪个有鱼上钩就开始往上钓。
- 异步I/O:田七只喜欢吃鱼,于是对秘书说:"你在这钓鱼,我现在去开会,钓到之后你打电话叫我,我过来接你。"
1.2 阻塞 vs 非阻塞
(1)非阻塞I/O的效率是不是比阻塞I/O的效率高?
如果单纯就I/O 的效率来讲,阻塞与非阻塞效率是相同的,因为 I/O = 等 + 拷贝,两者只是等待的方式不同。但非阻塞I/O 在等待的时间里做了更多事,所以,非阻塞对于时间的利用率更高。
(2)那么哪一种方式的效率更高呢?
谁在 I/O 过程中等待的时间最短,那么谁的效率就最高。对于赵六而言,他一次性监控了大量的鱼竿,钓完一杆(I/O),很快就发现又有鱼上钩了(数据就绪)。
这个过程中,赵六没怎么等,几乎一直在上钩。所以,多路转接 I/O 的效率是最高的。
1.3 同步I/O vs 异步I/O
阻塞、非阻塞、信号驱动和多路转接 I/O 都属于同步 I/O,对于同步 I/O 而言,可以简单理解为应用进程或线程一定参与了等待或拷贝中的一个环节,或者都参与了。
异步I/O 一定没有参与等待和拷贝任意一个环节。
2 ~> 非阻塞I/O
2.1 fcntl
c
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, .../* arg */);
- fd:即要控制的文件描述符。
- cmd:代表你要进行什么操作。
- 第三个参数:根据
cmd决定传指针或整数。 - 返回值:成功由
cmd决定;失败返回-1,错误码被设置。
cmd参数:
F_GETFL获取文件状态标志
c
int flags = fcntl(fd, F_GETFL);
if(flags == -1) { perror("fcntl"); }
F_SETFL设置文件状态标志
c
// 设置文件描述符为非阻塞:
// 1. 获取原有标志
int flags = fcntl(fd, F_GETFL);
// 2. 添加 O_NONBLOCK 非阻塞,不能直接赋值,要保留原有flag
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
F_DUPFD复制文件描述符
c
int newfd = fcntl(oldfd, F_DUPFD, low);
类似 dup()。
复制 fd,返回一个大于等于 minfd 的新文件描述符。
F_SETOWN设置拥有者(信号驱动 IO 必备)
c
// socket就绪时,向这个进程PID发送SIGIO信号
fcntl(sockfd, F_SETOWN, getpid());
今天我们只用到获取和设置文件描述符状态的功能!
2.2 非阻塞代码
当我们正常从标准输入获取数据,默认std::cin是阻塞的(即 0 号文件描述符阻塞)。

c++
// 将指定文件描述符设置为非阻塞
void SetNoblock(int fd)
{
int fl = fcntl(fd, F_GETFL);
if(fl < 0)
{
std::cout << "出错" << std::endl;
return;
}
// 保持原状态不变,并将将标准输入设置为非阻塞
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}
cpp
// 非阻塞IO
void test2()
{
SetNoblock(0); // 将标准输入设置为非阻塞
char buff[1024];
while(true)
{
int n = read(0, buff, sizeof(buff));
if(n > 0)
{
buff[n-1] = 0;
std::cout << buff << std::endl;
}
else if(n < 0)
{
if(errno == EINTR)
{
std::cout << "信号中断" << std::endl;
continue;
}
else if(errno == EAGAIN | EWOULDBLOCK)
{
std::cout << "底层数据没有准备好(未就绪)" << std::endl;
sleep(2);
continue;
}
else
{
std::cout << "出错" << std::endl;
break;
}
}
else break; // 读到文件末尾
}
}
文件描述符在非阻塞状态下,如果再调用读或者写之类的接口,就需要对返回值为 -1 这种情况进行更加细致的判断:
errno == EAGAIN||errno == EWOULDBLOCK:表示底层数据没有准备好,下次就绪后再来读。errno = EINTR:表示信号中断,重试即可。- 其他:表示真正出错了。

此时,如果底层没有数据就绪,就会直接返回。