随笔- 14  评论- 23  文章- 0 

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 };
posted on 2017-07-13 12:24 野路子程序员 阅读(...) 评论(...) 编辑 收藏