一、什么是IO模型
我们的应用都是部署在linux系统中,linux系统也是一种应用,它是基于计算机硬件的一种操作系统软件。当我们接收一次网络传输,计算机硬件的网卡会从网络中将读到的字节流写到linux的buffer缓冲区内存中,然后用户空间会调用linux对外暴露的接口,将linux中的buffer内存中的数据再读取到用户空间。这一次读操作就是一次IO。同样写也是这样的。
不同的操作系统,IO模型不一样,下面介绍的是Linux系统的几种IO模型
这样做是为了保护Linux操作系统,避免外部应用或者人为直接操作内核系统。
当线程操作在用户空间时候的状态称为:用户态
当线程操作在内核空间时候的状态称为:内核态
IO的性能瓶颈:
a.用户态与内核态的切换(数据拷贝)
b.读写线程的阻塞等待
linux的IO模型就是针对这两点去优化的
二、Linux的IO模型
1.阻塞IO模型
当用户应用线程调用linux操作系统的recvfrom函数读取数据的时候,如果内核的buffer内存中没有数据,那么用户线程会阻塞等待,直到内核的buffer内存中有数据了,才去将内核的buffer内存中的数据拷贝到用户应用内存中。
类似于你排队买包子,但是包子这时候没有了,但是你不知道还有没有包子,如果没有,你只能在那等待包子出炉,什么也做不了,干等着,直到包子出炉了,你才能拿到包子,放到自己的口袋中。
问题:
当一个线程阻塞住了,会导致后续所有的线程都阻塞住,即使后面的读写数据已经就绪,也无法进行读写。
2.非阻塞IO模型
当用户应用线程调用linux操作系统的recvfrom函数读取数据的时候,如果内核的buffer内存中没有数据,那么用户线程会直接拿到结果(没有数据)不需要等待,于是又会发起一次recvfrom函数调用,直到内核的buffer内存中有数据了,才去将内核的buffer内存中的数据拷贝到用户应用内存中。
类似于你排队买包子,老板直接和你说没有包子了,你已经知道了结果,你一遍又一遍的问老板,还有没有包子了,直到老板出炉了包子,告诉你,你才能拿到包子,放到口袋中。
问题:
如果一直没有数据的话,线程会死循环的调用recvfrom函数,频繁使用CPU资源,导致CPU资源的浪费。
3.IO多路复用
文件描述符(FD)
内核kernel)利用文件描述符(file descriptor)来访问文件。文件描述符是非负整数打开现存文件或新建文件时,内核会返回一个文件描述符。读写文件也需要使用文件描述符来指定待读写的文件。文件包含音频文件,常规文件,硬件设备等等,也包括网络套接字(Socket)。
IO多路复用就是利用单线程去监听多了文件描述符FD,并在某个文件描述符FD可读,可写的时候接收到通知,避免无效的等待,充分利用CPU资源。
select模式
用户应用线程调用select函数去监听多个FD文件描述符,如果没有数据,还是要等待,如果有就绪的文件FD,说明有数据,那就去读对应的FD就绪的文件数据,此时内核会将文件FD集合拷贝到用户内存中,然后去遍历FD集合,找到可以读的数据的FD,然后再去读取,读完了之后会将FD的集合再拷贝到内核内存中。
类似于你去餐厅排队点餐,这时候有一个服务员,服务员通过平板监控后厨,你只需要询问服务员有没有东西吃就可以了,如果没有你还是需要等待,但是如果有了,服务员通过监控就知道有东西可以吃了,就会让你点餐了。
select模式的问题:
a.需要将整个FD数组从用户空间拷贝到内核空间,select结束还要再次拷贝到用户空间
b.select无法得知是具体的哪一个FD就绪,需要便利整个FD集合(数组)
c.select监听的FD集合(数组)大小固定是1024,底层设计写死是1024个。
poll模式
poll模式其实和select模式原理差不多,不同的点在于,poll模式底层加上了一个event事件,分成读事件,写事件,异常事件等等。
流程:
a.先添加需要监听的事件,是读事件,还是写事件,可以是多个事件
b.将监听到的事件FD,转换成链表,保存在内核缓冲区
c.内核缓冲区将事件FD链表拷贝到用户缓冲区,并返回就绪的FD数量
d.用户缓冲区判断就绪的FD数量,如果大于0则开始便利事件FD链表
poll模式的问题:
a.需要将整个FD链表从用户空间拷贝到内核空间,poll结束还要再次拷贝到用户空间
b.poll无法得知是具体的哪个就绪的FD事件,需要便利整个FD事件(链表)
对比select模式
由于使用了链表,理论上事件个数可以是无数个,但是随着事件个数增多,链表的遍历性能会下降,而且当没有就绪事件的时候还是需要等待。
epoll模式
epoll模式是在poll模式的基础上再次改进,首先将存储事件的FD链表改成了红黑树(理论上也是无上限的),红黑树的遍历性能稳定,其次就是将具体的就绪事件单独复制出来然后拷贝给用户缓冲区,用户缓冲区拿到的是已经就绪的事件,无需遍历性能再次提升。
流程:
a.先将注册的监听事件
b.将所有的FD挂载在一个红黑树中
c.当FD就绪调用回调函数将对应的FD复制到一个链表中
d.将链表从内核缓冲区拷贝到用户缓冲区,并返回链表大小n
e.用户线程直接判断n大小,当n不为0的时候,直接读取链表(全部是就绪的FD)的数据即可
信号驱动IO
当用户应用线程调用linux操作系统的sigaction函数,直接返回,然后该线程去做其他事情了,当有数据来了的时候,内核空间会去递交信号给用户空间,此时用户空间会调用recvfrom函数去将数据从内核空间缓冲区拷贝到用户空间缓冲区,并处理数据。
类似于你点餐点完了,服务员会给你一个号码,然后你的餐好了,服务员会叫你的号码,然后你就去拿餐。
问题:
当调用的线程过多,对应的信号量会增多,SIGIO函数处理不及时,会导致保存信号的队列溢出;而且内核空间与用户空间频繁的进行信号量的交互,性能很差。
异步IO
性能上来说也是不错的,就是在实际开发中,需要控制它的线程并发数,所以实现起来会非常麻烦,所以使用很少
总结
三种IO多路复用对比来说epoll的效果是最好的。解决了select和poll模式中存在的问题。
而redis就是使用的epoll模式的IO模型。