redis源码分析之IO多路复用

文章目录

    • 1、简述
    • 2、多路复用的三个函数
    • 3、创建epoll实例
    • 4、绑定端口、监听端口
    • 5、向epoll实例注册连接事件
    • 6、从epoll实例中获取就绪的事件

1、简述

众所周知,redis是一款抗高并发的利器,据官方压测,单机可达10万qps。但背后实际处理命令的线程只有一条,这听上去其实挺匪夷所思的,因为在我们的日常开发中,说到高并发,多线程是一个非常常用的解决方案。那redis凭什么靠一条线程,就能支持高并发呢?最主要的原因,就是标题所说的IO多路复用,IO多路复用是怎么做的呢?这是老八股了,IO多路复用,背后依赖的是多路复用的函数,有select、poll、epoll,linux默认使用的是epoll函数,redis把客户端连接通过epoll函数给到内核,内核监听到连接有可读写的事件,就将该事件返回redis进行处理。那具体的实现细节呢?redis怎么给的内核,内核又怎么返回的?

2、多路复用的三个函数

epoll函数由3个函数组合来完成多路复用这件事。分别是:
epoll_create、epoll_ctl、epoll_wait
1)、epoll_create:创建epoll实例
2)、epoll_ctl:将连接对应的socket描述符注册到epoll实例中
3)、epoll_wait:获取epoll实例中可读写的描述符
画一个简单的流程图串一下这三个函数的作用
请添加图片描述
从图中可以看出,redis在启动的时候,先是通过epoll_create函数创建epoll实例,然后绑定端口、监听端口,然后通过epoll_ctl函数注册连接事件,最后会搞一个死循环,通过epoll_wait函数获取可读写的事件(每一个事件对应的都是一个可读写的客户端连接)
铺垫完上面的流程,我们看一下源码。redis的启动源码在server.c文件的main方法中,main方法是redis启动的入口,其中有很多流程,但是我们不要全部都看,就看图中流程涉及到的逻辑

3、创建epoll实例

首先是通过内核提供的epoll_create函数创建epoll实例,这个流程入口在initServer方法中.

void initServer(void) {......//创建epoll实例server.el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR);if (server.el == NULL) {serverLog(LL_WARNING,"Failed creating the event loop. Error message: '%s'",strerror(errno));exit(1);}......
}

aeCreateEventLoop是创建epoll实例的入口,我们进入这个方法。

aeEventLoop *aeCreateEventLoop(int setsize) {......if (aeApiCreate(eventLoop) == -1) goto err;......
}

其中又调用了一个aeApiCreate方法,这个方法是对epoll_create函数做了一层封装,我们继续进入aeApiCreate方法。

static int aeApiCreate(aeEventLoop *eventLoop) {......//创建epoll实例//这里的1024并不是说epoll函数只能监听1024个描述符.因为在2.6.8内核之后,内核维护的是一个动态的队列,理论上我们可以一直添加描述符state->epfd = epoll_create(1024); /* 1024 is just a hint for the kernel */......
}

这里就看到了我们想找的epoll_create函数。这种只看主流程的源码阅读方法,很容易能得到一些结论,也很容易坚持下去。

4、绑定端口、监听端口

创建完epoll实例后,接下来就是绑定端口、监听端口。
这部分的代码也是在initServer方法中,就在创建epoll实例的下方

void initServer(void) {......//创建epoll实例server.el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR);if (server.el == NULL) {serverLog(LL_WARNING,"Failed creating the event loop. Error message: '%s'",strerror(errno));exit(1);}server.db = zmalloc(sizeof(redisDb)*server.dbnum);......//绑定、监听端口if (server.port != 0 &&listenToPort(server.port,server.ipfd,&server.ipfd_count) == C_ERR)exit(1);......
}

绑定、监听端口的逻辑在listenToPort方法中,该方法的入参有3个值,第一个就是要绑定、监听的端口,默认是6379。第二个值是描述符,第三个是描述符的数量。这个时候,后面这两个参数还没值,需要到listenToPort方法中赋值。

int listenToPort(int port, int *fds, int *count) {......//绑定IPV6fds[*count] = anetTcp6Server(server.neterr,port,NULL,server.tcp_backlog);......//绑定IPV4fds[*count] = anetTcpServer(server.neterr,port,NULL,server.tcp_backlog);......(*count)++;
}

所以最终fds数组一共赋值2个值。count赋值2

5、向epoll实例注册连接事件

