目录
前言
1.用户空间和内核空间
1.2用户空间和内核空间的切换
1.3切换过程
2.阻塞IO
3.非阻塞IO
4.IO多路复用
4.1.IO多路复用过程
4.2.IO多路复用监听方式
4.3.IO多路复用-select
4.4.IO多路复用-poll
4.5.IO多路复用-epoll
4.6.select poll epoll总结
4.7.IO多路复用-事件通知机制
4.8.IO多路复用-web服务流程
5.信号驱动IO
6.异步IO
7.同步和异步
前言
Redis 以其卓越的性能和灵活的特性,成为众多开发者在缓存、消息队列等场景的首选。而 Redis 强大性能的背后,其网络模型与 IO 机制发挥着关键作用。数据的读写速度直接影响着用户体验。大家日常逛的电商平台,流畅加载商品详情页离不开它;刷社交软件时,点赞瞬间显示也有它的功劳 。接下来,我们就深入挖掘一下,看看 Redis 是如何通过它们实现高效运转的。
1.用户空间和内核空间
为了避免用户应用导致冲突甚至内核崩溃,用户应用与内核是分离的:
进程的寻址空间会划分两部分:内核空间,用户空间
这就代表了一个完整的32位寻址空间
那什么是寻址空间呢?
寻址空间是指计算机系统中处理器能够访问的内存地址范围。
计算机中的内存是由许多存储单元组成的,每个存储单元都有一个唯一的编号,这个编号就是地址。处理器通过地址来访问内存中的数据和指令。寻址空间就是这些地址的集合,它决定了处理器能够访问的内存大小。
我们还要在系统权限上进行划分,因为我们cpu运行的各种各样的命令里边,有一些命令风险等级比较低,有一些比较高。所以cpu会把各种各样的命令划分成四个不同的等级,Ring0风险等级最低,Ring3风险等级最高
- 用户空间只能执行受限的命令(Ring3),而且不能直接调用系统资源,必须通过内核提供的接口来访问
- 内核空间可以执行特权命令(Ring0),调用一切系统资源
1.2用户空间和内核空间的切换
应用程序在用户空间,内核应用在内核空间,但是我们一个进程它在执行过程中因为业务比较多,可能会执行一些普通命令和特权命令调用系统资源。因此进程会在用户空间和内核空间之间进行一个转换
linux系统为了提高io效率,会在用户空间和内核空间都加入缓冲区:
- 写数据时,要把用户缓冲数据拷贝到内核缓冲区,然后写入设备
- 读数据时,要从设备读取数据到内核缓冲区,然后拷贝到用户缓冲区
1.3切换过程
IO在用户空间和内核空间切换流程
写数据到磁盘过程
- 1.进程在做一些简单运算,字符串处理,之后要把数据写出到我们的磁盘需要调用我们的内核
- 2.写数据先写到缓冲区,要往磁盘写必须要切换到内核
- 3.切换到内核,内核没有我要写的数据,所以要先把用户缓冲区的数据拷贝到内核的缓冲区,然后再把缓冲区的数据写入我们的磁盘
读数据到用户空间
- 1.开始用户空间发起read的请求,请求到达内核空间判断有没有数据,如果要读的是磁盘,要先去寻址wati,for data,寻址到之后把数据读取到缓冲区
- 2.把数据从内核缓冲区拷贝到用户空间用户再区处理这些数据
从上图可知IO读写效率的主要原因是
- 1.数据等待过程,(用户空间读数据发起read请求,内核空间接收到这个请求需要去寻址和把数据写到缓冲区)
- 2.就是数据拷贝过程非常影响性能,一个空间缓冲区数据拷贝到另一个空间缓冲区
数据拷贝是由操作系统内核来完成的。
2.阻塞IO
不同IO模型的差别就是在1.等待数据就绪 2.读取数据过程
阻塞IO就是两个阶段都必须阻塞等待;
所以阻塞IO就是2.把数据写到内核缓冲区1.把内核缓冲区数据写到用户空间缓冲区这两个阶段都是阻塞状态。
3.非阻塞IO
顾名思义,非阻塞IO的recvfrom操作会立即返回结果而不是阻塞用户进程。
非阻塞IO就是第一阶段不停调用recvfrom去读取数据,读取不到不会阻塞一直反复调用直到成功(反而让cpu使用率增加)
然后第二阶段内核拷贝数据到用户空间依然是阻塞状态
4.IO多路复用
无论是阻塞IO还是非阻塞IO,用户应用在一阶段都需要调用recvfrom来获取数据,差别在于无数据时的处理方案:
- 如果调用recvfrom时,恰好没有数据,阻塞IO会使进程阻塞,非阻塞IO使CPU空转,都不能充分发挥CPU的作用。
- 如果调用recvfrom时,恰好有数据,则用户进程可以直接进入第二阶段,读取并处理数据
比如服务端处理客户端Socket时,在单线程情况下,只能依次处理每一个socket,如果正在处理的socket恰好未就绪(数据不可读或不可写),线程就会被阻塞,所有其它客户端socket都必须等待,性能自然会很差。
解决方案就是数据就绪了,用户应用就去读取数据
用户进程如何知道内核中数据是否就绪呢?
4.1.IO多路复用过程
文件描述符(File Descriptor):简称FD,是一个从0开始递增的无待号整数,用来关联Linux中的一个文件。在Linux中,一切皆文件,例如常规文件,视频,硬件设备等,当然也包括网络套接字(socket)。
IO多路复用:是利用单个线程来同时监听多个FD,并在某个FD可读,可写时得到通知,从而避免无效的等待,充分利用CPU资源IO多路复用过程:
- 用户应用首先去调用select函数,不在是调用recvfrom(recvfrom直接是尝试读取数据读的目标是具体某一个FD)
- select函数内部可以接收多个FD,也就是说可以把每个客户端socket对应的FD,传给select函数,然后传入到内核中,内核就可以去检查你要去监听的多个FD,有没有任何一个是就绪的,只要由任意一个或者多个就绪就会直接返回这个结果。
- 如果这n个FD都没有就绪那么就会稍微等一会,在等待过程中其实就会有后台进程去监听这些FD,一旦有任意一个或者多个就绪返回结果。这个等待不可避免的。
- 拿到readable结果了就去调用recvfrom我们可以明确知道哪些FD准备好啦,然后拷贝数据返回结果(循环调用)
其实IO多路复用1.等待数据就绪用户进程也是阻塞阶段,阶段二数据拷贝同样是阻塞的
区别在于:
- 阻塞IO调用的是调用recvfrom去监听某一个FD有没有就绪,没有就会阻塞
- IO多路复用调用的是select函数去监听多个FD,只要有一个FD就绪就去处理
4.2.IO多路复用监听方式
IO多路复用监听FD的方式,通知的方式又有多种实现,常见的有:
select
poll
epoll
差异:
- select和poll只会通知用户进程有FD就绪,但不确定具体是哪个FD,需要用户进程逐个遍历FD来确认
- epoll则会在通知用户进程FD就绪的同时,把已就绪的FD写入用户空间
4.3.IO多路复用-select
select是Linux中最早的IO多路复用实现方案:
select函数:
1.nfds:这是需要监视的最大文件描述符加 1。举例来说,若要监视的文件描述符为 3、4、5,那么
nfds
的值就是5 + 1 = 6
。为了能更精细地管理和监控不同类型的 IO 事件,select将FD分成三个集合
2.readfds:该集合用于监视文件描述符的可读事件。
3.writefds
:此集合用于监视文件描述符的可写事件。
4.exceptfds
:这个集合用于监视文件描述符的异常事件。执行流程:
首先用户空间
- 1.创建fd_set rfds集合
- 2.fd_set集合底层使用fds_bits[]来监听,共可以监听1024个Fd,要监听哪个就把哪个位置变为1。比如要监听fd = 1,2,5
- 3.执行select函数,把fds_bits[]数组拷贝到内核空间
内核空间
- 1.首先遍历fd_set看有没就绪
- 2.没有就绪则睡眠,后台监听
- 3.等待数据就绪被唤醒或超时
- 4.fd = 1数据就绪,其他没有就绪剔除,返回结果有几个就绪了,拷贝到用户空间遍历哪个就绪了
处理完之后再次把要监听数据放到集合里执行select去监听往复处理数据
缺点:
- 1.需要将整个fd_set从用户空间拷贝到内核空间,select结束还要再次拷贝回用户空间
- 2.select无法得知具体哪个FD就绪,需要遍历整个FD_set,
- 3.fd_set监听的fd数量不能超过1024
4.4.IO多路复用-poll
poll模式对select模式做了简单改进,但性能提升不明显,部分关键代码如下:
poll参数部分和select差不多主要区别在于fds,pollfd数组,没有去划分不同事件集合全部划分到一个数组当中。区别是哪种事件主要在于结构体中 events属性不同值代表不同监听类型
revents表示实际发生的事件类型,内核会把就绪的事件类型放在这个集合里,超时时间过了还没有就绪FD,就把这个值给成0返回给用户空间,这样一来就知道这个FD有没有发生事件。
IO流程:
1.创建pollfd数组,向其中添加关注的fd信息,数组大小自定义
2.调用poll函数,将pollfd数组拷贝到内核空间,转链表存储,无上限
3.内核遍历fd,判断是否就绪
4.数据就绪或超时后,拷贝pollfd数组到用户空间,返回就绪fd数量n
5.用户进程判断n是否大于0
6.大于0则遍历pollfd数组,找到就绪的fd与select相比:
- select模式中的fd_set大小固定为1024,而pollfd在内核中采用链表,理论上无上限
- 监听fd越多,每次遍历消耗时间也越久,性能反而会下降
4.5.IO多路复用-epoll
epoll模式是对select和poll的改进,它提供了三个函数:
eventpoll结构体:使用红黑树存储要监听的FD,和使用链表记录就绪的FD
2.接下来我们就要去监听FD了,会使用到第二个函数
这个函数epoll_ctl将我们一个FD添加到eventpoll里面,相当于监听FD
传入的参数包括(需要添加到哪个eventpoll,是增删改哪个操作,要监听的fd,监听事件类型)
3.第三个函数就是等待FD的监听就绪。函数传入参数(eventpoll指针,空数组用于接收就绪的FD,events数组最大长度,超时时间)返回就绪的数量
那么我怎么知道哪个FD就绪了呢?空数组就派上用场了
我们把这个数组拷贝到用户空间就知道哪些FD就绪了,不用去遍历
4.6.select poll epoll总结
select模式存在的三大问题:
- 能监听的FD最大不超过1024
- 每次select都需要把所有要监听的FD都拷贝到内核空间
- 每次都要遍历所有FD来判断就绪状态
poll模式的问题:
poll模式利用链表解决了select中监听FD上限问题,但依然要遍历所有FD,如果监听较多,性能会下降
epoll模式中如何解决这些问题的?
- 基于epoll实例中的红黑树保存要监听的FD,理论上无上限,而且增删改查效率都非常高,性能不会随监听FD数量增多而下降
- 每个FD只需要执行一次epoll_ctl添加到红黑树,以后每次epol_wait无需传递任何参数,无需重复拷贝到Fd到内核空间
- 内核会将就绪的FD拷贝到用户空间的知道位置,用户进程无需遍历所有FD就知道就绪的FD是谁
4.7.IO多路复用-事件通知机制
但FD有数据可读时,我们调用epoll_wait就可以得到通知。但是事件通知的模式有两种:
- LevelTriggered:简称lLT.当FD有数据可读时,会重复通知多次,直至数据处理完成。是epoll的默认模式。
- EdgeTriggered:简称LT.当FD有数据可读时,只会被通知一次,不管数据是否处理完成。
举个栗子:
- 假设一个客户端socket对应的fd已经注册到了epoll实例中
- 客户端socket发送了2kb的数据
- 服务端调用epoll_wait,得到通知说fd就绪
- 服务端从fd读取了1kb数据
- 回到步骤3(再次调用epoll_wait,形成循环)
总结:
ET模式避免了LT模式可能出现惊群现象
ET模式最好结合非阻塞IO读取FD数据,相比LT会复杂一些
4.8.IO多路复用-web服务流程
serverSocket:接收客户端请求
5.信号驱动IO
信号驱动IO是内核建立SIGIO的信号管理并设置回调,但内核有FD就绪时,会发出SIGIO信号通知用户,期间用户可以执行其它业务,无需阻塞等待。
首先信号驱动IO第一阶段处理流程:
- 用户进程一上来不是调用recvfrom,而是系统调用sigaction指定FD绑定信号处理函数,立即结束不用阻塞等待,
- 如果没有数据内核帮我们监听如果有数据了帮我们去唤醒并且提交一个信号给我们用户进程,
- 之前建立的SIGIO信号处理函数就去处理这个信号,整个过程用户不用去等待可以去干其他事情
第二阶段和其他阻塞IO一致
那么你可能会问这个IO这么好为什么不用而要去用多路复用IO?
当有大量IO操作时,信号较多,SIGIO处理函数不能及时处理可能导致信号队列溢出
而且内核空间与用户空间频繁的信号交互性能也较低。
6.异步IO
异步IO的整个过程都是非阻塞的,用户进程调用完异步API后尽可以去做其他事情,内核等待数据就绪并拷贝到用户空间后才会递交信号,通知用户进程。
- 1.异步IO并没有调用recvfrom函数而是调用aio_read通知内核说我想读哪个FD,读到哪里去就结束了。
- 2.内核把数据准备就绪再把数据拷贝完成通知用户进程
异步IO在两个阶段都是非阻塞的
这种模式听着非常好但是高并发场景下如果对异步 I/O 的并发度控制不当,可能会导致系统资源过度使用,从而影响性能。
过多的异步网络请求可能会导致网络拥塞,降低系统的响应速度。
7.同步和异步
IO操作是同步还是异步,关键看数据在内核空间与用户空间的拷贝过程(数据读写的IO操作),也就是阶段二是同步还是异步: