一个进程的内存空间可以分为用户空间和内核空间
当进行IO从网络中接受数据时,需要先通过内核态将数据接受到内核缓冲区中,然后将数据再传到用户缓冲区中,才能对这些数据进行处理
所以从外部读数据的时间消耗主要可以分成两个部分:等待数据就绪+读取数据
等待数据就绪:包括内核空间从硬件将数据读取到内核缓冲区的时间
读取数据:将内核缓冲区的数据拷贝到用户缓冲区的时间
阻塞IO:
在用户向内核空间请求数据的时候,如果没有数据,也在等待,直到有数据返回
非阻塞IO:
如果内核空间没有数据的话,不等立即返回,但是会一直问,直到问到数据
但是实际上提升也不多,因为阻塞IO一直等,但是非阻塞的也在一直问也没干啥,而且忙等可能会导致CPU空转,CPU使用率暴增
IO多路复用:
不管是阻塞IO还是非阻塞IO,调用recvfrom的时候,如果数据没有就绪要么等要么一直问询
如果数据就绪,就直接接受数据
思考这样的一个场景:如果一个线程监听了多个socket请求,由于是单个线程,所以只能依次处理这个socket请求,如果正在处理的这个socket数据还没有就绪,那就被阻塞,后面的socket就算数据就绪了也不能处理数据,所以效果很差
怎么解决:多线程(上下文开销就会增大)、先处理就绪的,没就绪的就不要等待处理
但是怎么知道有没有就绪?
Linux通过FD来标识就绪情况
FD(文件描述符):是一个从0开始递增的无符号整数,用来关联一个Linux中的一个文件
(在linux中,一切皆文件,例如常规文件、视频、硬件设备、网络套接字等,也就是每个socket都会对应一个FD)
现在流程就变成这样的,用户线程先调用selcet去查询这个线程监听的fd,内核去检查这些fd对应的socket有没有数据就绪,如果所有socket都没有就绪,那就等着,这个时候用户线程也会阻塞,如果有就绪的,那就把就绪的fd返回,然后用户线程再通过recvfrom就调用这个fd,就可以保证这个fd对应的数据一定是就绪的
recvfrom只能监听一个fd,当前fd没有就绪不代表其他fd没有就绪
selcet是一次性传多个fd进去,只要有任意一个就绪就会传回来
虽然没有数据就绪的时候用户应用也是阻塞的,但是实际上避免了很多的无效阻塞等待
select:只知道有fd就绪了,但是不知道是哪个fd就绪了,挨个去问看谁就绪了
poll:同selcet,
epoll:会直接通知有fd就绪的同时就把就绪的fd写入用户空间,避免了对fd的遍历过程
selcet:
一次执行:
用户创建fd_set readfds(这里只有读事件,所以只监听readfds)。fd_set 是一个long类型的数组,长度为1024/32,但是一个long有32位,所以一个fd_set还是有1024位
用户将要监听的fd对应的位置1(比如要监听fd=1,2,5,就把这个fd_set的第1,2,5位置1)
执行selcet(5+1, readfds, null, null,3)
将readfds拷贝到内核态
内核遍历readfds,如果没有就绪的,就休眠
等待数据就绪被唤醒或者超时
内核将readfds中就绪的置1,其他的置0
内核将修改后的readfds拷贝回用户空间,用户空间遍历找到就绪的fd,再yongrecvfrom读取数据
缺点:
每次都将readfds拷贝到内核空间,结束之后还得再拷贝回去
无法得知哪个fd就绪,只能遍历readfds
readfds的数量不超过1024
poll:
没有具体的数据划分,这里只有一个数组,但是数组中每个元素都有个类型
一次执行:
创建pollfd数组,添加关注的fd信息,数组大小自定义
调用poll函数,将pollfd数组拷贝到内核空间,将数组转成链表
内核遍历fd,判断是否就绪
有数据就绪或者超时之后把pollfd数组返回到用户空间,返回就绪fd数量n
用户判断就绪数量是否大于0
大于0就遍历数组,找到就绪的fd
改进和缺点:
相对selcet,实际上只改进了数量限制
但是数量没有限制的话,每次便利消耗时间更久,性能反而下降
epoll:
用户态首先会创建epoll实例,在内核态就会创建一个rb_root和一个list_head
通过epoll_ctl添加要监听的fd,关联callback,内核态就会把这个fd添加到rb_root的红黑树上
就绪的时候就会把这个fd添加到list_head上
当epoll_wait就会判断list_head上有没有节点,也就是有没有就绪的fd
所以内核态会通过一个链表记录就绪的fd,避免了遍历fd结合来得到就绪的fd
信号驱动IO
用户建立一个信号处理函数,调用之后立即返回,等内核有fd就绪好了之后递交信号,用户态收到信号再recvfrom进行系统调用接受数据
缺点:如果有大量IO操作,信号较多,信号处理函数不能及时处理可能导致信号队列溢出
内核态与用户态频繁交互信号性能低
异步IO:
异步IO没有执行recvfrom这个操作
用户调用aio_read执行系统调用后马上返回,等内核数据就绪之后直接将数据拷贝到用户态
异步在两个阶段都是不阻塞的
总结:
redis网络模型:
redis在核心业务处理部分是单线程,但是整个redis是多线程
(这里注意像关闭文件、AOF 刷盘、释放内存是后台线程,但是像AOF重写、bgsave保存RDB数据快照是子进程)
为什么redis要选择单线程:
1.最最重要的原因就是,redis是基于内存的,执行速度非常非常快,多线程不会带来巨大的性能提升,反而会导致上下文切换带来不必要的开销
2.多线程会面临线程安全问题,就需要引入锁等安全手段,实现复杂度高,性能也会有影响
流程:
首先有一个serverSocket(用来监听别人对这个服务端的连接),然后这个sercerSocket会对应一个fd,把这个fd注册到监听的红黑树(eventloop)上,然后等待就绪(在等待就绪之前会遍历客户socket,监听fd写事件,绑定【写处理器】)
当serverSocket有可读事件的时候(实际上就是有客户端连接上了这个服务端),就会触发【连接应答处理器】,会接受客户端的请求,得到客户端socket的fd,然后注册到eventloop上,然后继续等待事件
现在来的就可能是serverSocket的可读事件(建立新的连接),也可能是客户socket的可读事件,如果是客户socket的可读事件的话,就交给【命令请求处理器】去处理(相当于客户端的请求来了,要去处理客户端的请求)
【命令请求处理器】会把每个客户请求的请求数据写到缓冲区中,然后解析缓冲区中的命令转为redis命令,再处理解析好的命令,再根据命令去找对应的处理函数(每个命令都有一个对应的函数,比如set就对应一个setCommand函数),再将结果返回回去(先把返回的数据写到缓冲区中,然后放到一个队列中)
绑定了写处理器之后,再来的客户socket写事件就会交给【写处理器】,将队列中的数据取出来写到客户端的socket中
简化一下:
就是redis通过一个IO多路复用+事件派发机制,监听socket来的命令,如果是serversocket可读事件的话,就给【连接应答处理器】处理,如果是客户socket可读事件的话,就交给【命令请求处理器】,如果是客户端可写事件的话,就交给【命令回复处理器】去处理解决
性能瓶颈:从IO读数据和向IO写数据
所以redis6.0引入的多线程就是在【命令请求处理器】读取解析命令的时候和【命令回复处理器】将数据写回的时候使用多线程