redis源码笔记(一) —— 从redis的启动到command的分发
本作品采用知识共享署名 4.0 国际许可协议进行许可。转载联系作者并保留声明头部与原文链接https://luzeshu.com/blog/redis1
本博客同步在http://www.cnblogs.com/papertree/p/7159802.html
这个系列博客大部分完成于一年前,基于3.0.5版本(但是代码行数不一定完全相符,调试过程中会修改一些代码)。
这一篇博客针对第二篇涉及到的redisClient、redisDb、redisObject(robj)等几个结构体,以及redis程序的启动到循环、到分发command来进行讲解。
看redis源码时有个很深的感触就是c语言虽然不是“面向对象编程语言”,原生不支持类、继承等面向对象编程语言的概念,但不影响在c语言上运用“面向对象编程思想”进行开发。比如很多模块会定义一个结构体,还有相关的一系列函数,这些函数使用该结构体的指针类型作为第一个参数,实际上这是模拟了this指针的做法。可以把这些函数和结构体看成一个类的方法和成员。
1.1 redis的启动到进入等待 / aeEventLoop结构体
我们知道redis也是一个普通的服务端程序,监听6379(默认)端口。从main函数启动,最终进入事件驱动库进行循环等待。比如node的libuv。那么redis自己实现了一个简单的事件驱动库,放在ae.c文件。并且对系统层面支持的IO复用接口进行了封装,比如epoll(linux)、kqueue(OS X、FreeBSD等)、evport(Solaris 10等)、select。来看下图,可以知道当系统不支持其他IO复用接口时,默认使用了select模型。
图1-1-1
看到这段代码,产生一个疑问,我们都知道windows下最高效的IO复用模型应该属IOCP了,比如node使用的libuv库,就对IOCP进行了封装。但ae.c里面看到,redis不支持IOCP?于是针对这个问题,笔者google了一下,发现了两个有意思的链接。
http://www.oschina.net/news/23944/redis-deny-microsoft-windows-fixpack
http://oldblog.antirez.com/post/redis-win32-msft-patch.html
大体情况就是redis原生不支持IOCP,于是微软采用libuv把redis移植到了windows,在github上给redis提交了补丁。但redis作者拒绝将此补丁加入主干代码。第二个链接是redis作者对此的解释。
回归正题,那么可以看到redis从启动到进入等待的过程并不复杂。redis.c/main() -> ae.c/aeMain() -> (ae_epoll.c、ae_kqueue.c、ae_select.c等)/aeApiPoll(),看下图:
图1-1-2
1.1.1 redisServer结构体
看一下redisServer的结构体定义:
图1-1-3
注意几个成员,与稍后讲解有关:
aeEventLoop *el:这里表示的就是一个事件驱动库的结构体
int ipfd[REDIS_BINDADDR_MAX]:redis服务监听的socket fd
int ipfd_count:ipfd的计数成员
redis有一个全局的变量struct redisServer server,保存了当前redisServer的各种信息,包括aeEventLoop类型的server.el成员等。
当main函数调用了ae事件驱动库的aeMain()时,传了server.el,这里就是前面说的面向对象编程思想的做法了,server.el充当一个this指针。
当进入ae.c模块时,我们来看看aeEventLoop结构体。
1.1.2 aeEventLoop结构体
看一下 aeEventLoop结构体和几个相关的结构体、函数指针类型的定义:
图1-1-4
来看几个关键成员及相关的结构体,以及与之相关的方法:
1.1.2.1 void *apidata与aeApiState结构体与aeApiCreate()
从下图1-1-5里的aeApiCreate()函数里面可以看到,apidata实际上放的是一个aeApiState结构体指针,可以看到ae_epoll.c(图1-1-5左)、ae_vport.c(图1-1-5右)分别对aeApiState有不同的结构体定义,实际上是对不同操作系统(不同复用接口)的封装。
按照上面说的“面向对象编程思想”,aeApiState结构体相关的方法的“this指针”应该是aeApiState指针。
可以从图1-1-5中看到aeApiCreate()、aeApiResize()等几个跟aeApiState结构体相关的方法的定义,发现他们的“this指针”都是aeEventLoop*类型,而不是aeApiState*,当方法内部访问aeApiState时,通过eventLoop->apidata去访问。
注意到这几个方法内部(比如aeApiAddEvent),并不都仅仅只是使用了eventLoop->apidata,同时也访问了eventLoop的其他成员,所以这里使用aeEventLoop*作为“this指针”是合理的。
图1-1-5
1.1.2.2 events成员(aeFileEvent结构体的动态数组,以fd为索引)与aeCreateFileEvent()
aeCreateFileEvent()是aeEventLoop的一个方法成员,通过该方法,往aeEventLoop的events里添加一个aeFileEvent对象,可以看到图1-1-4的定义。可以看出aeFileEvent实际上代表的是一个事件handler,封装了事件的回调函数,以及对应的clientData。当epoll_wait()监听的fd有事件到来时,该对象被取出,回调函数被执行,clientData被回传。图1-1-4中的两个函数指针定义,就是该回调函数的类型。
这里举两个关键的使用位置:
1. 监听socket的回调函数
在main函数开始后,initServer的时候,会调用aeCreateFileEvent(),把server.ipfd[]中监听的fd依次创建一个aeFileEvent对象,响应函数为(aeFileProc*) acceptTcpHandler,加进事件驱动库,并添加到 server.el->events 成员里面,以fd为数组索引下标。注意了此时的clientData是NULL的,看一下此处的代码:
图1-1-6
2. 连接socket的回调函数
当有连接到来的时候,acceptTcpHandler被触发,此时redis创建了一个redisClient的对象,并同样调用了aeCreateFileEvent(),把相应的回调函数(aeFileProc*) readQueryFromClient同样封装成aeFileEvent对象,加进事件驱动库,添加到server.el->events成员里面,以fd为索引下标,此时的clientData是对应的redisClient对象,这个redisClient标识了一个客户端的连接,redisClient结构体、以及readQeuryFromClient如何分发处理command的详细介绍在1.2.3节。
来看一下对应的调用代码:
图1-1-7
可以看到networking.c文件里面,acceptTcpHandler、readQueryFromClient都是aeFileProc类型的回调函数。
*1.1.2.3 aeCreateTimeEvent()方法与timeEventHead成员(aeTimeEvent结构体的链表头,所以aeTimeEvent存在next成员)
这里额外讲多一个结构体类型,不在本篇博客“从启动到进入等待、从接收连接到分发命令”的主线,但是在第三篇博客《redis源码笔记(三) —— redis的哨兵模式以及高可用性》的3.2节里面会用到。
我们知道server进入epoll_wait()之后会进入等待,但是事实上redis-server是不断被定时唤醒的,因为它后台有一个定时任务函数 —— serverCron。这个后台执行任务被封装在aeTimeEvent对象里面,aeEventLoop对象(server.el)通过自身的aeCreateTimeEvent()方法去往自身的timeEventHead链表添加这样一个对象。在图1-1-6中可以看到initServer里面有aeCreateTimeEvent这么一个过程。
这里需要讲的是:这个后台任务是如何被周期性执行的,还有执行周期是什么。
看到图1-1-8中aeCreateTimeEvent的定义,看到第二个参数milliseconds,再看到图1-1-6里面initServer添加serverCron时该参数为1,不要误以为这个后台任务就是执行周期为1ms。
图1-1-8
先看到aeProcessEvents,每次进入aeApiPoll()前,aeProcessEvents都会调用aeSearchNearestTimer从eventLoop->timeEventHead 去找到第一个aeTimeEvent对象,通过该对象的when_sec和when_ms去计算下一次监听中断的时长。
图1-1-9
那么当上面根据eventLoop->timeEventHead计算的最短时长到达后,aeApiPoll返回,执行processTimeEvents,对eventLoop->timeEventHead里面所有过时了的aeTimeEvent对象进行“执行回调”,看代码:
图1-1-10
那么可以看到这个回调函数,大多数情况下就是上面的后台任务函数serverCron。根据该函数返回的retval,加上当前时间并更新到当前的aeTimeEvent对象的时间成员上面,那么就是说,这个后台任务的执行周期,是由该后台任务的返回值决定的,如果该函数返回了AE_NOMORE,那么这个aeTimeEvent对象就会从eventLoop->timeEventHead链表里面删除。
来看看serverCron函数的返回值:
图1-1-11
可以看出serverCron返回的是一个变量,1000/server.hz,这个hz就是频率的意思(还记得物理里面的单位吗,时间的倒数就是频率),比如频率为10,那么1000ms里面10次的间隔就是100ms。这个server.hz 可以通过配置文件redis.conf里面的hz 选项进行设置。
另外注意到上面的run_with_period这个宏定义。这个比如run_with_period(100) {} 限制了该代码块的“最小周期”是100ms,比如说,你的server.hz 是2,那么你的serverCron周期是500ms,那么周期大于100ms可以接受,每次执行serverCron时run_with_period(100)的代码块都会被执行。如果server.hz 是20,那么serverCron周期是50ms,那么周期小于100ms了,run_with_period(100){}的代码块会根据server.cronloops的计数来判断,每两次serverCron执行一次,如果server.hz是100,serverCron周期是10ms,那么每10次serverCron执行一次代码块,保证run_with_period(100)里面的代码真的是每100ms执行一次。
那么回到上面aeCreateFileEvent的第二个参数milliseconds、以及图1-1-6在initServer时调用这个时候传的“1”是指什么呢?其实跟processTimeEvents每次执行serverCron后拿到下一个周期的监听时长、添加到当前的aeTimeEvent对象上一样,这个initServer调用aeCreateFileEvent时传的“1”也表示下一个周期的监听时长,也就是这个aeTimeEvent封装的serverCron第一次被执行应该是在当前时间的1ms之后,而随后的周期性执行才是根据serverCron本身返回的值去决定下一个周期监听时长。
但是这里注意的是,serverCron第一次被执行也往往不是在1ms之后,我们看到图1-1-9的378到385这几行代码。在进入aeApiPoll前会进行计算下一个周期监听时长,计算方式就是从eventLoop->timeEventHead取出最近的那个aeTimeEvent,减去当前时间。但是当initServer执行aeCreateFileEvent()到这几行代码的时候,往往历经了几毫秒,那么这个最近的aeTimeEvent的时间已经过期了几毫秒。那么从上面的计算方式可以发现,tvp表示的应该是{900+毫秒,-1秒},但是第384行代码会把负值的秒清零。所以往往第一次serverCron的调用会是在900+毫秒之后。
小结:
通过上面几个结构体和相关方法的讲解,我们大概知道了从main函数启动,到进入监听等待的过程中,涉及到的相关结构体及方法。
下面来看一下从接收到客户端的连接请求、到command的分发过程。
1.2 从客户端的连接请求到command的分发
从1.1节看到,与“对客户端的连接请求处理”相关的是aeFileEvent结构体,当tcp连接请求到来时,acceptTcpHandler被调用,并针对该tcp连接创建一个新的aeFileEvent对象,用于处理后续到来的command,这个新建的aeFileEvent对象的回调函数是readQueryFromClient。
1.2.1 接收客户端连接请求 / acceptTcpHandler()
上面1.1.2.2节对acceptTcpHandler里面如何创建一个aeFileEvent对象(clientData*为redisClient指针,回调函数为readQueryFromClient)讲的很清楚。
1.2.2 接收客户端的命令 / readQueryFromClient()
上面1.1.2.2节也说了,当epoll_wait()监听的fd有事件到来时,该对象被取出,回调函数被执行,clientData被回传。
当建立的tcp连接有数据到来时,调用回调函数readQueryFromClient(),并把clientData回传(即privdata参数),实际上clientData就是针对该连接的redisClient对象。
可以看到这里,几乎都是对redisClient的操作。这里结合redisClient的结构体及相关的方法,来对这个流程进行讲解。
首先看源码:
1154 void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) {
1155 redisClient *c = (redisClient*) privdata;
1156 int nread, readlen;
1157 size_t qblen;
1158 REDIS_NOTUSED(el);
1159 REDIS_NOTUSED(mask);
1160
1161 server.current_client = c;
1162 readlen = REDIS_IOBUF_LEN;
1163 /* If this is a multi bulk request, and we are processing a bulk reply
1164 * that is large enough, try to maximize the probability that the query
1165 * buffer contains exactly the SDS string representing the object, even
1166 * at the risk of requiring more read(2) calls. This way the function
1167 * processMultiBulkBuffer() can avoid copying buffers to create the
1168 * Redis Object representing the argument. */
1169 if (c->reqtype == REDIS_REQ_MULTIBULK && c->multibulklen && c->bulklen != -1
1170 && c->bulklen >= REDIS_MBULK_BIG_ARG)
1171 {
1172 int remaining = (unsigned)(c->bulklen+2)-sdslen(c->querybuf);
1173
1174 if (remaining < readlen) readlen = remaining;
1175 }
1176
1177 qblen = sdslen(c->querybuf);
1178 if (c->querybuf_peak < qblen) c->querybuf_peak = qblen;
1179 c->querybuf = sdsMakeRoomFor(c->querybuf, readlen);
1180 nread = read(fd, c->querybuf+qblen, readlen);
1181 if (nread == -1) {
1182 if (errno == EAGAIN) {
1183 nread = 0;
1184 } else {
1185 redisLog(REDIS_VERBOSE, "Reading from client: %s",strerror(errno));
1186 freeClient(c);
1187 return;
1188 }
1189 } else if (nread == 0) {
1190 redisLog(REDIS_VERBOSE, "Client closed connection");
1191 freeClient(c);
1192 return;
1193 }
1194 if (nread) {
1195 sdsIncrLen(c->querybuf,nread);
1196 c->lastinteraction = server.unixtime;
1197 if (c->flags & REDIS_MASTER) c->reploff += nread;
1198 server.stat_net_input_bytes += nread;
1199 } else {
1200 server.current_client = NULL;
1201 return;
1202 }
1203 if (sdslen(c->querybuf) > server.client_max_querybuf_len) {
1204 sds ci = catClientInfoString(sdsempty(),c), bytes = sdsempty();
1205
1206 bytes = sdscatrepr(bytes,c->querybuf,64);
1207 redisLog(REDIS_WARNING,"Closing client that reached max query buffer length: %s (qbuf initial bytes: %s)", ci, bytes);
1208 sdsfree(ci);
1209 sdsfree(bytes);
1210 freeClient(c);
1211 return;
1212 }
1213 processInputBuffer(c);
1214 server.current_client = NULL;
1215 }
第1180代码读取当前TCP连接收到的数据,这些数据正是redis命令行的TCP数据格式。
在一个窗口“gdb src/redis-server”,并且“break networking.c:1180”,然后“run”。
tmux开另一个pane,运行“src/redis-cli”,然后输入“keys *”命令。
此时gdb会在断点的地方停下,然后“next”,查看 redisClient的querybuf成员。
gdb$ p c->querybuf
$8 = (sds) 0x7ffff0121008 "*2\r\n$4\r\nkeys\r\n$1\r\n*\r\n"
这便是redis-server接收到客户端的命令的最原始的数据(当然还有更原始的mac层、ip层的数据包是由系统处理的)。
关于redis命令交互的协议,文档上有详细介绍: https://redis.io/topics/protocol
最后readQeuryFromClient()->processInputBuffer(c)->processCommand() 进行command的分发和处理。
processCommand() 在src/redis.c 里面。同时,该文件里面有一个全局表维护着命令与对应的处理函数:
123 struct redisCommand redisCommandTable[] = {
124 {"get",getCommand,2,"rF",0,NULL,1,1,1,0,0},
125 {"set",setCommand,-3,"wm",0,NULL,1,1,1,0,0},
126 {"setnx",setnxCommand,3,"wmF",0,NULL,1,1,1,0,0},
127 {"setex",setexCommand,4,"wm",0,NULL,1,1,1,0,0},
128 {"psetex",psetexCommand,4,"wm",0,NULL,1,1,1,0,0},
129 {"append",appendCommand,3,"wm",0,NULL,1,1,1,0,0},
130 {"strlen",strlenCommand,2,"rF",0,NULL,1,1,1,0,0},
131 {"del",delCommand,-2,"w",0,NULL,1,-1,1,0,0},
132 {"exists",existsCommand,-2,"rF",0,NULL,1,-1,1,0,0},
133 {"setbit",setbitCommand,4,"wm",0,NULL,1,1,1,0,0},
134 {"getbit",getbitCommand,3,"rF",0,NULL,1,1,1,0,0},
135 {"setrange",setrangeCommand,4,"wm",0,NULL,1,1,1,0,0},
136 {"getrange",getrangeCommand,4,"r",0,NULL,1,1,1,0,0},
137 {"substr",getrangeCommand,4,"r",0,NULL,1,1,1,0,0},
138 {"incr",incrCommand,2,"wmF",0,NULL,1,1,1,0,0},
139 {"decr",decrCommand,2,"wmF",0,NULL,1,1,1,0,0},
140 {"mget",mgetCommand,-2,"r",0,NULL,1,-1,1,0,0},
141 {"rpush",rpushCommand,-3,"wmF",0,NULL,1,1,1,0,0},
142 {"lpush",lpushCommand,-3,"wmF",0,NULL,1,1,1,0,0},
143 {"rpushx",rpushxCommand,3,"wmF",0,NULL,1,1,1,0,0},
144 {"lpushx",lpushxCommand,3,"wmF",0,NULL,1,1,1,0,0},
145 {"linsert",linsertCommand,5,"wm",0,NULL,1,1,1,0,0},
146 {"rpop",rpopCommand,2,"wF",0,NULL,1,1,1,0,0},
147 {"lpop",lpopCommand,2,"wF",0,NULL,1,1,1,0,0},
148 {"brpop",brpopCommand,-3,"ws",0,NULL,1,1,1,0,0},
149 {"brpoplpush",brpoplpushCommand,4,"wms",0,NULL,1,2,1,0,0},
150 {"blpop",blpopCommand,-3,"ws",0,NULL,1,-2,1,0,0},
151 {"llen",llenCommand,2,"rF",0,NULL,1,1,1,0,0},
152 {"lindex",lindexCommand,3,"r",0,NULL,1,1,1,0,0},
153 {"lset",lsetCommand,4,"wm",0,NULL,1,1,1,0,0},
154 {"lrange",lrangeCommand,4,"r",0,NULL,1,1,1,0,0},
155 {"ltrim",ltrimCommand,4,"w",0,NULL,1,1,1,0,0},
156 {"lrem",lremCommand,4,"w",0,NULL,1,1,1,0,0},
157 {"rpoplpush",rpoplpushCommand,3,"wm",0,NULL,1,2,1,0,0},
158 {"sadd",saddCommand,-3,"wmF",0,NULL,1,1,1,0,0},
159 {"srem",sremCommand,-3,"wF",0,NULL,1,1,1,0,0},
160 {"smove",smoveCommand,4,"wF",0,NULL,1,2,1,0,0},
161 {"sismember",sismemberCommand,3,"rF",0,NULL,1,1,1,0,0},
......
287 };
redis源码笔记(一) —— 从redis的启动到command的分发的更多相关文章
- 曹工说Redis源码(2)-- redis server 启动过程解析及简单c语言基础知识补充
文章导航 Redis源码系列的初衷,是帮助我们更好地理解Redis,更懂Redis,而怎么才能懂,光看是不够的,建议跟着下面的这一篇,把环境搭建起来,后续可以自己阅读源码,或者跟着我这边一起阅读.由于 ...
- 曹工说Redis源码(3)-- redis server 启动过程完整解析(中)
文章导航 Redis源码系列的初衷,是帮助我们更好地理解Redis,更懂Redis,而怎么才能懂,光看是不够的,建议跟着下面的这一篇,把环境搭建起来,后续可以自己阅读源码,或者跟着我这边一起阅读.由于 ...
- 曹工说Redis源码(5)-- redis server 启动过程解析,以及EventLoop每次处理事件前的前置工作解析(下)
曹工说Redis源码(5)-- redis server 启动过程解析,eventLoop处理事件前的准备工作(下) 文章导航 Redis源码系列的初衷,是帮助我们更好地理解Redis,更懂Redis ...
- 曹工说Redis源码(6)-- redis server 主循环大体流程解析
文章导航 Redis源码系列的初衷,是帮助我们更好地理解Redis,更懂Redis,而怎么才能懂,光看是不够的,建议跟着下面的这一篇,把环境搭建起来,后续可以自己阅读源码,或者跟着我这边一起阅读.由于 ...
- 曹工说Redis源码(7)-- redis server 的周期执行任务,到底要做些啥
文章导航 Redis源码系列的初衷,是帮助我们更好地理解Redis,更懂Redis,而怎么才能懂,光看是不够的,建议跟着下面的这一篇,把环境搭建起来,后续可以自己阅读源码,或者跟着我这边一起阅读.由于 ...
- Redis源码分析:serverCron - redis源码笔记
[redis源码分析]http://blog.csdn.net/column/details/redis-source.html Redis源代码重要目录 dict.c:也是很重要的两个文件,主要 ...
- Redis源码笔记--服务器日志和函数可变参数处理server.c
前言 Redis源码中定义了几个和日志相关的函数,用于将不同级别的信息打印到不同的位置(日志文件或标准输出,取决于配置文件的设置),这些函数的定义位于 server.h 和server.c 文件中,包 ...
- Redis 源码简洁剖析 07 - main 函数启动
前言 问题 阶段 1:基本初始化 阶段 2:检查哨兵模式,执行 RDB 或 AOF 检测 阶段 3:运行参数解析 阶段 4:初始化 server 资源管理 初始化数据库 创建事件驱动框架 阶段 5:执 ...
- Redis源码笔记-初步
目录 目录 1 1. 前言 2 2. 名词 2 3. dict.c 2 3.1. siphash算法 2 3.2. 核心函数 3 3.3. 核心宏 3 3.4. 核心结构体 3 3.4.1. dict ...
随机推荐
- URL的标准格式
URL的标准格式 scheme://host:port/path?query#fragment 1. scheme:协议 2. host:主机 3. port:端口 4. path:路径 5. qu ...
- golang实现dns域名解析(三):响应报文分析
前面说了构造请求发送报文,接下来我们好好研究下如何解析服务器端发回来的应答信息. 首先还是用前面的程序代码发一个请求,用抓包工具看看应答的内容有哪些: 截图的第一部分是返回信息的统计,表明这个返回的包 ...
- OOP中this指向详解
谁调用了函数,this就指向谁 >>> this指向的永远只可能是对象!!! >>> this指向谁,永远不取决于this写在哪,而是取决于函数在哪调用!!! &g ...
- Python os.walk的用法与举例
os.walk(top, topdown=True, onerror=None, followlinks=False) 可以得到一个三元tupple(dirpath, dirnames, filena ...
- [Leetcode] Binary search--275 H-Index
Follow up for H-Index: What if the citations array is sorted in ascending order? Could you optimize ...
- 搭建rtmp直播流服务之3:java开发ffmpeg实现rtsp转rtmp并实现ffmpeg命令的接口化管理架构设计及代码实现
上一篇文章简单介绍了java如何调用ffmpeg的命令:http://blog.csdn.net/eguid_1/article/details/51777716 上上一篇介绍了nginx-rtmp服 ...
- PHP的面向对象 — 封装、继承、多态
K在上一次的基础篇中给大家介绍了一下关于PHP中数组和字符串的使用方法等,这一次,K决定一次性大放送,给大家分享一下PHP中面向对象的三大特性:封装.继承.多态三个方面的知识. 一.封装 在PHP中, ...
- Unexpected end of input 和 Unexpected token var 和 Unexpected token ;
在写jsp的时候使用的一段代码一直调试,出现Unexpected token ; 错误. 所以最后把代码各种精简,得到了如下的测试示例代码 <% String aaa="123&quo ...
- Replication-删除发布备注
1.删除replication,先删除replication的作业,再删除对应的订阅,再删除发布: 2.相关脚本:删除监视器里不存在的条目sp_removedistpublisherdbreplica ...
- vue+websocket+express+mongodb实战项目(实时聊天)(二)
原项目地址:[ vue+websocket+express+mongodb实战项目(实时聊天)(一)][http://blog.csdn.net/blueblueskyhua/article/deta ...