Redis为什么快?
- 单线程处理命令不需要线程上下文切换的开销。
- 网络多线程+后台主从复制和刷盘等操作使用fork子进程的方式在另一个cpu核上操作较少网络开销。
- 网络多线程实际上是使用的IO多路复用机制。
- 设计了多种高效的数据结构以及数据库使用哈希表存储数据的方式。
- RESP这种简单的网络传输协议避免序列化和网络传输占用太多时间。
本来主要讨论的是Redis的IO多路复用机制。
网络模型
在《UNIX网络编程》一书中,总结归纳了5种IO模型:
- 阻塞IO(Blocking IO)
- 非阻塞IO(Nonblocking IO)
- IO多路复用(IO Multiplexing)
- 信号驱动IO(Signal Driven IO)
- 异步IO(Asynchronous IO)
阻塞和非阻塞
阻塞和非阻塞对于操作系统来说是针对于系统调用来体现的。如果发生IO类的系统调用,必须要等到系统调用完成再返回,或者中途被中断、出错等情况才返回,这就是阻塞式IO。如果不管IO系统调用成功与否直接返回的话就是非阻塞式IO的调用。
同步和非同步
这里的同步与非同步,也是指I/O操作。当把阻塞、非阻塞、同步和非同步放在一起时,会让我们非常混乱。
同步是否就是阻塞,非同步是否就是非阻塞呢?
实际上在I/O操作中,它们是不同的概念。同步既可以是阻塞的,也可以是非阻塞的,而常用的Linux的I/O调用实际上都是同步的。这里的同步和非同步,是指I/O数据的复制工作是否同步执行。
- 就是说当数据到来了,读取数据的过程是否需要等待,不等待数据获取完成就是异步,等待数据获取完成就是同步。
- 异步的话就需要再数据获取完成之后再进行回调。
同步阻塞IO
应用程序想要去读取数据的时候是无法直接去读取磁盘数据的,需要先到内核里边去等待内核操作硬件拿到数据,这个过程就是1,是需要等待的,等到内核从磁盘上把数据加载出来之后,再把这个数据写给用户的缓存区,这个过程是2,如果是阻塞IO,那么整个过程中,用户从发起读请求开始,一直到读取到数据,都是一个阻塞状态。
以下是具体流程:
DMA 的工作方式如下:
- CPU 需对 DMA 控制器下发指令,通知它需要读取的数据以及存放的位置;
- 接下来,DMA 控制器会向磁盘控制器发出指令,通知它从磁盘读数据到其磁盘缓冲区中,接着磁盘控制器将缓冲区的数据传输到内核缓冲区;
- 当磁盘控制器把数据传输到内存的操作完成后,磁盘控制器在总线上发出一个确认成功的信号到 DMA 控制器;
- DMA 控制器收到信号后,DMA 控制器发中断通知 CPU 指令完成,CPU 就可以直接用内存里面现成的数据了;
可以看到,CPU 当要读取磁盘数据的时候,只需给 DMA 控制器发送指令,然后返回去做其他事情,当磁盘数据拷贝到内存后, DMA 控制机器通过中断的方式,告诉 CPU 数据已经准备好了,可以从内存读数据了。仅仅在传送开始和结束时需要 CPU 干预。
用户去读取数据时,会去先发起recvform一个命令,去尝试从内核上加载数据,如果内核没有数据,那么用户就会等待,此时内核会去从硬件上读取数据,内核读取数据之后,会把数据拷贝到用户态,并且返回ok,整个过程,都是阻塞等待的,这就是阻塞IO。
- 阻塞IO就是这1和2两个阶段都需要等待
阶段一:
- 用户进程尝试读取数据(比如网卡数据)
- 此时数据尚未到达,内核需要等待数据
- 此时用户进程也处于阻塞状态
阶段二:
- 数据到达并拷贝到内核缓冲区,代表已就绪
- 将内核数据拷贝到用户缓冲区
- 拷贝过程中,用户进程依然阻塞等待
- 拷贝完成,用户进程解除阻塞,处理数据
同步非阻塞IO
阶段一:
- 用户进程尝试读取数据(比如网卡数据)
- 此时数据尚未到达,内核需要等待数据
- 返回异常给用户进程
- 用户进程拿到error后,再次尝试读取
- 循环往复,直到数据就绪
阶段二:
- 将内核数据拷贝到用户缓冲区
- 拷贝过程中,用户进程依然阻塞等待
- 拷贝完成,用户进程解除阻塞,处理数据
- 可以看到,非阻塞IO模型中,用户进程在第一个阶段是非阻塞,第二个阶段是阻塞状态。虽然是非阻塞,但性能并没有得到提高。而且忙等机制会导致CPU空转,CPU使用率暴增。
IO多路复用
如果调用recvfrom时,恰好没有数据,阻塞IO会使CPU阻塞,非阻塞IO使CPU空转,都不能充分发挥CPU的作用。 如果调用recvfrom时,恰好有数据,则用户进程可以直接进入第二阶段,读取并处理数据。
IO多路复用依赖于文件描述符机制(File Descriptor)。
文件描述符
简称FD,是一个从0 开始的无符号整数,用来关联Linux中的一个文件。在Linux中,一切皆文件,例如常规文件、视频、硬件设备等,当然也包括网络套接字(Socket)。
网络模型可以利用一个线程监听多个FD,并在某个FD可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源。
IO多路复用的实现
-
什么是I/O多路复用?
- IO 多路复用是一种同步IO模型,实现一个线程可以监视多个文件句柄;
- 一旦某个文件句柄就绪,就能够通知应用程序进行相应的读写操作;
- 没有文件句柄就绪就会阻塞应用程序,交出CPU。
-
为什么有I/O多路复用机制?
-
没有之前有BIO和NIO两种模式
-
BIO
- 同步阻塞
- 服务端有多少线程只能监控多少任务
-
NIO
- 服务端每ac一个请求就加入集合,然后轮询使用recv接收数据,没有数据立即返回错误,每次轮询浪费cpu
-
IO多路复用在LInux系统下有三种实现方式:
- select
- poll
- epoll
- 其中select和poll是在数据准备好的时候将所有FD都发送给监听线程,在所有准备好的FD中去查找,效率为O(N)
- epoll则是将准备好的数据直接发送给对应的FD,时间复杂度为O(1)
IO多路复用-select方式
- select
- 监听的文件描述符fd_set上限1024个
- 每次需要把fd_set从用户态复制到内核态,select结束还需要再次拷贝到用户空间,耗费资源
- 在内核态需要遍历fd_set标记可读/可写,用户态读取也需要遍历,也就是两次遍历和两次复制操作,耗时。
- 轮询的方式检查事件是否就绪,时间复杂度O(N)
如下图所示:
IO多路复用-poll方式
IO流程:
- 创建pollfd数组,向其中添加关注的fd信息,数组大小自定义
- 调用poll函数,将pollfd数组拷贝到内核空间,转链表存储,无上限
- 内核遍历fd,判断是否就绪
- 数据就绪或超时后,拷贝pollfd数组到用户空间,返回就绪fd数量n
- 用户进程判断n是否大于0,大于0则遍历pollfd数组,找到就绪的fd
poll
- 解决了select的第一个问题 - 监听文件描述符的上限
- 使用了链表解决
- 还是存在第二个和第三个问题,甚至遍历的时间由于监听FD的增加还会增加耗时
IO多路复用模型-epoll函数
-
epoll 事件驱动
-
使用红黑树监听fd
- 只在乎活跃事件,不活跃的事件的连接数对其无影响,只有就绪才会触发事件回调,高效的查找事件是否重复
- 没有监听上限
-
双向链表保存就绪事件
- 找的时候返回链表中的就绪事件即可,时间复杂度O(1)
-
使用了mmap减少了内存复制的开销
-
LT(水平触发)(默认)和ET(边沿触发)
- LevelTriggered:简称LT,也叫做水平触发。只要某个FD中有数据可读,每次调用epoll_wait都会得到通知。
- EdgeTriggered:简称ET,也叫做边沿触发。只有在某个FD有状态变化时,调用epoll_wait才会被通知。ET需要保证每次都读完数据,以及读取时候的异常处理要求非常高,所以一般用LT。
-
零拷贝
首先介绍DMA的发展过程。
无 DMA 控制器 IO 过程
- 用户线程发起read()系统调用,从用户态转到内核态。
- 操作系统收到请求,CPU发出对应的控制指令给磁盘控制器,cpu释放执行其他任务。
- 磁盘控制器收到指令将对应的数据读取到磁盘控制器的内部缓冲区,然后产生中断通知CPU。
- CPU收到中断信号,将磁盘控制器缓冲区的数据读入寄存器,然后从寄存器读到内核缓冲区,这个过程CPU无法处理其他事情。
- 当内核缓冲区数据足够的时候,CPU将内核缓冲区的数据读入用户缓冲区。
DMA 控制器
-
如果 CPU 搬运的数据很小,那么问题并不大,但是如果需要拷贝大量数据肯定是不行的,因为 CPU 的资源十分宝贵。为了解决这个问题,于是出现了 DMA(Direct Memory Access,直接内存访问) 技术。
-
简单来说就是:在进行 I/O 设备和内存的数据传输的时候,数据搬运的工作全部交给 DMA 控制器,而 CPU 不再参与任何与数据搬运相关的事情,这样 CPU 就可以去处理别的事务。
DMA参与的拷贝过程如下图所示:
-
用户进程发起 read 调用,向操作系统发送 I/O 请求,进程进入阻塞状态;
-
操作系统收到请求后,CPU 发送一个请求给 DMA , CPU 释放执行其它任务。
-
DMA 进一步将 I/O 请求发送磁盘控制器,磁盘控制器收到指令后,开始准备数据并将数据放入磁盘控制器的内部缓冲区,然后产生一个中断。
-
DMA 收到中断信号后将磁盘控制器缓冲区的数据拷贝到内核缓存区(此时不占用 CPU 资源,CPU 可以执行其它任务)。
-
当 DMA 读取到足够多的数据到内核缓冲区后,发送中断信号给 CPU ,CPU 将内核缓冲区的数据拷贝到用户缓冲区中。
注:早期 DMA 只存在于主板上,现在 I/O 设备越来越多,数据传输需求也越来越复杂,所以每个 I/O 设备中都有自己的 DMA 控制器。
零拷贝的实现
这里直接参考 [小林coding的讲解](9.1 什么是零拷贝? | 小林coding (xiaolincoding.com))即可。
参考文献
- 小林coding
- 黑马Redis原理篇