IO模型 阻塞IO,非阻塞IO,IO多路复用,信号驱动IO,异步IO详解

大家好 我是积极向上的湘锅锅💪💪💪

简介

当用户应用想直接操作硬件上的数据时,是没有权限直接操作的,必须使用内核对外提供的指令或者函数
如果数据没还有准备好,必须等待数据就绪,总的来说,各种的IO模型主要是相差在俩个阶段

  • 等待数据就绪:将磁盘的数据读取到内核缓冲区
  • 读取数据:将内核缓冲区的数据读取到用户缓冲区

在这里插入图片描述


1. 阻塞IO

俩个阶段都阻塞
在这里插入图片描述

  • 用户应用调用recvform函数,内核检查有无数据
  • 如果没有数据,那就等待数据从磁盘读取到内核
  • 再从内核拷贝到用户空间

在整个过程中,进程都要阻塞等待数据的就绪和拷贝


2. 非阻塞IO

非阻塞IO会在调用recvform函数立即返回结果,但是如果数据还没有准备好,跟阻塞IO区别在于第一阶段不再是阻塞等待,而是会一直调用recvform函数询问内核是否准备好
在这里插入图片描述
看的出来这思路跟自旋一样,都是让CPU去做无用功,去尝试获取数据,那就导致了CPU在高并发的情况下会使用率暴增


3 IO多路复用

文件描述符(File Descriptor),简称FD,是一个从0开始递增的无符号整数,用来关联Linux中的一个文件,在Linux中,一切皆文件,例如常规文件,视频,硬件设备等

IO多路复用: 是利用单个线程监听多个FD,在某个FD可读,可写时候得到通知,从而避免无效的等待,充分利用CPU资源

常见的监听FD,通知的方式有三种

  • select
  • poll
  • epoll

差异:

  • select和poll只会通知用户进程有FD就绪,但不确认具体是哪个FD,需要用户进程遍历整个FD来确认
  • epoll则会在通知用户进程FD就绪的同时,把已经就绪的FD写入用户空间

3.1 IO多路复用-select

在这里插入图片描述
先从上往下看

  • __fd_mask是一个long int,在c语言里面占4个字节,32个bit位
  • 而fd_set是来装载FD的,是FD的一个集合,本身长度为32,因为前面修饰为__fd_mask,__fd_mask又是占32bit,所以总共可装载的FD为1024个bit,0代表该FD未准备就绪,1代表该FD准备就绪
  • select函数里面值得注意的是有不同的fd_set集合,在select根据fd的特性是分隔开的,而超时时间可以理解为select要等待一个或者多个就绪后返回,或者超时返回,那超时时间就是限制是否等待,或者最多等待多久

第一阶段流程如下
在这里插入图片描述
用户空间所做的就是传参和调用函数,将fd_set拷贝到内核空间,内核就会遍历一遍fd_set,如果没有数据就绪,那就休眠等待,直到唤醒或者超时

如果数据就绪,select就会返回已经就的FD的个数
而内核那就会将就绪的FD保留,未就绪的从fd_set删除,然后再拷贝回用户缓冲区,把用户空间创建的rdfs给覆盖掉

在这里插入图片描述

因为fd_set是以01代表就绪还是未就绪,所以用户空间还得重新遍历一遍fd_set,来获取已经就绪的FD是哪些,然后读取数据

可以看得出来,其实过程相当麻烦且臃肿

  • 需要进行俩次拷贝,从用户空间到内核空间,内核空间再到用户空间
  • 用户空间还得重新遍历整个fd_set
  • fd_set数量最多不能超过1024

3.2 IO多路复用-poll

与select相比,其实poll模式也就是在长度的限制上做了改进,poll模式下存储的FD个数将没有上限,但是同时也是一把双刃剑,监听的FD越多,每次遍历消耗的时间也越久,性能反而随之下降

在这里插入图片描述

