Redis 启动流程全解析(server.c 到 main 函数)

启动一个 Redis 实例看起来很简单,redis-server 一敲就完了。但你有没有想过,从按下回车到 Redis 开始接受连接,中间发生了什么?

这篇文章主要从 server.cmain 函数开始,来一步步拆解 Redis 的启动流程~

先看 main 函数的全貌

main 函数在 server.c 的第 4000 行附近,核心流程可以概括为:

1初始化基础库 → 加载配置 → 初始化服务器 → 加载数据 → 进入事件循环

代码骨架是这样的:

 1int main(int argc, char **argv) {
 2    // 1. 基础初始化
 3    initServerConfig();
 4    moduleInitModulesSystem();
 5    
 6    // Sentinel 模式初始化(如果是)
 7    if (server.sentinel_mode) {
 8        initSentinelConfig();
 9        initSentinel();
10    }
11    
12    // 2. 加载配置文件和命令行参数
13    resetServerSaveParams();
14    loadServerConfig(configfile, options);
15    
16    // 3. 以守护进程方式运行(如果配置了)
17    if (background) daemonize();
18    
19    // 4. 初始化服务器
20    initServer();
21    
22    // 5. 从磁盘加载数据
23    loadDataFromDisk();
24    
25    // 6. 进入事件循环
26    aeMain(server.el);
27    
28    return 0;
29}

下面逐个展开。

第一步:基础初始化

main 函数开头做了一些必须先做的初始化:

 1setlocale(LC_COLLATE,"");
 2tzset();                    // 时区初始化
 3zmalloc_set_oom_handler(redisOutOfMemoryHandler);  // 内存不足处理
 4srand(time(NULL)^getpid()); // 随机种子
 5gettimeofday(&tv,NULL);
 6
 7// 生成哈希种子,用于字典的哈希函数
 8char hashseed[16];
 9getRandomHexChars(hashseed,sizeof(hashseed));
10dictSetHashFunctionSeed((uint8_t*)hashseed);

这些操作和 Redis 业务逻辑无关,但是基础库需要的。比如哈希种子,每次启动都不一样,防止哈希碰撞攻击。

然后判断是否是 Sentinel 模式:

1server.sentinel_mode = checkForSentinelMode(argc,argv);

判断方式很简单:看可执行文件名是不是 redis-sentinel,或者参数里有没有 --sentinel

第二步:initServerConfig - 初始化默认配置

这个函数很长,大概 500 行,主要就是给 server 这个全局结构体的各个字段赋默认值。

 1void initServerConfig(void) {
 2    // 互斥锁初始化
 3    pthread_mutex_init(&server.next_client_id_mutex,NULL);
 4    pthread_mutex_init(&server.lruclock_mutex,NULL);
 5    pthread_mutex_init(&server.unixtime_mutex,NULL);
 6
 7    // 生成运行 ID
 8    getRandomHexChars(server.runid,CONFIG_RUN_ID_SIZE);
 9    server.runid[CONFIG_RUN_ID_SIZE] = '\0';
10    
11    // 基础配置
12    server.port = CONFIG_DEFAULT_SERVER_PORT;  // 6379
13    server.dbnum = CONFIG_DEFAULT_DBNUM;       // 16
14    server.verbosity = CONFIG_DEFAULT_VERBOSITY;
15    server.maxidletime = CONFIG_DEFAULT_CLIENT_TIMEOUT;
16    server.tcpkeepalive = CONFIG_DEFAULT_TCP_KEEPALIVE;
17    
18    // AOF 相关
19    server.aof_state = AOF_OFF;
20    server.aof_fsync = CONFIG_DEFAULT_AOF_FSYNC;
21    
22    // RDB 相关
23    server.rdb_filename = zstrdup(CONFIG_DEFAULT_RDB_FILENAME);
24    server.rdb_compression = CONFIG_DEFAULT_RDB_COMPRESSION;
25    
26    // 内存相关
27    server.maxmemory = CONFIG_DEFAULT_MAXMEMORY;
28    server.maxmemory_policy = CONFIG_DEFAULT_MAXMEMORY_POLICY;
29    
30    // ... 还有很多
31}

有个有意思的地方——RDB 默认的 save 策略:

1resetServerSaveParams();
2appendServerSaveParams(60*60,1);   // 1小时内有1次修改就save
3appendServerSaveParams(300,100);   // 5分钟内有100次修改
4appendServerSaveParams(60,10000);  // 1分钟内有10000次修改

还有命令表的初始化:

1server.commands = dictCreate(&commandTableDictType,NULL);
2server.orig_commands = dictCreate(&commandTableDictType,NULL);
3populateCommandTable();

populateCommandTable 把所有命令注册到字典里,支持 rename-command 配置项重命名命令。

第三步:加载配置

