五种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。

posted @ 2022-09-24 20:28  卖寂寞的小男孩  阅读(57)  评论(0)    收藏  举报