「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!」
前言
经过多年的发展, Redis已经经历了6个大版本, 6.0系列也增加了很多新特性, 如果IO的多线程处理等。同时源码依旧保持了非常高的水准, 简洁的目录布局以及清晰地文档注释和代码结构, 让人可以非常愉快的解读开发者的意图以及解决方案, 相比php的源码则背负了沉重的历史包袱, 话不多说, 手撕代码。
- 源码版本: 6.0.14
1. 测试用例
根目录下的 ./src/server.c 为服务的启动文件, 启动后首先判断是否为测试启动, 根据参数不同的测试方法,如图示可以非常方便找到 ziplist.c, quicklist.c 下面对应的方法
2. 环境和资源初始化
其中OOM的handler函数处理如下:
逻辑为打印日志 & 退出服务。
3. 初始化服务配置
调用 void initServerConfig(void) 方法 初始化 server 对象的默认字段值, 比较重要的如:
4. 初始化ACL
- ACL: 根据源码注释翻译: 必须尽快初始化ACL子系统,因为基本网络代码和客户端创建依赖于ACL,关于ACL网络库备注: www.oschina.net/p/acl
- moduleInitModulesSystem: 初始化模块系统, 包括设置键位通知订户列表和静态客户端, 设置命令过滤列表,创建计时器基数树,初始化事件监听列表等
5. 运行参数内存转移
将可执行路径和参数按顺序存放在安全的地方,以便稍后能够重新启动服务器, 可见高可用的思想在细节处的体现:
6. 初始化哨兵配置
7. 命令行和配置解析
8. 初始化服务
- 注册信号量相关函数
signal(SIGHUP, SIG_IGN);
signal(SIGPIPE, SIG_IGN);
setupSignalHandlers();
makeThreadKillable();
- 初始化服务器配置
server.aof_state = server.aof_enabled ? AOF_ON : AOF_OFF;
server.hz = server.config_hz;
server.pid = getpid();
server.in_fork_child = CHILD_TYPE_NONE;
server.main_thread_id = pthread_self();
server.current_client = NULL;
server.fixed_time_expire = 0;
...
- 创建全局共享内存对象, 并且尽可能大的提高主服务进程可用文件描述符数量, 应为除了 maxclient 参数确定的最大可操作文件描述符之外,服务还需要其他的文件描述符, 如: 持久化、监听套接字、日志文件等
// 创建全局对象
createSharedObjects();
shared.crlf = createObject(OBJ_STRING,sdsnew("\r\n"));
shared.ok = createObject(OBJ_STRING,sdsnew("+OK\r\n"));
...
// 调整文件打开数量限制
// 首先尝试读取独夫妻设定的文件数量
// 如果失败则使用: 1024 - 配置的最小服务需求fd数量(默认32)
// 否则根据如下方法计算并设定
adjustOpenFilesLimit();
...
while(bestlimit > oldlimit) {
rlim_t decr_step = 16;
limit.rlim_cur = bestlimit;
limit.rlim_max = bestlimit;
if (setrlimit(RLIMIT_NOFILE,&limit) != -1) break;
setrlimit_error = errno;
/* We failed to set file limit to 'bestlimit'. Try with a
* smaller limit decrementing by a few FDs per iteration. */
if (bestlimit < decr_step) break;
bestlimit -= decr_step;
}
...
- 创建事件监听轮训: aeCreateEventLoop, 事件相关的操作这里简单待过, 下篇文章详细解读Redis的事件机制
server.el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR);
...
- 重点: 创建socket, 并且监听。服务会监听两个配置的端口, 一个是服务端口, 一个是tls端口, 如果有配置或者初始化值, 则做监听操作, 调用同样的函数: listenToPort, listenToPort中会做一些IPv6的特殊操作处理, 其中比较重要的方法是: anetTcpServer, 此方法原型为:
int anetTcpServer(char *err, int port, char *bindaddr, int backlog)
{
return _anetTcpServer(err, port, bindaddr, AF_INET, backlog);
}
跟踪此函数调用, 最终进入: anetListen 进入我们熟悉的bind, listen系列函数, 完成socket的创建, 监听和绑定操作:
static int anetListen(char *err, int s, struct sockaddr *sa, socklen_t len, int backlog) {
// 熟悉的绑定操作
if (bind(s,sa,len) == -1) {
anetSetError(err, "bind: %s", strerror(errno));
close(s);
return ANET_ERR;
}
// 熟悉的监听操作
if (listen(s, backlog) == -1) {
anetSetError(err, "listen: %s", strerror(errno));
close(s);
return ANET_ERR;
}
return ANET_OK;
}
监听完成后, 会通过循环初始化每一个DB库对象, struct redisServer 中的*db字段是一个redisDb类型的指针数组, 然后依旧是server对象的参数赋值,在此不展示代码, 有兴趣的同学可以自己调试查看, 完成后会创建aE时间时间和aE文件时间, 分别调用: aeCreateFileEvent, aeCreateFileEvent,同属于时间相关Api, 下一篇重点介绍, 接下来就是熟悉的Accept
# 先调用:
void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask)
# 步进:
int anetTcpAccept(char *err, int s, char *ip, size_t ip_len, int *port) {
...
static int anetGenericAccept(char *err, int s, struct sockaddr *sa, socklen_t *len)
...
}
# 步进:
int fd;
while(1) {
fd = accept(s,sa,len);
if (fd == -1) {
if (errno == EINTR)
continue;
else {
anetSetError(err, "accept: %s", strerror(errno));
return ANET_ERR;
}
}
break;
}
return fd;
至此完成服务的socket 创建, 绑定, 监听, accept操作。
- 最后一步是初始化其他操作,解释如下:
# 初始化脚本缓存
replicationScriptCacheInit();
# 初始化lua 脚本运行环境
scriptingInit(1);
# 初始化慢日志
slowlogInit();
# 初始化延时监控
latencyMonitorInit();
至此, initServer 处理完成。
9. 其他初始化或者检查操作
// 熟悉的redis ACSII 字符画, 字符画变量定义: ascii_logo
void redisAsciiArt(void)
// 检查tcp-backlog参数设置, 也是一个比较重要的参数设置, 不同的场景可以有优化设置
void checkTcpBacklogSettings(void)
// 接下来是针对各个平台的不同设置或者初始化操作, 在此只解读linux下的操作:
moduleLoadFromQueue();
ACLLoadUsersAtStartup();
InitServerLast();
loadDataFromDisk();
// 设置cpu亲和性
redisSetCpuAffinity();
// 设置OOM score
setOOMScoreAdj();
10. 启用事件处理器, 轮训事件并处理
事件机制比较重要, 内容也比较多, 同理, 我们下一篇单独解读Redis事件机制。
aeMain(server.el):
// 函数实现
eventLoop->stop = 0;
while (!eventLoop->stop) {
aeProcessEvents(eventLoop, AE_ALL_EVENTS|
AE_CALL_BEFORE_SLEEP|
AE_CALL_AFTER_SLEEP);
}
aeDeleteEventLoop(server.el);
// 函数实现
aeApiFree(eventLoop);
zfree(eventLoop->events);
zfree(eventLoop->fired);
/* Free the time events list. */
aeTimeEvent *next_te, *te = eventLoop->timeEventHead;
while (te) {
next_te = te->next;
zfree(te);
te = next_te;
}
zfree(eventLoop);
// 结束
return 0;
End
至此Redis的启动流程解读完成, 可以看到Redis对处理很多操作系统细节方面做的非常到位, 非常值得我们学习, 下一篇我们开始分析Redis的时间和线程机制,希望各位看官给一个小小的赞, 一起进步 :)