配置来源有两个:配置文件和命令行参数。

 1if (argc >= 2) {
 2    j = 1;
 3    sds options = sdsempty();
 4    char *configfile = NULL;
 5
 6    // 第一个参数如果不是 --开头,就当配置文件路径
 7    if (argv[j][0] != '-' || argv[j][1] != '-') {
 8        configfile = argv[j];
 9        server.configfile = getAbsolutePath(configfile);
10        j++;
11    }
12
13    // 剩下的参数转成配置字符串
14    // 比如 --port 6380 转成 "port 6380\n"
15    while(j != argc) {
16        if (argv[j][0] == '-' && argv[j][1] == '-') {
17            if (sdslen(options)) options = sdscat(options,"\n");
18            options = sdscat(options,argv[j]+2);
19            options = sdscat(options," ");
20        } else {
21            options = sdscatrepr(options,argv[j],strlen(argv[j]));
22            options = sdscat(options," ");
23        }
24        j++;
25    }
26    
27    // 加载配置
28    loadServerConfig(configfile, options);
29}

这样设计的好处是配置可以灵活组合:

1redis-server /etc/redis.conf --port 6380 --maxmemory 1gb

配置文件里的设置会被命令行参数覆盖。

loadServerConfig 函数做的就是逐行解析配置,设置到 server 结构体里。支持 INCLUDE 引入其他配置文件。

第四步:守护进程化

如果配置了 daemonize yes,Redis 会调用 daemonize() 函数:

1int background = server.daemonize && !server.supervised;
2if (background) daemonize();

daemonize() 的实现是经典的 Unix 守护进程创建流程:

 1void daemonize(void) {
 2    int fd;
 3
 4    if (fork() != 0) exit(0);  // 父进程退出
 5    setsid();                   // 创建新会话
 6    
 7    if ((fd = open("/dev/null", O_RDWR, 0)) != -1) {
 8        dup2(fd, STDIN_FILENO);
 9        dup2(fd, STDOUT_FILENO);
10        dup2(fd, STDERR_FILENO);
11        if (fd > STDERR_FILENO) close(fd);
12    }
13}

fork 后父进程退出,子进程脱离终端,重定向标准输入输出到 /dev/null。

第五步:initServer - 真正的服务器初始化

这是最核心的初始化函数,干了下面这些事:

5.1 信号处理

1signal(SIGHUP, SIG_IGN);   // 忽略终端挂起
2signal(SIGPIPE, SIG_IGN);  // 忽略管道破裂
3setupSignalHandlers();     // 注册 SIGINT、SIGTERM 等信号处理

5.2 创建各种链表和数据结构

1server.clients = listCreate();              // 客户端列表
2server.clients_to_close = listCreate();     // 待关闭客户端
3server.slaves = listCreate();               // 从节点列表
4server.monitors = listCreate();             // monitor 客户端
5server.clients_pending_write = listCreate();// 待写回客户端
6server.unblocked_clients = listCreate();    // 已取消阻塞客户端

5.3 创建事件循环

1server.el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR);
2if (server.el == NULL) {
3    serverLog(LL_WARNING, "Failed creating the event loop...");
4    exit(1);
5}

CONFIG_FDSET_INCR 是个冗余值,确保 fd 数量够用。

5.4 监听端口

1if (server.port != 0 &&
2    listenToPort(server.port,server.ipfd,&server.ipfd_count) == C_ERR)
3    exit(1);

listenToPort 创建 socket,bind,listen,把 fd 存到 server.ipfd 数组。

如果配置了 Unix socket:

1if (server.unixsocket != NULL) {
2    server.sofd = anetUnixServer(server.neterr, server.unixsocket,
3        server.unixsocketperm, server.tcp_backlog);
4}

5.5 初始化数据库

Redis 默认创建 16 个数据库(由 dbnum 配置),通过 SELECT n 命令切换。

 1server.db = zmalloc(sizeof(redisDb)*server.dbnum);
 2
 3for (j = 0; j < server.dbnum; j++) {
 4    server.db[j].dict = dictCreate(&dbDictType,NULL);        // 数据字典
 5    server.db[j].expires = dictCreate(&keyptrDictType,NULL); // 过期时间字典
 6    server.db[j].blocking_keys = dictCreate(&keylistDictType,NULL);
 7    server.db[j].ready_keys = dictCreate(&objectKeyPointerValueDictType,NULL);
 8    server.db[j].watched_keys = dictCreate(&keylistDictType,NULL);
 9    server.db[j].id = j;
10}

每个 redisDb 结构体包含多个字典,各司其职:

字典 用途
dict 存储所有键值对,核心数据结构
expires 存储键的过期时间(指针指向 dict 中的 key)
blocking_keys 存储 BLPOP 等命令阻塞等待的 key 及对应客户端
ready_keys LPUSH/RPUSH 后唤醒阻塞客户端的待处理 key
watched_keys MULTI/EXEC 事务中 WATCH 监视的 key

