网络:11.五种IO模型与非阻塞IO
五种IO模型与非阻塞IO
核心思想:单位时间内,减少10中,等待的比重!
一.引入
人=进程;鱼=数据;钓鱼=IO,鱼竿=文件描述符,河=操作系统;桶=用户缓冲区
- 张三:专注钓鱼,鱼漂不动,张三不动 – 阻塞IO
- 李四:不会因为鱼没有上钩而卡在检测鱼漂上 – 非阻塞IO
- 王五:通过给鱼竿挂上铃铛,让鱼上钩反向通知我 – 信号驱动IO
- 赵六:拉了一卡车的鱼竿(100只鱼竿同时钓)-- 多路复用,多路转接
- 田七:我是喜欢吃鱼!发起钓鱼,小王钓鱼了 – 异步IO
问题:
-
阻塞VS非阻塞:会因为IO条件不具备,阻塞会卡住,直到条件就绪,非阻塞,检测到IO条件不具备,出错返回。
不同:等待方式不同!阻塞和非阻塞对一件IO任务的效率是一样的,鱼咬没咬钩跟看不看没关系,只是非阻塞在等的时候做了更多的其他事情,这些事情可能包含其他IO任务,所以说非阻塞IO效率高!
-
谁的钓鱼效率最高?
赵六:单位时间内,鱼咬钩的概率高,等的比重,就会很低!
-
王五有没有等待呢?
没有,但是王五只是不需要检测,也参与钓鱼中钓的过程!
-
结论:阻塞,非阻塞,信号驱动,多路复用—同步IO
只要有人参与了IO不管是[等,拷贝],就是同步的! -
同步IO vs 异步IO
IO=等+拷贝,凡是参与IO等或者拷贝任意一个或多个阶段—同步IO;
否则,发起IO或者IO工作流和你的工作流无关—异步IO。 -
IO同步≠线程同步
两者没有任何关系,IO同步是是否参与的问题;线程同步是先后顺序问题。
二.五种IO模型
1. 阻塞IO
在内核将数据准备好之前, 系统调用会一直等待. 所有的套接字, 默认都是阻塞方式.
阻塞IO是最常见的IO模型.
2. 非阻塞IO
如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK错误码.
非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符, 这个过程称为轮询. 这对CPU来说是较大的浪费, 一般只有特定场景下才使用.
3. 信号驱动IO
内核将数据准备好的时候, 使用SIGIO信号通知应用程序进行IO操作.
4. IO多路转接
虽然从流程图上看起来和阻塞IO类似. 实际上最核心在于IO多路转接能够同时等待多个文件描述符的就绪状态.
5. 异步IO
由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据).
6. 小结
任何IO过程中, 都包含两个步骤. 第一是等待, 第二是拷贝. 而且在实际的应用场景中, 等待消耗的时间往往都远远高于拷贝的时间. 让IO更高效, 最核心的办法就是让等待的时间尽量少.
三.高级IO重要概念
在这里, 我们要强调几个概念
1. 同步通信 vs 异步通信
(synchronous communication/ asynchronouscommunication)
同步和异步关注的是消息通信机制.
- 所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回. 但是一旦调用返回,就得到返回值了; 换句话说,就是由调用者主动等待这个调用的结果;
- 异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果; 换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果; 而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用.
另外, 我们回忆在讲多进程多线程的时候, 也提到同步和互斥. 这里的同步通信和进程之间的同步是完全不相干的概念.
- 进程/线程同步也是进程/线程之间直接的制约关系
- 是为完成某种任务而建立的两个或多个线程,这个线程需要在某些位置上协调他们的工作次序而等待、传递信息所产生的制约关系. 尤其是在访问临界资源的时候.
以后在看到 “同步” 这个词, 一定要先搞清楚大背景是什么. 这个同步, 是同步通信异步通信的同步, 还是同步与互斥的同步.
2. 阻塞 vs 非阻塞
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.
- 阻塞调用是指调用结果返回之前,当前线程会被挂起. 调用线程只有在得到结果之后才会返回.
- 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程.
3. 理解这四者的关系
[妖怪蒸唐僧的例子]
4. 其他高级IO
非阻塞IO,纪录锁,系统V流机制,I/O多路转接(也叫I/O多路复用),readv和writev函数以及存储映射IO(mmap),这些统称为高级IO.
我们此处重点讨论的是I/O多路转接
四.非阻塞IO
1. fcntl
一个文件描述符, 默认都是阻塞IO.
函数原型如下.
fcntl 的作用是对文件描述符进行各种控制操作(如复制fd、设置非阻塞、修改文件状态标志等)
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
传入的cmd的值不同, 后面追加的参数也不相同.
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).
我们此处只是用第三种功能, 获取/设置文件状态标记, 就可以将一个文件描述符设置为非阻塞.
参数:
fd:文件描述符。cmd:控制命令,常见的有:F_GETFL:获取文件状态标志F_SETFL:设置文件状态标志(如设置O_NONBLOCK非阻塞)
arg(可选参数):根据cmd不同含义不同,例如:F_SETFL时传入新的 flag(如O_NONBLOCK)
返回值:
-
成功:返回根据cmd不同返回不同值
-
-
cmd为F_GETFL时
返回一个 int 类型的“位图”,里面包含当前 fd 的状态标志,比如:
O_RDONLY,O_WRONLY,O_RDWR,O_APPEND,O_NONBLOCK -
cmd为F_GETFL时
返回
0
-
-
-
失败:返回
-1并设置errno
2.使用fcntl设置非阻塞
基于fcntl, 我们实现一个SetNoBlock函数, 将文件描述符设置为非阻塞.
void SetNoBlock(int fd) {
int fl = fcntl(fd, F_GETFL);
if (fl < 0) {
perror("fcntl");
return;
}
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}
- 使用F_GETFL将当前的文件描述符的属性取出来(这是一个位图).
- 然后再使用F_SETFL将文件描述符设置回去. 设置回去的同时, 加上一个O_NONBLOCK参数.
注意:
fcntl(fd, F_SETFL, O_NONBLOCK); // ❌ 错误写法
这样会覆盖原来的 flag
3. 轮询方式读取标准输入
read返回值是-1,并设置errno为 EAGAIN 或 EWOULDBLOCK时,代表现在还没读到数据,不是数据出错了。
read返回值是-1,并设置errno为EINTR时,代表读的过程中被信号中断,没读完。
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
// θ标准输入,默认就是阻塞的
void SetNonBlock(int fd)
{
int fl = fcntl(fd, F_GETFL);
if(fl<0)
{
perror("fcntl");
}
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}
int main()
{
SetNonBlock(0);
char buffer[1024];
while (true)
{
// Linux中ctrl+d:标识输入结束,read返回值是o,类似读到文件结尾
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);
}
else if (errno == EINTR) //read被信号中断
{
continue;
}
else
{
// read出错
exit(-1);
}
}
else
{
break;
}
}
return 0;
}
结果:
数据还没准备好...
abc <-手动输入的
abc <-程序返回的
数据还没准备好...
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐








所有评论(0)