最近一直在看redis的源码,准备把对源码的理解以及阅读心得记录下来,避免忘记并可以和大家分享、谈论。看代码的思路很简单,直接从main函数走起,先看看初始化过程。
redis中一个最重要的数据结构是redis_server,会创建一个这个结构的全局变量server,表示当前redis的配置及状态,初始化的大部分工作就是设置这个结构的属性。
可以把初始化工作主要划分为4个部分:
1)为server设置默认值
2)解析命令行参数
3)解析配置文件
4)初始化server
初始化完成后,就会启动事件循环,以接收、服务请求。下面分步骤解析main函数,不会关注sentinel逻辑。
(1)首先是main函数的起始部分,主要进行简单的初始化工作,包括设置collate,设置随机数种子,然后调用initServerConfig()为全局变量server设置默认值。
struct timeval tv; /* We need to initialize our libraries, and the server configuration. */ #ifdef INIT_SETPROCTITLE_REPLACEMENT spt_init(argc, argv); #endif // <MM> // 使用环境变量初始化 字符串比较 // </MM> setlocale(LC_COLLATE,""); zmalloc_enable_thread_safeness(); zmalloc_set_oom_handler(redisOutOfMemoryHandler); // <MM> // 设置random的种子,之后会生成随机的run id,所有加入进程id因素 // </MM> srand(time(NULL)^getpid()); gettimeofday(&tv,NULL); dictSetHashFunctionSeed(tv.tv_sec^tv.tv_usec^getpid()); server.sentinel_mode = checkForSentinelMode(argc,argv); // <MM> // 为redisServer设置初始默认值 // </MM> initServerConfig(); /* We need to init sentinel right now as parsing the configuration file * in sentinel mode will have the effect of populating the sentinel * data structures with master nodes to monitor. */ if (server.sentinel_mode) { initSentinelConfig(); initSentinel(); }
在initServerConfig函数中,大部分是对server的属性设置默认值,还有一部分是调用populateCommandTable函数对redis的命令表初始化。全局变量redisCommandTable是redisCommand类型的数组,保存redis支持的所有命令。server.commands是一个dict,保存命令名到redisCommand的映射。populateCommandTable函数会遍历全局redisCommandTable表,把每条命令插入到server.commands中,根据每个命令的属性设置其flags。
redisCommand结构如下:
struct redisCommand { // 命令名称,在server.commands命令表中,以命令名位key char *name; // 命令处理函数 redisCommandProc *proc; // command的操作数,>0时表示确切的操作数,<0则表示至少有arity个操作数 int arity; // 标记位的字符表示形式,主要用于命令表的初始化 char *sflags; /* Flags as string representation, one char per flag. */ // 属性标记位,bitwise,指定command的类型 int flags; /* The actual flags, obtained from the ‘sflags‘ field. */ // 下面的4个属性都是用于从客户端的一个请求解析出key,比如mset k1, v1, k2, v2 ... /* Use a function to determine keys arguments in a command line. */ redisGetKeysProc *getkeys_proc; /* What keys should be loaded in background when calling this command? */ int firstkey; /* The first argument that‘s a key (0 = no keys) */ int lastkey; /* The last argument that‘s a key */ int keystep; /* The step between first and last key */ // 统计信息 long long microseconds, calls; };
redisCommand有属性proc,表示命令处理函数。在处理客户端请求时,获取到命令名,从server.commands字典中获取到redisCommand,然后回调其proc函数。
(2)命令行解析部分,主要是解析出命令行配置选项,以及通过命令行传入的配置项。然后会解析配置文件,主要是通过两个函数:
loadServerConfig:完成的功能很简单,就是将文件内容读到字符串中。并将通过命令行传入的配置项追加到该字符串后。
loadServerConfigFromString:从字符串中解析出配置项,并设置server的相关属性。
此步完成后,server中的简单属性(整数、字符串)基本都设置完成。
(3)接下来是初始化部分。设置为守护进程,创建pid文件,设置进程title以及打印启动日志。其中最重要的是initServer函数初始化基础数据结构。在初始化之后,需要从磁盘加载数据,可以是rdb或者aof。
// <MM> // 守护进程 // </MM> if (server.daemonize) daemonize(); initServer(); // <MM> // 创建pid文件 // </MM> if (server.daemonize) createPidFile(); // <MM> // 设置进程名字 // </MM> redisSetProcTitle(argv[0]); // <MM> // 打印启动日志 // </MM> redisAsciiArt(); if (!server.sentinel_mode) { /* Things not needed when running in Sentinel mode. */ redisLog(REDIS_WARNING,"Server started, Redis version " REDIS_VERSION); #ifdef __linux__ linuxOvercommitMemoryWarning(); #endif // <MM> // 从磁盘加载数据,rdb或aof // </MM> loadDataFromDisk(); if (server.ipfd_count > 0) redisLog(REDIS_NOTICE,"The server is now ready to accept connections on port %d", server.port); if (server.sofd > 0) redisLog(REDIS_NOTICE,"The server is now ready to accept connections at %s", server.unixsocket); } else { sentinelIsRunning(); } /* Warning the user about suspicious maxmemory setting. */ if (server.maxmemory > 0 && server.maxmemory < 1024*1024) { redisLog(REDIS_WARNING,"WARNING: You specified a maxmemory value that is less than 1MB (current value is %llu bytes). Are you sure this is what you really want?", server.maxmemory); }
initServerConfig中会对server的整型、字符串类型的属性设置,initServer主要对复合数据结构list、dict等初始化,并创建事件循环,初始化监听socket等。下面看下initServer函数:
首先,注册信号处理函数。
int j; signal(SIGHUP, SIG_IGN); signal(SIGPIPE, SIG_IGN); setupSignalHandlers();
如果设置开启syslog,则初始化。
if (server.syslog_enabled) { openlog(server.syslog_ident, LOG_PID | LOG_NDELAY | LOG_NOWAIT, server.syslog_facility); }
初始化客户端相关的数据结构。redis会将所有连接的客户端,slave以及monitor的客户端组织成链表,此处主要是创建这些链表。
server.current_client = NULL; server.clients = listCreate(); server.clients_to_close = listCreate(); server.slaves = listCreate(); server.monitors = listCreate(); server.slaveseldb = -1; /* Force to emit the first SELECT command. */ server.unblocked_clients = listCreate(); server.ready_keys = listCreate();
redis中会对经常使用的对象,创建常量池,以避免频繁创建、回收的开销。这些对象包括经常使用的响应内容以及小于10000的整数,还有表示bulk个数的响应头以及bulk长度的响应头,这两个响应头只包括长度小于32的。
// <MM> // 创建常量池,避免频繁创建 // </MM> createSharedObjects();
调整打开描述符限制,需要调用getrlimit和setrlimit。
// <MM> // 根据maxclients配置,调整打开文件描述符限制 // </MM> adjustOpenFilesLimit();
初始化事件循环,具体过程在事件循环一节中讲述。然后根据配置创建db数组。
// <MM> // 初始化event loop,如是epoll实现,会调用epoll_create创建epoll的fd // </MM> server.el = aeCreateEventLoop(server.maxclients+REDIS_EVENTLOOP_FDSET_INCR); server.db = zmalloc(sizeof(redisDb)*server.dbnum);
初始化监听socket,就是调用socket、bind、listen等,并将socket设置为非阻塞。如果配置了unix domain socket,也会进行相应的初始化。
/* Open the TCP listening socket for the user commands. */ if (server.port != 0 && listenToPort(server.port,server.ipfd,&server.ipfd_count) == REDIS_ERR) exit(1); /* Open the listening Unix domain socket. */ if (server.unixsocket != NULL) { unlink(server.unixsocket); /* don‘t care if this fails */ server.sofd = anetUnixServer(server.neterr,server.unixsocket, server.unixsocketperm, server.tcp_backlog); if (server.sofd == ANET_ERR) { redisLog(REDIS_WARNING, "Opening socket: %s", server.neterr); exit(1); } anetNonBlock(NULL,server.sofd); } /* Abort if there are no listening sockets at all. */ if (server.ipfd_count == 0 && server.sofd < 0) { redisLog(REDIS_WARNING, "Configured to not listen anywhere, exiting."); exit(1); }
接下来的代码,会初始化server.db数组中的每个db,主要创建相关的dict。然后关于pubsub、rdb、aof、replication相关数据结构的初始化。代码就不贴了。
然后添加定时器事件serverCron,在启动事件循环1ms后执行。redis中的事件循环就只有这一个,每0.1s(大约)执行一次,主要处理一些异步任务,比如清除expired keys,更新统计,check进行rdb、aof子进程的状态,然后会调用clientCron和databaseCron等。
// <MM> // 注册serverCrom时间事件,启动event loop 1ms后执行 // </MM> /* Create the serverCron() time event, that‘s our main way to process * background operations. */ if(aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) { redisPanic("Can‘t create the serverCron time event."); exit(1); }
将所有监听socket添加到事件循环。可以看到会有acceptTcpHandler处理客户端连接的建立。
// <MM> // 为所有的监听socket添加file event,事件处理器是acceptTcpHandler用于处理连接的创建 // </MM> /* Create an event handler for accepting new connections in TCP and Unix * domain sockets. */ for (j = 0; j < server.ipfd_count; j++) { if (aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE, acceptTcpHandler,NULL) == AE_ERR) { redisPanic( "Unrecoverable error creating server.ipfd file event."); } } if (server.sofd > 0 && aeCreateFileEvent(server.el,server.sofd,AE_READABLE, acceptUnixHandler,NULL) == AE_ERR) redisPanic("Unrecoverable error creating server.sofd file event.");
如果开启aof,则创建aof文件。
/* Open the AOF file if needed. */ if (server.aof_state == REDIS_AOF_ON) { server.aof_fd = open(server.aof_filename, O_WRONLY|O_APPEND|O_CREAT,0644); if (server.aof_fd == -1) { redisLog(REDIS_WARNING, "Can‘t open the append-only file: %s", strerror(errno)); exit(1); } }
最后部分,校验并设置maxmemory配置,初始化lua脚本、slow log以及latency monitor,最后的bioInit初始化异步io线程。redis是单线程,对于重IO操作,比如aof的fsync的调用会由异步线程调用,避免阻塞主线程的事件循环。以上就是initServer函数。
/* 32 bit instances are limited to 4GB of address space, so if there is * no explicit limit in the user provided configuration we set a limit * at 3 GB using maxmemory with ‘noeviction‘ policy‘. This avoids * useless crashes of the Redis instance for out of memory. */ if (server.arch_bits == 32 && server.maxmemory == 0) { redisLog(REDIS_WARNING,"Warning: 32 bit instance detected but no memory limit set. Setting 3 GB maxmemory limit with ‘noeviction‘ policy now."); server.maxmemory = 3072LL*(1024*1024); /* 3 GB */ server.maxmemory_policy = REDIS_MAXMEMORY_NO_EVICTION; } replicationScriptCacheInit(); scriptingInit(); slowlogInit(); latencyMonitorInit(); // <MM> // 初始化异步阻塞IO处理线程 // </MM> bioInit();
(4)main函数的最后,是启动事件循环。在事件循环的每次迭代,sleep之前会调用beforeSleep函数,进行一些异步处理。此处首先设置beforeSleep函数,然后启动aeMain事件循环。当从事件循环退出后,清理事件循环,然后退出。
aeSetBeforeSleepProc(server.el,beforeSleep); // <MM> // 开启event loop // </MM> aeMain(server.el); aeDeleteEventLoop(server.el); return 0;
以上便是redis的初始化、启动过程。总体上,比较简单,相比nginx的配置文件解析,模块初始化,进程初始化要简单不少,读起来不会太费劲。下一篇介绍事件循环。