dictexpires 分开存储的设计很巧妙:不设置过期时间的 key 不需要在 expires 中占空间,节省内存。过期检查时只需遍历 expires 字典。

5.6 注册时间事件 - serverCron

1if (aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {
2    serverPanic("Can't create event loop timers.");
3    exit(1);
4}

serverCron 是 Redis 的定时任务中心,负责:

  • 清理过期 key
  • 更新 LRU 时钟
  • 处理 BGSAVE 和 AOF 重写
  • 主从复制心跳
  • 内存统计
  • 等等

1ms 后首次触发,之后根据返回值决定下次触发间隔。

5.7 注册文件事件 - 接受连接

1for (j = 0; j < server.ipfd_count; j++) {
2    if (aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE,
3        acceptTcpHandler,NULL) == AE_ERR)
4    {
5        serverPanic("Unrecoverable error creating server.ipfd file event.");
6    }
7}

把监听 socket 的读事件注册到事件循环,回调是 acceptTcpHandler。有新连接时触发,accept 后创建 client 结构体。

5.8 初始化其他模块

1if (server.cluster_enabled) clusterInit();  // 集群
2replicationScriptCacheInit();               // 复制脚本缓存
3scriptingInit(1);                           // Lua 脚本
4slowlogInit();                              // 慢查询日志
5latencyMonitorInit();                       // 延迟监控
6bioInit();                                  // 后台 IO 线程

第六步:loadDataFromDisk - 加载数据

 1void loadDataFromDisk(void) {
 2    long long start = ustime();
 3    
 4    if (server.aof_state == AOF_ON) {
 5        // AOF 模式,加载 AOF 文件
 6        if (loadAppendOnlyFile(server.aof_filename) == C_OK)
 7            serverLog(LL_NOTICE,"DB loaded from append only file: %.3f seconds",
 8                (float)(ustime()-start)/1000000);
 9    } else {
10        // RDB 模式,加载 RDB 文件
11        rdbSaveInfo rsi = RDB_SAVE_INFO_INIT;
12        if (rdbLoad(server.rdb_filename,&rsi) == C_OK) {
13            serverLog(LL_NOTICE,"DB loaded from disk: %.3f seconds",
14                (float)(ustime()-start)/1000000);
15        }
16    }
17}

如果 AOF 开了,优先加载 AOF,因为 AOF 数据更完整。否则加载 RDB。

加载数据可能很慢,取决于数据量和磁盘速度。期间 Redis 会打印进度日志。

第七步:进入事件循环

1aeSetBeforeSleepProc(server.el, beforeSleep);
2aeSetAfterSleepProc(server.el, afterSleep);
3aeMain(server.el);
4aeDeleteEventLoop(server.el);

beforeSleep 在每轮事件循环开始前执行,主要做:

  • 处理待写回的客户端数据
  • 快速处理一些过期 key
  • 解除阻塞客户端

aeMain 就是那个死循环:

1void aeMain(aeEventLoop *eventLoop) {
2    eventLoop->stop = 0;
3    while (!eventLoop->stop) {
4        if (eventLoop->beforesleep != NULL)
5            eventLoop->beforesleep(eventLoop);
6        aeProcessEvents(eventLoop, AE_ALL_EVENTS|AE_CALL_AFTER_SLEEP);
7    }
8}

至此,Redis 开始接受连接,处理请求。

启动流程梳理

 1main()
 2  
 3  ├─ 基础初始化(时区、随机种子、哈希种子)
 4  
 5  ├─ initServerConfig()     默认配置
 6  
 7  ├─ loadServerConfig()     加载配置文件和命令行参数
 8  
 9  ├─ daemonize()            守护进程化(可选)
10  
11  ├─ initServer()           核心!
12       ├─ 信号处理
13       ├─ 创建事件循环
14       ├─ 监听端口
15       ├─ 初始化数据库
16       ├─ 注册 serverCron 时间事件
17       ├─ 注册 acceptTcpHandler 文件事件
18       └─ 初始化集群、Lua、慢日志等模块
19  
20  ├─ loadDataFromDisk()     加载 RDB/AOF
21  
22  └─ aeMain()               进入事件循环,开始服务

一些有意思的细节

32 位实例的内存限制

1if (server.arch_bits == 32 && server.maxmemory == 0) {
2    serverLog(LL_WARNING,"Warning: 32 bit instance detected but no memory limit set. Setting 3 GB maxmemory limit with 'noeviction' policy now.");
3    server.maxmemory = 3072LL*(1024*1024);
4    server.maxmemory_policy = MAXMEMORY_NO_EVICTION;
5}

32 位进程地址空间只有 4GB,不限制的话容易 OOM。Redis 自动设置 3GB 限制。

进程标题

1redisSetProcTitle(argv[0]);

设置进程标题,ps 能看到 “redis-server *:6379” 这样的名字,方便排查。

1redisAsciiArt();

启动时打印那个 Redis 的 ASCII art logo。

— END —