栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

Redis的启动,事件和epoll网络通信

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Redis的启动,事件和epoll网络通信

Redis的启动

redis中server.c中的main函数负责启动Redis服务。启动流程大致如下:

  1. 启动前的一系列检查动作,包括是否是sentinel启动,配置项初始化,ACL,rdb/AOF等
  2. 读取配置项(一系列动作)
  3. 打印启动日志
  4. 调用initServer函数初始化Redis运行时数据
  5. 。。。
  6. 启动事件循环器(提供服务)
  7. 服务停止

下面是initServer函数的主要流程:

  1. 设置一些信号处理函数, 例如SIGNT CANCEL
  2. 创建事件循环器
  3. 开启接受文件事件的端口,TCP Socket,TLS Socket,UnixSocket。如果都没有配置就会报错
  4. 初始化数据库
  5. 创建时间事件,执行函数是serverCorn,负责处理Redis中的定时任务(过期数据,RDB等)
  6. 为文件事件注册处理函数(acceptTcpHandler,acceptTLSHandler,acceptUnixHandler)
  7. 注册事件循环器的狗子函数,事件循环器在每次阻塞前后都会调用钩子函数
  8. 打开AOF
  9. Cluster初始化
Redis事件机制

Redis是一个事件驱动的程序,主要处理文件事件和时间事件。

  • 文件事件:利用IO复用机制,监听Socket等文件描述符上发生的事件(由客户端或其他Redis服务器发送网络请求触发)
  • 时间事件:负责完成Redis内部定时任务

Redis事件机制实现的代码在ae.h,ae.c中,分为ae抽象层和I/O复用层上下两层,其中ae抽象层基于I/O服用层。由于I/O复用涉及操作系统内核,不同的操作系统提供了不同的API接口,Redis也针对不同的操作系统做了适配。最常见的是Linux操作系统的epoll。

aeEventLoop是Redis中的事件循环器,负责管理所有的事件。几个比较重要的字段有

aeFileEvent *events:已注册的文件事件表

aeFiredEvent *fired:已就绪的文件事件表

aeTimeEvent *timeEventHead :时间事件表

ae层I/O层Linux系统调用

Redis启动

创建事件循环器

aeCreateEventLoop()

aeApiCreate()epoll_create()

添加事件

监听端口

aeCreateFileEvent()aeApiAddEvent()epoll_ctl()
运行时(循环调用)aeProcessEvents()aeApiPoll()

epoll_wait()

epoll_ctl()

  • 在Redis启动时,initServer()函数会调用 aeCreateEventLoop()函数,创建事件循环器,在Linux内核层面就是创建了一个epoll实例
  • 随后会调用aeCreateFileEvent()函数,监听相应的Socket文件描述符。在内核层面就是让epoll实例绑定了相应的端口
  • 在运行时,会循环调用aeProcessEvents()函数,提供服务,直至redis关闭。
  1. 首先计算最大阻塞时间。
    如果有时间事件,就用最近的时间事件的事件-当前的时间作为阻塞时间
    如果没有时间事件,就检查是否允许阻塞,如果允许,就阻塞到有文件事件就绪。如果不允许,就不断询问系统是否有文件事件就绪。
  2. 进程阻塞前beforeSleep()函数
  3. 进程阻塞,aeApiPoll()函数实现,底层调用epoll_wait()函数。
  4. 进程阻塞后afterSleep()函数
  5. epoll_wait()函数返回值是已就绪的文件描述符数量k,如果数量大于0代表已经有事件就绪。由于aeEventLoop中的event字段是文件事件起始文件描述符的地址,epoll底层会将已就绪的事件进行重排,放在前面,所以只需要从头开始遍历k个就可以了,时间复杂度是O(k)。
  6. 先处理文件事件,根据不同的事件类型,调用不同的事件处理器处理。
  7. 再处理时间事件
多路IO复用

 IO分为BIO,NIO,AIO。分别为同步阻塞,同步非阻塞,异步非阻塞。

