用户空间和内核空间
任何Linux发行版,其系统内核都是Linux。我们的应用都需要通过Linux内核与硬件交互 , 为了避免用户应用导致冲突甚至内核崩溃,用户应用与内核是分离的:
-
进程的寻址空间会划分为两部分:内核空间、用户空间
-
用户空间只能执行受限的命令(Ring3),而且不能直接调用系统资源,必须通过内核提供的接口来访问
-
内核空间可以执行特权命令 (Ring0),调用一切系统资源
io操作样例(Linux系统为了提高l0效率,会在用户空间和内核空间都加入缓冲区):
-
写数据时,要把用户缓冲数据拷贝到内核缓冲区,然后写入设备
-
读数据时,要从设备读取数据到内核缓冲区,然后拷贝到用户缓冲区
阻塞IO
阻塞IO就是两个阶段都必须阻塞等待 :
非阻塞IO
lO多路复用
无论是阻塞10还是非阻塞10,用户应用在一阶段都需要调用recvfrom来获取数据,差别在于无数据时的处理方案:
-
如果调用recvfrom时,恰好没有数据,阻塞10会使进程阻塞,非阻塞10使CPU空转,都不能充分发挥CPU的作
-
如果调用recvfrom时,恰好有数据,则用户进程可以直接进入第二阶段,读取并处理数据
文件描述符(File Descriptor)
简称FD,是一个从0 开始递增的无符号整数,用来关联Linux中的一个文件。在Linux中,一切皆文件,例如常规文件、视频、硬件设备等,当然也包括网络套接字 (Socket)
IO多路复用:是利用单个线程来同时监听多个FD,并在某个FD可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源。
不过监听FD的方式、通知的方式又有多种实现,常见的有 : select poll epoll 。
select和poll只会通知用户进程有FD就绪,但不确定具体是哪个FD,需要用户进程逐个遍历FD来确认 。 epoll则会在通知用户进程FD就绪的同时,把已就绪的FD写入用户空间 。
lO多路复用-select
fd_set : 记录要监听的fd集合,及其对应状态
select函数: 用于监听多个fd的集合
select模式存在的问题:
-
需要将整个fd_set从用户空间拷贝到内核空间,select结束还要再次拷贝回用户空间
-
select无法得知具体是哪个fd就绪,需要遍历整个fd_set
-
fd_set监听的fd数量不能超过1024
lO多路复用-poll
l0流程:
-
创建pollfd数组,向其中添加关注的fd信息,数组大小自定义
-
调用poll函数,将pollfd数组拷贝到内核空间,转链表存储,无上限
-
内核遍历fd,判断是否就绪
-
数据就绪或超时后,拷贝pollfd数组到用户空间,返回就绪fd数量n
-
用户进程判断n是否大于0
-
大于0则遍历pollfd数组,找到就绪的fd
与select对比:
-
select模式中的fd set大小固定为1024,而pollfd在内核中采用链表,理论上无上限
-
监听FD越多,每次遍历消耗时间也越久,性能反而会下降
lO多路复用-epoll
epoll模式是对select和poll的改进,它提供了三个函数 :
执行流程 :
基于epoll模式的web服务的基本流程如图:
数据就绪或超时后,拷贝已经就绪的 fd 到用户空间,返回就绪fd数量n
总结
select模式存在的三个问题:
- 能监听的FD最大不超过1024
- 每次select都需要把所有要监听的FD都拷贝到内核空间
- 每次都要遍历所有FD来判断就绪状态
poll模式的问题:
- poll利用链表解决了select中监听FD上限的问题,但依然要遍历所有FD,如果监听较多,性能会下降
epoll模式优点:
- 基于epoll实例中的红黑树保存要监听的FD,理论上无上限,而且增删改查效率都非常高,性能不会随监听的FD数量增多而下降
- 每个FD只需要执行一次epoll_ctl添加到红黑树,以后每次epol_wait无需传递任何参数,无需重复拷贝FD到内核空间
- 内核会将就绪的FD直接拷贝到用户空间的指定位置,用户进程无需遍历所有FD就能知道就绪的FD是谁
信号驱动IO
信号驱动10是与内核建立SIGI0的信号关联并设置回调,当内核有FD就绪时,会发出SIGI0信号通知用户,期间用户应用可以执行其它业务,无需阻塞等待。
问题 :
当有大量I0操作时,信号较多,SIGIO处理函数不能及时处理可能导致信号队列溢出而且内核空间与用户空间的频繁信号交互性能也较低
异步IO
异步10的整个过程都是非阻塞的,用户进程调用完异步API后就可以去做其它事情,内核等待数据就绪并拷贝到用户2间后才会递交信号,通知用户进程
异步IO模型中,用户进程在两个阶段都是非阻塞状态
Redis网络模型
Redis的核心业务部分 (命令处理),是单线程 , 整个Redis 就是多线程
在Redis版本迭代过程中,在两个重要的时间节点上引入了多线程的支持:
-
Redis v4.0: 引入多线程异步处理一些耗时较长的任务,例如异步删除命令unlink
-
Redis v6.0:在核心网络模型中引入 多线程,进一步提高对于多核CPU的利用率
Redis单线程好处:
-
抛开持久化不谈,Redis是纯内存操作,执行速度非常快,它的性能瓶颈是网络延迟而不是执行速度,因此多线程并不会带来巨大的性能提升
-
多线程会导致过多的上下文切换,带来不必要的开销
-
引入多线程会面临线程安全问题,必然要引入线程锁这样的安全手段,实现复杂度增高,而且性能也会大打折扣
Redis 单线程网络模型流程 :
初始服务 :
-
监听TCP端口,创建ServerSocket(epoll实例),并得到SSFD
-
注册连接处理器
- 将SSFD注册到epoll实例上 并绑定读处理器(处理客户端的读事件)
- 注册前置处理器 beforeSleep,绑定命令回复处理器
开始循环监听事件:
- 调用前置处理器 beforeSleep
- 遍历队列中的client,监听FD写事件绑定写处理器sendReplyToclient,将数据通过写处理器sendReplyToclient发送会客户端
-
等待FD就绪,类似epoll_wait
-
遍历处理就绪的FD,调用对应的处理器
-
ServerSocket的FD可读(即客户端连接):接受客户端请求,调用对应的处理器 将对应的FD注册到epoll实例上
-
客户端连接FD可读: 读取请求数据,写会响应。
-
获取当前客户端,客户端中有缓冲区用来读和写
-
将请求数据写入c->queryBuf (输入缓冲区)
-
解析queryBuf数据转为Redis命令
-
执行命令尝试把结果写到 c-buf 客户端写缓存区
-
如果c->buf写不下,则写到 c->reply,这是一个链表,容量无上限
-
将客户端添加到server.clients_pending_write这个队列,等待被写出
数据读处理器:
接收socket连接,获取FD
创建connection,关联fd
监听socket的FD读事件,并绑定读处理器readQueryFromClient