五种IO模型总结
文章目录
1.提高IO效率的本质
当我们了解了传输层TCP协议的数据缓冲区之后,对用户层的IO就有了一个新的理解,其实用户层调用send等函数发送数据的过程其实是向发送缓冲区中写数据的过程,而调用recvform的过程其实就是向接收缓冲区中读取的过程。而真正什么时候发送数据以及怎么发数据其实是由传输层决定的,即由OS来决定的。

因此我们发现我们所说的IO其实就是缓冲区的读写,而缓冲区的读写的过程分为两个内容:等待+拷贝数据。当缓冲区数据就绪的时候,应用层就可以从缓冲区中拷贝数据或者向缓冲区中发送数据了;当缓冲区没有就绪的时候就需要应用层来进行等待了。
因此我们提高IO的效率其实就是研究两个部分即:改变等待的方式与减少等待的比重。
2.五种IO模型
我们将IO模型与钓鱼的例子结合起来,方便理解。
2.1阻塞等待
第一个人去钓鱼,全程盯着鱼鳔看,一旦鱼鳔下沉则把鱼捞出。这种情况称为阻塞等待,即就绪条件不满足,就一直等待。

此时recv发挥了两个作用:
第一当数据报没有准备好的时候进行等待;
第二当数据报准备好之后进行拷贝。
2.2非阻塞等待
第二个人去钓鱼,隔一段时间看一次鱼鳔,当某一次看到鱼鳔沉下去的时候再来鱼竿。这种情况称为非阻塞等待,在等待期间可以去做其他的事情。

在这里经过不断调用recvfrom来查询状态,当状态没有就绪的时候应用层还可以做其他的事情。
2.3信号驱动
第三个人去钓鱼,他改造了鱼竿,当鱼鳔下沉的时候鱼竿上的铃铛会响,然后这个人就会抬起鱼竿。即不会主动对就绪状态进行检测,而是当就绪之后由内核来通知,这一IO过程称为信号驱动。

这里的recvfrom只做一项工作,即读缓冲区,在数据报没有准备好之前,用户和内核之间是异步状态。我们也可以查看一下这个SIGIO的信号,是29号信号。可以使用自定义捕捉来完成一些需求。

2.4多路转接
有一个很享受钓鱼过程的富豪来钓鱼,他在河边放了100个鱼竿,并告诉他的佣人定时轮询这100个鱼竿,一旦发现哪个鱼竿的鱼鳔下降了,立刻通知我,然后富豪来抬起那个鱼竿。这种方式是效率最高的方式。我们可以站在鱼的角度,100个鱼竿上钩的概率大大增加。我们称这种方式为多路转接。

再强调一遍,IO的过程其实就分为两个部分,一个是等的部分,一个是拷贝的部分。在多路转接这里,select的作用相当于佣人就是在等。然后告诉recvfrom哪个好了,recvfrom的作用就只有拷贝。
2.5异步等待
此时又有一个富豪,他有一个佣人,他让他的佣人去钓鱼,钓鱼的方式他并不关心,他给了佣人钓鱼工具,一个桶和一个电话。当佣人将鱼装满桶之后给他打电话,富豪就离开了。富豪全过程都没有进行参与。这种方式称为异步IO。

当内核中拷贝完成之后,直接递交在aio_read中的指定信号中,用户对数据进行处理即可。
2.6补充概念
2.6.1同步与异步IO
前四种情况,钓鱼者参与了等或者钓鱼的过程,称为同步通信。最后一种情况,钓鱼者没有参与钓鱼的过程称为异步通信。
所谓同步,就是在发出一个调用的时候,在没有得到结果之前,该调用就不返回,但是一旦调用返回,就得到返回值了;换句话说,就是由调用者主动等待这个调用的结果。
异步则是相反的,调用发出之后,这个调用就直接返回了,所以没有返回结果;换句话说,当一个异步过程调用发出之后,调用者不会立刻得到结果;而是在调用发生后被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。
2.6.2阻塞和非阻塞
阻塞和非阻塞关注的是程序在等待结果时候的状态。
阻塞调用:指调用结果返回之前,当前进程会被挂起,调用线程只有在得到结果之后才会返回。
非阻塞调用:指在不能立刻得到结果之前,该调用不会阻塞当前进程。
2.6.3事件就绪
事件就绪指的是缓冲区中的数据达到一定的阈值,此时上层就可以进行读写缓冲区了。不过决定权在上层,上层可以选择不读,不读的话一定不是一个好的代码。
3.实现非阻塞IO
3.1fcntl函数介绍
大部分情况下都是阻塞IO,比如read函数就是一个阻塞IO,我们可以通过fcntl函数来将阻塞IO变成非阻塞IO。

当成功返回大于0的数(表示某一种含义),失败返回-1。第一个参数代表文件描述符,第二个参数代表功能,cmd不同,该函数的功能也不同,最后一个参数是可变参数扩展,用于指定具体的功能。
F_DUPFD:复制一个现有的描述符。
F_GETFD或F_SETFD:获得/设置文件描述符标记。
F_GETFG或F_SETFL:获得/设置文件状态标记。
F_GETOWN或F_SETOWN:获得/设置异步IO的所有权。
F_GETLK,F_SETLK或F_SETLKW:获得/设置记录锁。
我们这里只设置第三个功能,获取/设置文件状态标记,就可以将一个文件描述符设置为非阻塞。同时需要将可变参数扩展在原来状态基础上或上O_NONBLOCK注意fcntl是设置某一个文件的状态而不是某一个函数。设置之后关于这个文件的所有IO都要变为非阻塞状态。
3.2设置为非阻塞状态
3.2.1阻塞等待代码
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<errno.h>
int main()
{
while(1)
{
char buffer[1024];
ssize_t s=read(0,buffer,sizeof(buffer)-1);
if(s>0)
{
buffer[s]=0;
write(1,buffer,strlen(buffer));
printf("read success,s:%d,errno:%d",s,errno);
}
}
}
这就是一个简单的从键盘读,再向显示器打印的程序(Linux一切皆文件,所以也是一个IO的过程),如果缓冲区没有数据(键盘没有输入),那么就不会读取。即需要手动使用\n来刷新缓冲区。
3.2.2改写成非阻塞等待代码
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<errno.h>
#include<fcntl.h>
void SetNonBlock(int fd)
{
int f1=fcntl(fd,F_GETFL);
if(f1<0)
{
perror("fcntl");
return;
}
fcntl(fd,F_SETFL,f1|O_NONBLOCK);
}
int main()
{
errno=0;
SetNonBlock(0);
while(1)
{
sleep(1);
char buffer[1024];
ssize_t s=read(0,buffer,sizeof(buffer)-1);
if(s>0)
{
buffer[s]=0;
write(1,buffer,strlen(buffer));
printf("read success,s:%d,errno:%d\n",s,errno);
}
else
{
if(errno==EAGAIN||errno==EWOULDBLOCK)
{
printf("数据没有准备好\n");
printf("errno:%d\n",errno);
continue;
}
else
{
printf("read error\n");
}
}
}
}
因为数据没有准备好和读出错的时候,显示器上都不会有数据,所以我们引入errno来进行区分,当errno的值为EAGAIN或者EWOULDBLOCK的时候,说明是没有准备好。否则是读出错,我们可以运行程序来观察errno的值:

发现他的值是11,但是当读入成功之后值还是11,其实这里应该改为0的,但是可能是系统觉得read的返回值可以说明一切了就没有修改这个errno,errno的初始值是0,当数据没有准备好就被改成了11。

浙公网安备 33010602011771号