需要注意的是同步/异步是针对调用方的,当调用方发起请求,需要主动询问服务端是否可以连接就是同步IO。若调用方发起请求之后,马上做自己的其他事情,不必等待结果。当服务端准备好连接时再通知调用方,这种就是异步IO。

阻塞/非阻塞是针对服务端的。首先网络I/O是不需要CPU的,CPU只能和内存交互。当客户端发起请求,要先发送到服务端的缓冲区的,由于IO速度很慢,服务端的CPU需要等到IO就绪才能读取数据,其余时间都只能等着。这就是阻塞。相反,服务端的CPU只在缓冲区就绪时才读取数据,其余时间可以处理别的事情,这就是非阻塞。

BIO是传统的阻塞IO,服务端一个线程对应一个套接字。当客户端发起连接时,客户端要等到服务端socket套接字就绪,服务端要等待缓冲区就绪,所以客户端和服务端都是阻塞的,效率很低。

为了解决这个问题,可以采用服务端多线程的BIO,就是让不同的线程分别处理不同的socket连接,这样显然会提高服务端的处理速度,但是对服务端的性能要求很高,因为多线程是要耗费计算机资源的。

NIO。NIO分为分多种,包括非阻塞I/O模型,I/O复用模型,信号驱动I/O模型等。

  • 非阻塞IO模型:服务端进程会循环地询问系统内核,缓冲区数据是否准备好,直到数据准备为止。这段时间进程时非阻塞的。
  • IO多路复用:服务端用一个管理进程来监控所有的socket连接,如果某一个缓冲区数据准备好,监控进程就会修改socket文件描述符的状态,将他标为就绪。工作进程就会处理已经就绪的连接数据。因此,服务端的工作进程是可以不用阻塞于某一个socket连接的。
  • 信号驱动式I/O模型:我们可以用信号,让内核在描述符就绪时发送SIGIO信号通知我们

 AIO:

在IO中共有三个进程。一个时客户端,一个是服务端的用户进程,另一个是服务端的系统内核。

考虑以下两个场景:

客户端发送连接请求,一定要通过服务端内核才能传输数据,所以客户端是调用方。

服务端用户进程要处理客户端发过来的数据,由于数据是保存在缓冲区的,缓冲区是否就绪是由内核来判断的,所以,用户进程是调用方。

无论哪种情况,用户内核都是服务提供方,若为异步IO,那么用户进程和客户端都不会阻塞,因此只有异步非阻塞IO。很显然异步IO有明显的优势,但是目前的操作系统内核对AIO的支持还不是很完善。目前普遍使用就是NIO。Redis使用的就是IO多路复用

select,poll ,epoll

这三个函数都是Linux系统提供的系统调用函数,用于监测socket连接的文件描述符是否就绪。

最开始出现的就是select,这个函数的实现是在客户端构造了一个1024bit长度的bitmap。每一位代表一个fd,0代表未就绪,1代表就绪。用户进程在系统调用时,将bitmap拷贝到内核空间,内核对每一个文件描述符进行轮询,置位,再返回给用户进程。用户进程再依次轮询,从而知道哪个socket连接就绪。

但是这有明显的缺点:

  1. bitmap长度固定1024,只能监测1024个连接,对于某些高负载服务器来讲,连接数都上万,太少了。
  2. bitmap不可以复用,每次都需要重新申请。
  3. bitmap需要从用户进程拷贝到内核
  4. 用户进程每次都需要遍历bitmap,时间复杂度位O(1024)

poll 在select进行了改进,他不再使用bitmap,而是使用event的结构体数组,增加了revent字段。这样就没有长度限制了,而且数据也可以复用。但是问题3,4依然没有解决

epoll完美解决了select的全部问题。

epoll在poll的基础上,通过共享内存的方式,避免了revent数组的拷贝,同时,在内核层面上对event结构体的数据结构进行了优化。

首先使用了红黑树的结构来存储event结构体,这样就可以在O(log n)的时间复杂度下对fd进行增删改查。

然后,对fd进行了重排,将就绪的fd放到等待列表的前端,并返回最大的fd就绪数量k,这样用户进程就不需要轮询了,只需要遍历前K个fd就可以了。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/970338.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号