一、用户空间和内核空间
应用需要通过Linux内核与硬件交互。
![](https://img-blog.csdnimg.cn/img_convert/ac21f049c9e2cc3e929f541314bb773e.png)
内核本质也是应用,运行的时候也需要CPU资源、内存资源。用户应用也在消耗这些资源。
为了避免用户应用导致冲突甚至内核崩溃,用户应用与内核是分离的:
进程的寻址空间会划分为两部分:内核空间、用户空间
用户空间只能执行受限的命令(Ring3),而且不能直接调用系统资源,必须通过内核提供的接口来访问
内核空间可以执行特权命令(Ring0),调用一切系统资源。
寻址空间:无论内核还是用户应用,都无法直接访问物理内存,而是分配虚拟的内存空间,映射到不同的物理内存空间。内核和用户应用,再去访问虚拟内存空间,就需要有对应的虚拟地址(无符号的整数)
示例:32位系统,带宽32,地址的最大值就是2的32次方,也就是寻址的范围从0到2的32次方,也就是4GB.
![](https://img-blog.csdnimg.cn/img_convert/540c5fc4b015622495497aa27c8fc32e.png)
![](https://img-blog.csdnimg.cn/img_convert/629386bd757ba278927f569a9d227d52.png)
IO在用户空间和内核空间切换的整体流程:
![](https://img-blog.csdnimg.cn/img_convert/9870f95a64173904c722e47804b778f6.png)
二、阻塞IO
访问流程图
![](https://img-blog.csdnimg.cn/img_convert/397ce977d65e227cc8493a257bdf8776.png)
顾名思义,阻塞IO就是两个阶段都必须阻塞等待。
![](https://img-blog.csdnimg.cn/img_convert/25ea3bee916514ffbdebd88b25772daf.png)
三、非阻塞IO
顾名思义,非阻塞IO的recvfrom操作会立即返回结果而不是阻塞用户进程。
![](https://img-blog.csdnimg.cn/img_convert/d8fb3d3a2e50280b724519a6c4a69836.png)
可以看到,非阻塞IO模型中,用户进程在第一个阶段是非阻塞,第二个阶段是阻塞状态。虽然是非阻塞,但是性能并没有得到提高。而且忙等机制导致CPU空转,CPU使用率暴增。
四、IO多路复用
1、背景
无论是阻塞IO还是非阻塞IO,用户应用在一阶段都需要调用recvfrom来获取数据,差别在于无数据时的处理方案:
如果调用recvfrom时,恰好没有数据,阻塞IO会使进程阻塞,非阻塞IO使CPU空转,都不能充分发挥CPU的作用。
如果调用recvfrom时,恰好有数据,则用户进程可以直接进入第二阶段,读取并处理数据
比如服务端处理客户端Socket请求时,在单线程情况下,只能依次处理每一个socket,如果正在处理的soket恰好未就绪(数据不可读或不可写),线程就会被阻寒,所有其它客户端socket都必须等待,性能自然会很差。
这就像服务员给顾客点餐,分两步:
1、顾客思考要吃什么(等待数据就绪);
2、顾客想好了,开始点餐(读取数据)。
第一步要提高效率的几种方法:
1、方案一:增加更多服务员(多线程)
2、方案二:不排队,谁想好了吃什么(数据就绪了),服务员就给谁点餐(用户应用就去读取数据)
那么问题来了:用户进程如何知道内核中数据是否就绪呢?
2、IO多路复用
文件描述符(File Descriptor):简称FD,是一个从0 开始递增的无符号整数,用来关联Linux中的一个文件。在Linux中,一切皆文件,例如常规文件、视频、硬件设备等,当然也包括网络套接字 (Socket)。
IO多路复用:是利用单个线程来同时监听多个FD,并在某个FD可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源。
![](https://img-blog.csdnimg.cn/img_convert/4b13feb93a6eea0d63aeb0266587d188.png)
监听FD的方式、通知的方式又有多种实现,常见的有:
1、select
2、poll
3、epoll
差异:
(1)select和poll只会通知用户进程有FD就绪,但不确定具体是哪个FD,需要用户进程逐个遍历FD来确认;
(2)epoll则会在通知用户进程FD就绪的同时,把已就绪的FD写入用户空间
3、select
(1)底层框架
select是Linux中最早的I/O多路复用时限方案:
![](https://img-blog.csdnimg.cn/img_convert/cb5de9f7efa7ad30c9ecdfcd63318133.png)
(2)执行流程
用户空间创建fd_set rfds,默认值0,大小1024bit位
用户空间假如要监听 fd=1,2,5,把1、2、5bit位置为1
用户空间执行select(5+1, rfds, null, null, 3)
用户空间拷贝fd_set 到内核空间
内核空间遍历fd_set
内核空间没有就绪,则休眠。
内核空间等待数据就绪,被唤醒或超时。未就绪的改成0
内核空间拷贝fd_set 到用户空间,覆盖用户空间的fd_set
用户空间遍历fd_set,找到就绪的fd,读取其中数据
![](https://img-blog.csdnimg.cn/img_convert/df143bc1e76b36f7999ada0b1698008b.png)
![](https://img-blog.csdnimg.cn/img_convert/03e4877d5180a2da68655e7233902c31.png)
![](https://img-blog.csdnimg.cn/img_convert/0f0e2c25b722148a21d9f2fba57c7796.png)
(3) select模式存在的问题
需要将整个fd_set从用户空间拷贝到内核空间,select结束还要再次拷贝回用户空间
select无法得知具体是哪个fd就绪,需要遍历整个fd_set
fd_set监听的fd数量不能超过1024
4、poll
(1)底层框架
poll模式对select模式进行了简单改进,但性能提升不明显。
![](https://img-blog.csdnimg.cn/img_convert/cb0c9b1475d03730bb98b3c6f2a0b524.png)
(2)执行流程
创建pollfd数组,向其中添加关注的fd信息,数组大小自定义
调用poll函数,将pollfd数组拷贝到内核空间,转链表存储,无上限
内核遍历fd,判断是否就绪
数据就绪或超时后,拷贝pollfd数组到用户空间,返回就绪fd数量n
用户进程判断n是否大于0
大于0则遍历pollfd数组,找到就绪的fd
(3)对比select
select模式中的fd_set大小固定为1024,而pollfd在内核中采用链表,理论上无上限
监听FD越多,每次遍历消耗时间也越久,性能反而会下降
5、epoll
(1)底层代码
epoll模式是对select和poll的改进,它提供了三个函数:
![](https://img-blog.csdnimg.cn/img_convert/16419e6685fcf6c9883d0f4ac6073d59.png)
(2)执行流程
1、调用epoll_create(1),创建epoll实例
![](https://img-blog.csdnimg.cn/img_convert/b668f4ae7fd10e139e54d8293a969d83.png)
2、调用epoll_ctl(…),添加要监听的FD,关联callback,当callback触发时,把对应的FD加入到链表list_head中。
![](https://img-blog.csdnimg.cn/img_convert/882c356bf3fc74581de204efa8f9576d.png)
![](https://img-blog.csdnimg.cn/img_convert/1b4d1e1e19fc78921081b7e678b0c238.png)
3、epoll_wait(…, events)等待FD就绪
![](https://img-blog.csdnimg.cn/img_convert/15d5896edf711311733d4987aa075659.png)
6、总结
1、select模式存在的三个问题:
能监听的FD最大不超过1024
每次select都需要把所有要监听的FD都拷贝到内核空间
每次都要遍历所有FD来判断就绪状态
2、poll模式的问题:
poll利用链表解决了select中监听FD上限的问题,但依然要遍历所有FD,如果监听较多,性能会下降
3、epoll模式中如何解决这些问题的?
基于epoll实例中的红黑树保存要监听的FD,理论上无上限,而且增删改查效率都非常高,性能不会随监听的FD数量增多而下降
每个FD只需要执行一次epoll_ctl添加到红黑树,以后每次epol_wait无需传递任何参数,无需重复拷贝FD到内核空间
内核会将就绪的FD直接拷贝到用户空间的指定位置,用户进程无需遍历所有FD就能知道就绪的FD是谁
7、事件通知机制
当FD有数据可读时,我们调用epoll_wait就可以得到通知。事件通知的模式有两种:
LevelTriggered:简称LT。当FD有数据可读时,会重复通知多次,直到数据处理完成。是Epoll的默认模式。
EdgeTriggered:简称ET。当FD有数据可读时,只会被通知一次。不管数据是否处理完成。
区别:
拷贝数据之前,会将链表中的fd从list_head中断开连接,然后拷贝。
假如数据没有处理完,
当采用ET时,直接删掉fd,再次调用epoll_wait,list_head中没有数据;
当采用LT,会再次添加到 list_head,再次调用epoll_wait,list_head中有数据;
![](https://img-blog.csdnimg.cn/img_convert/068f2190b8921f7d079d880761e1199a.png)
结论:
ET模式避免了LT模式可能出现的惊群现象
ET模式最好结合非阻塞IO读取FD数据,相比LT会复杂一些
8、web服务流程
基于epoll模式的web服务的基本流程图:
![](https://img-blog.csdnimg.cn/img_convert/4d950713ec1f543f4c9552e95c1f9103.png)
五、信号驱动IO
信号驱动IO是与内核建立SIGIO的信号关联并设置回调,当内核有FD就绪时,会发出SIGIO信号通知用户,期间用户应用可
以执行其它业务,无需阻塞等待,
![](https://img-blog.csdnimg.cn/img_convert/437c6447dcd7c42f6e8750c5aa60ff9c.png)
当有大量IO操作时,信号较多,SIGIO处理函数不能及时处理可能导致信号队列溢出。
而且内核空间与用户空间的频繁信号交互性能也较低。
六、异步IO
![](https://img-blog.csdnimg.cn/img_convert/442cc0cfa8fc9e3636466042319350bd.png)
IO操作是同步还是异步,关键看数据在内核空间与用户空间的拷贝过程(数据读写的IO操作),也就是阶段二是同步还是异步:
![](https://img-blog.csdnimg.cn/img_convert/1ffc7c25b7e43b7fef4d30aea27154c4.png)
七、Redis网络模型
面试题:Redis到底是单线程还是多线程?
如果仅聊Redis核心业务部分(命令处理),答案是单线程;
如果是聊整个Redis,那么答案就是多线程。
在Redis版本迭代过程中,在两个重要的时间点上引入了多线程支持:
Redis4.0:引入多线程异步处理一些耗时较长的任务,例如异步删除命令unlink
Redis6.0:在核心网络模型中引入多线程,进一步提高对于多核CPU的利用率
Redis为什么要选择单线程?
抛开持久化不谈,Redis是纯内存操作,执行速度非常快,它的性能瓶颈是网络延迟而不是执行速度,因此多线程并不会带来巨大的性能提升。
多线程会导致过多的上下文切换,带来不必要的开销。
引入多线程会面临线程安全问题,必然要引入线程锁这样的安全手段,实现复杂度增高,而且性能也会大打折扣。
1、单线程
Redis通过IO多路复用来提高网络性能,并且支持各种不同的多路复用实现,并且将这些实现进行封装,提供了统一的高性能事件库API库AE:
![](https://img-blog.csdnimg.cn/img_convert/d4f8f9c399affa9eb72d7cd36391dca8.png)
ae.c文件判断服务器类型,选择执行哪个ae.c文件
![](https://img-blog.csdnimg.cn/img_convert/1f37d444c56cef83fd08d7f5dfe24235.png)
Redis单线程网络模型的整个流程:
![](https://img-blog.csdnimg.cn/img_convert/98016b3ab6048bd07cf4c7efe8c238f4.png)
![](https://img-blog.csdnimg.cn/img_convert/b2bd4d183db14653a627b301335f8efb.png)
整个流程:
![](https://img-blog.csdnimg.cn/img_convert/efa4d3a8167e5ab35454ecefd6a2532a.png)
Redi6.0版本中引入了多线程,目的是为了提高IO读写效率。因此在解析客户端命令、写响应结果时采用了多线程。核心的命令执行、IO多路复用模块依然是由主线程执行。
![](https://img-blog.csdnimg.cn/img_convert/ecefa19d53f891e7322dd225bbdeb6af.png)