这个逻辑还是在initServer方法中。server.ipfd_count的值就是上面的那个count值,是2。所以这个循环会执行2次,注册2个连接事件,一个IPV4、一个IPV6
aeCreateFileEvent,是一个非常重要的方法,是用来创建事件的。该方法有5个入参。
第一个是redis对应epoll实例的结构体,第二个是需要注册的描述符,第三个是需要注册的事件类型,第四个是事件触发后的回调函数,最后一个是客户端数据。我们是注册连接事件,所以不会有客户端数据,此时客户端还没有连接redis

void initServer(void) {......//注册连接事件for (j = 0; j < server.ipfd_count; j++) {......if (aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE,acceptTcpHandler,NULL) == AE_ERR)......}......
}

我们进入aeCreateFileEvent方法,

int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,aeFileProc *proc, void *clientData)
{......//aeApiAddEvent函数内部调用epoll_ctl函数if (aeApiAddEvent(eventLoop, fd, mask) == -1)return AE_ERR;......//将acceptTcpHandler回调函数挂到当前连接事件上if (mask & AE_READABLE) fe->rfileProc = proc;if (mask & AE_WRITABLE) fe->wfileProc = proc;......
}

aeCreateFileEvent主要就是做两件事,注册连接事件、给事件挂回调函数,回调函数就是acceptTcpHandler。aeApiAddEvent是对epoll_ctl函数的封装。我们进入其中看一下

static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {......//调用epoll的epoll_ctl函数注册事件,一共4个参数。//1、epoll实例//2、要执行的操作类型,添加事件还是修改修改事件。第一次肯定是添加事件//3、要监听的文件描述符//4、epoll_event类型变量if (epoll_ctl(state->epfd,op,fd,&ee) == -1) return -1;......
}

这里,我们就看到了epoll_ctl函数。

6、从epoll实例中获取就绪的事件

这个获取就绪事件的动作,是在main方法的aeMain函数中。

int main(int argc, char **argv) {......//执行aeMain函数开启事件循环处理框架aeMain(server.el);......
}

我们进入aeMain函数。

void aeMain(aeEventLoop *eventLoop) {//只要redis实例没有停止,while循环就会一直执行eventLoop->stop = 0;//redis服务是否停止的标志,如果stop值变为1,说明redis服务停止了while (!eventLoop->stop) {......aeProcessEvents(eventLoop, AE_ALL_EVENTS|AE_CALL_AFTER_SLEEP);}
}

我们看到获取就绪的事件函数是aeProcessEvents,我们进入其中看一下

int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{......//调用多路复用APInumevents = aeApiPoll(eventLoop, tvp);......
}

可以看到一个aeApiPoll函数,该函数是对epoll_wait函数的封装,我们继续进入aeApiPoll函数。

static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {aeApiState *state = eventLoop->apidata;int retval, numevents = 0;//等待有可读写的事件发生.返回值为可读写的事件数量retval = epoll_wait(state->epfd,state->events,eventLoop->setsize,tvp ? (tvp->tv_sec*1000 + tvp->tv_usec/1000) : -1);if (retval > 0) {int j;//获得监听到的事件数量numevents = retval;//针对每一个就绪的事件进行处理for (j = 0; j < numevents; j++) {//保存事件信息int mask = 0;//获取到当前就绪的这个事件struct epoll_event *e = state->events+j;//EPOLLIN代表epoll模型的读事件,这一行代码的意思是将epoll的读事件映射到redis事件驱动框架的读事件if (e->events & EPOLLIN) mask |= AE_READABLE;//EPOLLOUT代表epoll模型的写事件,这一行代码的意思是将epoll的写事件映射到redis事件驱动框架的写事件if (e->events & EPOLLOUT) mask |= AE_WRITABLE;//EPOLLERR:错误事件,表示文件描述符对应套接字出错if (e->events & EPOLLERR) mask |= AE_WRITABLE;if (e->events & EPOLLHUP) mask |= AE_WRITABLE;//将epoll模型中已就绪的描述符映射到redis事件循环框架的就绪事件数组中eventLoop->fired[j].fd = e->data.fd;//给已就绪的事件设置事件类型eventLoop->fired[j].mask = mask;}}return numevents;
}

在其中,我们看到了epoll_wait函数,epoll_wait一共四个入参。
第一个是:要监听的描述符集合,第二个是要监听的事件类型,第三个是要监听的描述符数量,第四个是等待结果返回的超时时间
返回了结果后,下面的逻辑就是处理这个就绪的事件,这个方法是redis IO多路复用的关键所在,redis不停的接收客户端请求,这个方法是主要逻辑,我给每一行代码都加了注释,可以细看一下。
redis的这部分多路复用逻辑写的很清晰,可以认真梳理一下,对多路复用的原理会有更清晰的认识。
文章参考了极客时间的redis源码课程《redis源码剖析与实战》,文章写的挺好,有兴趣的小伙伴可以去看看。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/160721.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

