Redis启动流程
阅读一个C项目,直接从main函数入手是一个很不错的选择。通过梳理redis.c中的main函数逻辑,我们可以很清晰的了解到redis启动过程中主要流程。

上图只简略的列出了主要的函数,下面我们跟着main函数一步步学习redis启动的过程。
spt_init
处理进程启动时的参数和环境变量,以便于后期修改进程名。为什么改个程序名要如此大费周折呢,那是因为agrv和environ是连续存储的。我找了些相关的资料可以查阅:redis里的小秘密:设置进程名,Linux修改进程名称(setproctitle())。
处理了参数和环境变量后,redis陆续执行了一些系列的修改和定义:setlocale(设置地域),zmalloc_set_oom_handler(设置内存不足时的操作),srand(初始化随机种子), gettimeofday, getRandomHexChars,dictSetHashFunctionSeed等等。之后redis检查执行的可执行文件名是否是redis-sentinel或者参数中包含--sentinel,如果是的话redis将进入哨兵模式,进而转入哨兵模式的执行流程中。本文我们只讨论redis普通节点的启动过程。
initServerConfig( )
这一步主要是在初始化全局的server(上一篇)对象中的各个数据成员。为下一步解析参数和配置文件做好准备。
argc & argv
在这一步中,redis检查传入的各项参数,读取并解析配置文件(loadServerConfig)。如果没有配置redis为supervised模式,并且指定了后台运行redis时,redis执行进入后台的方法:
if (background) daemonize();
redis是如何将自己转变成为守护进程的呢?下面我们展开看看daemonize():
void daemonize(void) {
int fd;
if (fork() != 0) exit(0); /* parent exits */
setsid(); /* create a new session */
/* Every output goes to /dev/null. If Redis is daemonized but
* the 'logfile' is set to 'stdout' in the configuration file
* it will not log at all. */
if ((fd = open("/dev/null", O_RDWR, 0)) != -1) {
dup2(fd, STDIN_FILENO);
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
if (fd > STDERR_FILENO) close(fd);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
redis先fork出一个子进程,并退出父进程,以此断开和终端的交互。STDIN_FILENO(STDIN_FILENO = 0),STDOUT_FILENO(STDOUT_FILENO = 1)和STDERR_FILENO(STDERR_FILENO = 2)文件句柄分别与标准输入,标准输出,标准错误输出相关联,所以用户应用程序调用open函数打开文件时,默认都是以3索引为开始句柄。将标准输入,标准输出,标准错误输出都定向(dup2())到fd对应的/dev/null中,然后关闭fd。执行完此操作后,后台模式运行的redis继续执行后面的流程。
initServer( )
在这一步中,redis首先对信号做了一些处理:
signal(SIGHUP, SIG_IGN);
signal(SIGPIPE, SIG_IGN);
setupSignalHandlers();
2
3
signal(SIGHUP, SIG_IGN)redis多作为守护进程运行,这时其不会有控制终端,首先忽略掉SIGHUP信号。signal(SIGPIPE, SIG_IGN)TCP是全双工的信道,可以看作两条单工信道, TCP连接两端的两个端点各负责一条。 当对端调用close时, 虽然本意是关闭整个两条信道, 但本端只是收到FIN包。按照TCP协议的语义,表示对端只是关闭了其所负责的那一条单工信道,仍然可以继续接收数据。也就是说,因为TCP协议的限制,一个端点无法获知对端的socket是调用了close还是shutdown。对一个已经收到FIN包的socket调用read方法,如果接收缓冲已空,则返回0,这就是常说的表示连接关闭。但第一次对其调用write方法时,如果发送缓冲没问题,会返回正确写入(发送)。但发送的报文会导致对端发送RST报文,因为对端的socket已经调用了close,完全关闭,既不发送,也不接收数据。所以第二次调用write方法(假设在收到RST之后),会生成SIGPIPE信号,导致进程退出。为了避免进程退出, 可以捕获SIGPIPE信号, 或者忽略它, 给它设置SIG_IGN信号处理函数。
createSharedObjects
然后redis继续初始化全局的server(上一篇)对象,并在里面初始化了全局的shared对象(createSharedObjects(void))。createSharedObjects 这个函数主要是创建一些共享的全局对象。我们平时在跟redis服务交互的时候,如果有遇到错误,会收到一些固定的错误信息或者字符串比如:-ERR syntax error,-ERR no such key,这些字符串对象都是在这个函数里面进行初始化的,此外,redis还初始化了OBJ_SHARED_INTEGERS(默认为10,000)个整数对象用于共享。
adjustOpenFilesLimit
该函数主要检查下系统的可允许打开文件句柄数,对于redis来说至少要32(CONFIG_MIN_RESERVED_FDS)个文件句柄,如果检测到环境不合适,会去修改环境变量,以适合redis的运行。
aeCreateEventLoop
server.el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR);
宏CONFIG_FDSET_INCR展开是CONFIG_MIN_RESERVED_FDS+96,初始化event loop时,redis作者antirez设置总的描述符数 = server.maxclients + RESERVED_FDS + 富余的数目(保证安全),RESERVED_FDS 默认为32。
这个函数很重要,redis的事件对象就是在这个函数里面创建的,包括一些高并发异步机制对象也是在这里面初始化的。
aeEventLoop *aeCreateEventLoop(int setsize) {
aeEventLoop *eventLoop;
int i;
if ((eventLoop = zmalloc(sizeof(*eventLoop))) == NULL) goto err;
eventLoop->events = zmalloc(sizeof(aeFileEvent)*setsize);
eventLoop->fired = zmalloc(sizeof(aeFiredEvent)*setsize);
if (eventLoop->events == NULL || eventLoop->fired == NULL) goto err;
eventLoop->setsize = setsize;
eventLoop->lastTime = time(NULL);
eventLoop->timeEventHead = NULL;
eventLoop->timeEventNextId = 0;
eventLoop->stop = 0;
eventLoop->maxfd = -1;
eventLoop->beforesleep = NULL;
eventLoop->aftersleep = NULL;
if (aeApiCreate(eventLoop) == -1) goto err;
/* Events with mask == AE_NONE are not set. So let's initialize the
* vector with it. */
for (i = 0; i < setsize; i++)
eventLoop->events[i].mask = AE_NONE;
return eventLoop;
err:
if (eventLoop) {
zfree(eventLoop->events);
zfree(eventLoop->fired);
zfree(eventLoop);
}
return NULL;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
aeApiCreate是作者封装的I/O多路复用函数中的一个。对于异步机制的选择,按性能的高到低的顺序排列可以看到redis是这样一个顺序
evport -> epoll -> kqueue -> select。
/* 选择系统最佳的I/O多路复用函数库。按性能的高到低的顺序排列 */
#ifdef HAVE_EVPORT
#include "ae_evport.c" /* Solaris系统内核提供支持的 */
#else
#ifdef HAVE_EPOLL
#include "ae_epoll.c" /* LINUX系统内核提供支持的 */
#else
#ifdef HAVE_KQUEUE
#include "ae_kqueue.c" /* Mac 系统提供支持的 */
#else
#include "ae_select.c" /* 是POSIX提供的, 一般的操作系统都有支撑 */
#endif
#endif
#endif
2
3
4
5
6
7
8
9
10
11
12
13
14
listenToPort
listenToPort(server.port,server.ipfd,&server.ipfd_count)
全局结构体对象server的成员bindaddr中保存了要绑定和监听的IP(可以是多个),listenToPort依次绑定这些IP和server.port端口。
anetUnixServer
这个函数则是启动uinx socket的监听。
evictionPoolAlloc
初始化LRU键池。
aeCreateTimeEvent
这个函数主要作为定时任务的注册,在这里redis注册了serverCron()的定时任务,时间间隔是1毫秒,再执行一次之后就会对时间间隔进行重新设定。
aeCreateFileEvent
创建文件事件。将之前监听的TCP socket和unix socket描述符加入到文件事件链表中,而且是只读事件,并分别绑定事件的读操作为acceptTcpHandler和acceptUnixHandler。当有新的连接到来时,监听描述符的可读,redis会执行acceptTcpHandler或acceptUnixHandler来接受新的TCP或unix域连接请求。对于TCP连接来说,接受连接并处理请求的流程如下:
acceptTcpHandler() -> anetTcpAccept() -> acceptCommonHandler() -> createClient() -> linkClient()
acceptTcpHandler调用anetTcpAccept(封装了accept函数)从已经完成连接的队列头返回一个已完成连接,如果成功继续调用acceptCommonHandler方法。acceptCommonHandler方法会调用createClient方法在server端创建并初始化客户端对象,最后加入到server.clients链表表尾后。下面列出了客户端对象初始化的部分代码。
client *createClient(int fd) {
client *c = zmalloc(sizeof(client));
/*
* 在其他上下文环境下执行命令时(比如通过Lua脚本),我们需要一个非连接的客户端,可以通过传入-1作为描述符来创建。
*/
if (fd != -1) {
anetNonBlock(NULL,fd); /* 设置描述符为非阻塞 */
anetEnableTcpNoDelay(NULL,fd); /* 设置描述符为非延迟TCP */
if (server.tcpkeepalive)
anetKeepAlive(NULL,fd,server.tcpkeepalive);
/* 为客户端创建可读文件事件,并传入可读时的操作readQueryFromClient */
if (aeCreateFileEvent(server.el,fd,AE_READABLE,
readQueryFromClient, c) == AE_ERR)
{
close(fd);
zfree(c);
return NULL;
}
}
selectDb(c,0); /* 默认选择第0个数据库 */
uint64_t client_id;
atomicGetIncr(server.next_client_id,client_id,1);
c->id = client_id;
c->fd = fd;
c->name = NULL;
...
if (fd != -1) linkClient(c);
initClientMultiState(c); /* 为客户端执行MULTI/EXEC命令做初始化 */
return c;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
在初始化客户端对象时,redis为这个客户端创建了相应的可读文件事件,并指定了描述符可读时的操作readQueryFromClient。当服务端接收到来自客户端的数据时,继而该已连接描述符可读,redis调用readQueryFromClient读取客户端发来的命令并执行相应的操作。
在创建初始的文件和时间事件后,redis继续后面的初始化:
replicationScriptCacheInit
scriptingInit
slowlogInit
latencyMonitorInit
bioInit
后台I/O服务初始化。创建3个功能互不影响的线程,每个线程都有一个工作队列。主线程生产任务放到任务队里,这三个线程消费这些任务。任务队列和取出消费的时候都得加锁,防止竞争,使用条件变量来等待任务以及通知。
aeMain
在执行完initServer()之后,redis时初始化进入尾声,但仍需要完成一些其他的工作:
createPidFile() -> redisSetProcTitle(argv[0]) -> redisAsciiArt() -> checkTcpBacklogSettings() -> ...
aeSetBeforeSleepProc() -> aeSetAfterSleepProc() -> aeMain()
2
最终到达redis的核心aeMain()函数,进入时间循环主函数,永不退出,除非服务被终止。函数代码如下:
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
while (!eventLoop->stop) {
if (eventLoop->beforesleep != NULL)
eventLoop->beforesleep(eventLoop);
aeProcessEvents(eventLoop, AE_ALL_EVENTS|AE_CALL_AFTER_SLEEP);
}
}
2
3
4
5
6
7
8
aeMain()循环调用aeProcessEvents()来不停的处理事件(时间事件和文件事件)。那么,redis是如何高效快速处理这两种事件的呢?这就要好好读一读Redis事件驱动(下一篇)的实现了。