poll函数:

  • 相比select,poll函数更加的简洁了,将FD的参数单独剥离出来,只用一个pollfd的数组表示,而这个数组的长度是没有上限的
  • 还有一点改进是poll函数中指明了需要查询的元素个数,也就是查询的长度是已知的,而select是未知的
  • 超时时间跟select一致

pollfd:

  • 不再使用二进制位表示,直接使用FD的整数表示
  • 不再将同一事件类型放在相同集合,在一个不同的pollfd数组中,每一个pollfd都可以有不同的事件类型
  • 实际发生的类型表示如果已经准备就绪,那就是第二个参数中所表明的事件类型,否则就为0表示

IO流程:

  • 创建pollfd数组,将所要查询的fd添加进去,数组大小自定义
  • 调用poll函数,将pollfd数组拷贝到内核空间,转为链表存储
  • 等待数据准备就绪或者超时,拷贝pollfd到用户空间,并且返回已经就绪的FD个数
  • 判断个数是否大于0
  • 个数大于0则用户空间扫描pollfd数组,找到就绪的FD

3.3 IO多路复用-epoll

相比于前俩种模式,epoll做出了相当大的改进

在这里插入图片描述
首先,会将需要监听的FD添加到内核空间
调用epoll_create函数,会创建一个eventpoll实例
而里面的俩个参数,分别是一颗红黑树(省内存、且有序)和链表,分别代表要监听的FD和已经就绪的FD,跟前俩种模式相比,也是将监听的FD和就绪的FD分开来,不再是一个数组进行保存

在这里插入图片描述
而提供的epoll_ctl函数,其中参数也很明了
也就是向这颗红黑树添加节点,并且设置callback函数进行监听,如果有准备就绪的FD,自动触发,这个对应的FD就加入链表当中

在这里插入图片描述

然后,就需要从这个链表上面取出数据了
调用epoll_wait函数,值得注意的是,在参数里面,设置一个空的数组,并且数组设置最大长度

有什么作用?
因为我们知道需要查询的FD已经在红黑树里面,所以只需要拷贝一份链表里面的FD到空数组里面,也就是说,在这个数组里面,一定是已经就绪的FD,也就减去了需要遍历找出哪些FD就绪的的时空消耗

整个流程如下
在这里插入图片描述
跟前俩种模式相比

  • 不再是每一次都需要传所有要监听的FD,只需要一次创建eventpoll实例,在之后新加入的FD,调用event_ctl即可,而等待操作也是如此,大大减少了拷贝的次数
  • 不再是需要将整个FD数组都拷贝回用户空间,也不需要再去遍历FD数组找到就绪的FD,直接返回的就是就绪的FD
  • FD数量也没有上限,但是理论上来讲从性能角度来看,链表也不能太长
  • 红黑树的性能不会随着FD增加而改变

4. 信号驱动IO

信号驱动IO表示在内核SIGIO建立信号关联回调,如果有FD就绪了,就会发出SIGIO信号通知用户,期间用户可以执行其他的业务,无需阻塞等待,但是就绪之后,还是需要调用recvform函数将数据拷贝回用户空间

在这里插入图片描述
问题来了,为什么信号驱动IO看起来那么的完美无缺,为什么没有广泛的使用呢?

假设一个场景,当产生大量的IO操作的时候,同一时间可能返回大量的信号到信号队列里面,那么极有可能回产生队列溢出,并发性能低


5. 异步IO

异步的概念运用的已经极其广泛,异步IO也是如此
在俩个阶段都是非阻塞

在这里插入图片描述
整个过程中用户只需要调用aio_read函数通知内核我想要哪些数据,然后就不管了

剩下的事情,等待数据,拷贝数据,全都交给内核去做,拷贝完了之后再通知用户数据已经就绪拷贝好

好比一个场景:
一个人想吃饭,给妈妈说要吃什么,妈妈全部给准备好,然后再通知这个人去吃饭

同样,在高并发的情况下,由于用户只需要调用aio_read,如果请求过多,内核的负担是很大的,需要进行限流


该篇内容的拓展推荐:
Redis 单线程与多线程模型详解

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

owensweat

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值