园区网真实详细配置大全案例

实现要求&#xff1a; 1、只允许行政部电脑对全网telnet管理 2、所有dhcp都在核心 3、wifi用户只能上外网&#xff0c;不能访问局域网其它电脑 4、所有接入交换机上bpdu保护 5、只允许vlan 10-40上网 5、所有接入交换机开dhcp snoop 6、所有的交换机指定核心交换机为ntp时间服务…

MySQL 外连接和内连接的查询优化怎么做?

目录 1. 表连接方式的分类和需要注意的细节 2. 表连接时底层做了什么事&#xff1f; 3. 左外连接优化方案 4. 内连接优化方案 1. 表连接方式的分类和需要注意的细节 多表连接查询&#xff0c;大体上可以分为内连接与外连接。 内连接的意思就是把两个表有关联的部分都取出…

cmake find_package、引用GDAL 初步学习

上次的源码的CMakeLists.txt文件里有 find_package(GDAL REQUIRED) 这句; 从字面意思看此源码需要GDAL库; 查了一下,find_package 指令的基本功能是查找第三方库,并返回其细节; 我当前GDAL安装在D:\GDAL; 先把它的CMakeLists.txt重命名为别的,不使用; 新建一个C…

C++:set和map的使用

set和map的使用 1.关联式容器2.key模型和key_value模型3.set3.1一些注意点3.2set的使用3.3习题 4.multiset5.map5.1一些注意点5.2map的使用5.3习题 6.multimap 1.关联式容器 序列式容器&#xff1a;比如我们之前讲的vector、string、list等均为序列式容器&#xff0c;特点是按…

ubuntu中如何设置中文输入

文章目录 1.找到设置&#xff08;settings&#xff09;2.找到keyboard3.点击Chinese&#xff0c;选择intelligent pinyin&#xff0c;并点击add4.打开浏览器测试一下 1.找到设置&#xff08;settings&#xff09; 2.找到keyboard 3.点击Chinese&#xff0c;选择intelligent pin…

项目实战:通过axios加载水果库存系统的首页数据

1、创建静态页面 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>Title</title><link rel"stylesheet" href"style/index.css"><script src"script/axios.mi…

Go语言与Python语言的性能比较

目录 一、背景与意义 二、执行速度 三、内存消耗 四、并发性能 五、编译速度与开发效率 六、综合考虑 七、应用场景 八、未来发展趋势 总结 一、背景与意义 在编程世界中&#xff0c;Go语言和Python语言都占有一席之地。Go语言是由Google开发的&#xff0c;其设计初衷…

升级 MacOS 系统后,playCover 内游戏打不开了如何解决

我们有些小伙伴在升级了 macOS 系统后大概率会遇到之前能够正常使用的 playCover 突然游戏打不开了&#xff0c;最近 mac 刚刚正式推出了 MacOS 14.1 ,导致很多用户打开游戏会闪退&#xff0c;我们其实只需要更新一下 playCover 就可以解决 playCover 正式版更新会比较慢所以我…

LInux之在同一Tomcat下使用不同的端口号访问不同的项目

&#x1f389;&#x1f389;欢迎来到我的CSDN主页&#xff01;&#x1f389;&#x1f389; &#x1f3c5;我是君易--鑨&#xff0c;一个在CSDN分享笔记的博主。&#x1f4da;&#x1f4da; &#x1f31f;推荐给大家我的博客专栏《LInux实战开发》。&#x1f3af;&#x1f3af; …

ruby语言怎么写个通用爬虫程序?

Ruby语言爬虫是指使用Ruby编写的网络爬虫程序&#xff0c;用于自动化地从互联网上获取数据。其中&#xff0c;CRawler是一个基于文本的小型地牢爬虫&#xff0c;它被设计为可扩展&#xff0c;所有游戏数据均通过JSON文件提供&#xff0c;程序仅处理游戏引擎。除此之外&#xff…

spring入门程序

2023.11.4 今天学习了一下spring的简单使用。 首先需要配置一下spring context和junit的依赖&#xff0c;在pom.xml文件中添加&#xff1a; <dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><ver…

Cube MX 开发高精度电流源跳坑过程/SPI连接ADS1255/1256系列问题总结/STM32 硬件SPI开发过程

文章目录 概要整体架构流程技术名词解释技术细节小结 概要 1.使用STM32F系列开发一款高精度恒流电源&#xff0c;用到了24位高精度采样芯片ADS1255/ADS1256系列。 2.使用时发现很多的坑&#xff0c;详细介绍了每个坑的具体情况和实际的解决办法。 坑1&#xff1a;波特率设置…