提起Redis
,我们经常会说其底层是一个单线程模型,但这是不严谨的。Redis
单线程指的是网络请求模块使用了一个线程,即一个线程处理所有网络请求,其他模块该使用多线程,仍会使用了多个线程。既然是单线程模型,那么CPU
不是Redis
的瓶颈。Redis
的瓶颈最有可能是机器内存或者网络带宽。
Redis
基于Reactor
模式开发了自己的网络事件处理器,称之为文件事件处理器(File Event Hanlder
)。文件事件处理器由Socket
、IO
多路复用程序、文件事件分派器(dispather
),事件处理器(handler
)四部分组成,文件事件处理器的模型如下所示:
IO
多路复用程序会同时监听多个socket
,当被监听的socket
准备好执行accept
、read
、write
、close
等操作时,与这些操作相对应的文件事件就会产生。IO
多路复用程序会把所有产生事件的socket
压入一个队列中,然后有序地每次仅一个socket
的方式传送给文件事件分派器,文件事件分派器接收到socket
之后会根据socket
产生的事件类型调用对应的事件处理器进行处理。
文件事件处理器分为几种:
set
、lpush
等;set
、get
等命令的结果;事件种类:
AE_READABLE
:与两个事件处理器结合使用。
socket
的AE_READABLE
事件关联起来;AE_READABLE
事件关联起来;AE_WRITABLE
:当服务端有数据需要回传给客户端时,服务端将命令回复处理器与socket
的AE_WRITABLE
事件关联起来。Redis
的客户端与服务端的交互过程如下所示:
内核态拥有完全的底层资源控制权限,可以执行任何的CPU
指令,访问任何内存地址,其占有的处理机是不允许被抢占的。
用户程序是运行在操作系统之上,这些程序运行时称之为用户态,用户态下不能直接访问底层硬件和内存地址,只能通过委托系统调用的方式来访问底层硬件和内存。
从用户态切换到内核态有三种方式:
fork()
指令实际上就是执行了一个创建新进程的系统调用。系统调用的机制其核心在于**使用了操作系统为用户特别开放的一个中断来实现的,例如Linux
的int 80h
中断;CPU
发出相应的中断信号。这时CPU
会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序。如果先前执行的是用户态下的指令,那么这个切换过程就是用户态转为内核态。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作;CPU
在执行运行处于用户态的程序时,发生了一些不可知的异常,这个时候就会触发由当前运行进行切换到处理此异常的内核相关程序中,也就是转到了内核态,比如缺页异常;这三种是用户态切换到内核态的主要方式,系统调用是主动的,后面两种是被动的。
Linux
的整体架构图如下所示:
同步/异步关注的是消息通信机制。
同步:所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回。等前一件做完了才能做下一件事。
异步:异步的概念和同步相对。当一个异步过程调用发出后,调用者若不能立刻得到结果,此时可以直接返回然后执行其他任务,等到获得了结果之后通过状态、通知或者回调等手段通知调用者。
同步、异步一般发生在不同的线程/进程之间,如Thread1
和Thread2
是同步执行还是异步执行的。
阻塞和非阻塞关注的是程序在等待调用结果时的状态。
阻塞: 阻塞调用是指调用返回之前,当前线程会被挂起,只有当调用得到结果后才返回。
非阻塞:与阻塞相反,非阻塞调用是指在不能立即得到结果之前,该函数不会将当前线程阻塞,而是立即返回。
IO
一般分为磁盘IO和网络IO,这里我们主要关注网络IO。一次完整的网络IO
过程如下所示:
从上图可以看出,数据无论从网卡到用户空间还是从用户空间到网卡都需要经过内核。
1 阻塞IO模型
当应用程序调用一个 IO
函数,其底层会委托操作系统的recvfrom()
去完成,当数据还没有准备好时,revfrom
会一直阻塞,等待数据准备好。当数据准备好后,从内核拷贝到用户空间,recvfrom
返回成功,IO
函数调用完成。过程如下所示:
阻塞IO
模型的优点是编程简单,但缺点是需要配合大量线程使用。应用进程没接收一个连接,就需要为此连接创建一个线程来处理该连接上的读写任务。
调用进程在等待数据的过程中不会被阻塞,而是会不断地轮询查看数据有没有准备好。当数据准备好后,将数据从内核空间拷贝到用户空间,完成IO
函数的调用。等待数据的过程是非阻塞的,但数据拷贝时仍是阻塞的。过程如下所示:
非阻塞io
的优点在于可以实现使用一个线程同时处理多个连接的需求,减少线程的大量使用。缺点在于要不断地去轮询检查数据是否准备好,比较耗费CPU
。
为了解决非阻塞IO
不断轮询导致CPU
占用升高的问题,出现了IO
复用模型。IO
复用中,使用其他线程帮助去检查多个线程数据的完成情况,提高效率。
Linux
中提供了select
、poll
和epoll
三种方式来实现IO
复用。一个线程可以对多个IO
端口进行监听,当有读写事件产生时会分发到具体的线程进行处理。过程如下所示:
IO
复用只需要阻塞在select
,poll
或者epoll
,可以同时处理和管理多个连接。缺点是当select
、poll
或者epoll
管理的连接数过少时,这种模型将退化成阻塞IO
模型。并且还多了一次系统调用:一次select
、poll
或者epoll
一次recvfrom
。
应用程序可以创建一个信号驱动程序SIGIO
,当数据没有处理好时,应用程序继续运行,不会被阻塞。当数据准备好之后,操作系统向应用程序发送信号,之后信号驱动程序就会执行,在信号处理函数中调用 IO
函数处理数据。过程如下所示:
信号驱动IO
模型的优点在于非阻塞,缺点在于串行处理信号驱动程序,当前一个SIGIO
没有被处理的情况下,后一个信号也不能被处理。在信号量大的时候会导致后面的信号不能被及时感知。
相比于同步IO
,异步IO
不是顺序执行的。应用进程在执行aio_read
系统调用之后,无论数据是否准备好,都会直接返回给用户进程,然后应用进程可以去做别的事情。当数据准备好之后,内核直接复制数据给用户进程,然后内核向进程发送通知。过程如下:
信号驱动IO
模型中内核通知应用进程数据何时准备好,而在异步IO
模型中内核将数据复制完成之后告知应用进程IO
操作已完成。
在异步IO
模型中,应用进程调用aio_read
以及数据被拷贝到用户空间这两个过程都是非阻塞的。
IO
模型公有五种,前四种模型区别在于第一部分,即系统调用,但是第二部分都是一样的,即将数据从内核空间拷贝到用户空间这个过程,进程阻塞于redvfrom
的调用。而最后一种,异步IO
模型,在系统调用和数据拷贝过程都是非阻塞的。