Redis源码探究系列—启动流程全解析(server.c 到 main 函数)

欢迎各位同学关注我哦~
在这个 AI 喧嚣的时代
不忘初心,戒骄戒躁,认真沉淀
Deepincode
启动一个Redis实例看起来很简单,`redis-server` 一敲就完了。但你有没有想过,从按下回车到 Redis 开始接受连接,中间会发生啥事儿呢?

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

先看 main 函数的全貌

在开始详细了解启动流程之前呢,我们可以从main函数入手,大体上看看这个方法中干了哪些事情,

main 函数的核心流程可以概括为:

初始化基础库 -> 判断Sentinel模式 -> 初始化默认配置 -> 加载配置 -> 守护进程化 -> 初始化服务器 -> 加载数据 -> 进入事件循环

代码的主体骨架是下面这样的,其中去掉了一些错误处理、日志输出、条件编译等代码,只保留关键流程~

int main(int argc, char **argv) {
    // ==================== 1. 基础初始化 ====================
    setlocale(LC_COLLATE,"");              // 设置本地化环境,影响字符串比较
    tzset();                               // 初始化时区信息,填充timezone全局变量
    zmalloc_set_oom_handler(redisOutOfMemoryHandler);  // 设置内存分配失败的回调函数
    srand(time(NULL)^getpid());            // 初始化随机数种子,用时间+进程ID增加随机性
    gettimeofday(&tv,NULL);                // 获取当前时间,用于后续计时
    
    char hashseed[16];                     // 生成16字节的哈希种子
    getRandomHexChars(hashseed,sizeof(hashseed));  // 随机生成十六进制字符串
    dictSetHashFunctionSeed((uint8_t*)hashseed);   // 设置字典哈希函数种子,防止哈希攻击
    
    // ==================== 2. 判断是否 Sentinel 模式 ====================
    server.sentinel_mode = checkForSentinelMode(argc,argv);  // 检查是否以Sentinel模式启动
    
    // ==================== 3. 初始化默认配置 ====================
    initServerConfig();                    // 初始化server结构体的默认配置值
    moduleInitModulesSystem();             // 初始化模块子系统
    
    // 保存可执行文件路径和命令行参数,用于后续重启
    server.executable = getAbsolutePath(argv[0]);  // 获取绝对路径
    server.exec_argv = zmalloc(sizeof(char*)*(argc+1));  // 分配参数数组
    for (j = 0; j < argc; j++) 
        server.exec_argv[j] = zstrdup(argv[j]);   // 复制每个参数
    
    // ==================== 4. Sentinel 模式特殊初始化 ====================
    if (server.sentinel_mode) {
        initSentinelConfig();              // Sentinel专用配置
        initSentinel();                    // 初始化Sentinel数据结构
    }
    
    // ==================== 5. 加载配置 ====================
    resetServerSaveParams();               // 重置RDB save参数为空
    loadServerConfig(configfile, options); // 加载配置文件和命令行参数
    
    // ==================== 6. 守护进程化 ====================
    server.supervised = redisIsSupervised(server.supervised_mode);  // 检测是否被 supervisor 管理
    int background = server.daemonize && !server.supervised;  // 判断是否后台运行
    if (background) 
        daemonize();                       // fork 子进程并脱离终端
    
    // ==================== 7. 初始化服务器 ====================
    initServer();                          // 核心!创建事件循环、监听端口、初始化数据库等
    if (background || server.pidfile) 
        createPidFile();                   // 创建pid文件
    redisSetProcTitle(argv[0]);            // 设置进程标题,方便ps查看
    redisAsciiArt();                       // 打印Redis ASCII art logo
    checkTcpBacklogSettings();             // 检查系统TCP backlog设置,打印警告
    
    // ==================== 8. 加载数据 ====================
    if (!server.sentinel_mode) {           // Sentinel模式不加载数据
        serverLog(LL_WARNING,"Server initialized");
        moduleLoadFromQueue();             // 加载配置中指定的模块
        loadDataFromDisk();                // 加载RDB或AOF文件
    } else {
        sentinelIsRunning();               // Sentinel启动完成回调
    }
    
    // ==================== 9. 进入事件循环 ====================
    aeSetBeforeSleepProc(server.el, beforeSleep);  // 设置每轮循环前的回调
    aeSetAfterSleepProc(server.el, afterSleep);    // 设置每轮循环后的回调
    aeMain(server.el);                     // 进入主事件循环,开始处理请求
    aeDeleteEventLoop(server.el);          // 循环结束后清理(通常不会执行到这里)
    
    return 0;
}

下面逐个展开每个步骤的具体内容。

第一步:基础初始化

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

setlocale(LC_COLLATE,"");
tzset();                    // 时区初始化
zmalloc_set_oom_handler(redisOutOfMemoryHandler);  // 内存不足处理
srand(time(NULL)^getpid()); // 随机种子
gettimeofday(&tv,NULL);

// 生成哈希种子,用于字典的哈希函数
char hashseed[16];
getRandomHexChars(hashseed,sizeof(hashseed));
dictSetHashFunctionSeed((uint8_t*)hashseed);

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

第二步:判断 Sentinel 模式

server.sentinel_mode = checkForSentinelMode(argc,argv);

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

int checkForSentinelMode(int argc, char **argv) {
    int j;
    if (strstr(argv[0],"redis-sentinel") != NULL) return 1;
    for (j = 1; j < argc; j++)
        if (!strcmp(argv[j],"--sentinel")) return 1;
    return 0;
}

这个判断必须在 initServerConfig() 之前完成,因为后续的初始化流程会根据 server.sentinel_mode 走不同分支:

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

这个函数大约 200 行,主要就是给 server 这个全局结构体的各个字段赋默认值。
这些默认值可以通过 redis.conf 配置文件或命令行参数覆盖。采用"先设默认值,再加载配置"的方式,必须确保所有字段都有确定的初始值。

void initServerConfig(void) {
    // 互斥锁初始化
    pthread_mutex_init(&server.next_client_id_mutex,NULL);
    pthread_mutex_init(&server.lruclock_mutex,NULL);
    pthread_mutex_init(&server.unixtime_mutex,NULL);
    
    updateCachedTime();  // 更新缓存时间
    
    // 生成运行 ID
    getRandomHexChars(server.runid,CONFIG_RUN_ID_SIZE);
    server.runid[CONFIG_RUN_ID_SIZE] = '\0';
    
    // 基础配置
    server.port = CONFIG_DEFAULT_SERVER_PORT;  // 6379
    server.dbnum = CONFIG_DEFAULT_DBNUM;       // 16
    server.verbosity = CONFIG_DEFAULT_VERBOSITY;
    server.maxidletime = CONFIG_DEFAULT_CLIENT_TIMEOUT;
    server.tcpkeepalive = CONFIG_DEFAULT_TCP_KEEPALIVE;
    
    // AOF 相关
    server.aof_state = AOF_OFF;
    server.aof_fsync = CONFIG_DEFAULT_AOF_FSYNC;
    
    // RDB 相关
    server.rdb_filename = zstrdup(CONFIG_DEFAULT_RDB_FILENAME);
    server.rdb_compression = CONFIG_DEFAULT_RDB_COMPRESSION;
    
    // 内存相关
    server.maxmemory = CONFIG_DEFAULT_MAXMEMORY;
    server.maxmemory_policy = CONFIG_DEFAULT_MAXMEMORY_POLICY;
    
    // ... 还有很多
}

这个方法中可以看到一些我们在面试中经常被提到的RDB持久化的默认的save策略:

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

还有命令表的初始化:

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

命令表是一个字典(dict),key 是命令名(如 “get”、“set”),value 是 redisCommand 结构体指针。当客户端发送 SET key value 时,Redis 会:

  1. 解析命令名 “set”
  2. server.commands 字典中查找对应的 redisCommand
  3. 检查参数个数、权限、标志等
  4. 调用 redisCommand.proc 指向的函数(这里是 setCommand
客户端命令 "SET foo bar"
        ↓
解析命令名 "set"
        ↓
dictFind(server.commands, "set")
        ↓
获取 redisCommand 结构体
        ↓
检查 arity=-3(至少 3 个参数)✓
检查 flags 中 CMD_WRITE 标志 ✓
        ↓
调用 setCommand(client)
        ↓
返回 "+OK\r\n"

这种设计让 Redis 的命令处理非常灵活:

  • 新增命令只需在 redisCommandTable[] 加一行
  • rename-command 可以修改命令名或禁用命令
  • 命令标志系统控制着权限、复制、脚本执行等行为

之后调用 moduleInitModulesSystem() 初始化模块系统。

第四步:Sentinel 模式特殊初始化

如果是 Sentinel 模式,需要提前初始化 Sentinel 相关数据结构,因为后续加载配置文件时会往里面填充要监控的主节点信息:

if (server.sentinel_mode) {
    initSentinelConfig();
    initSentinel();
}

第五步:加载配置

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

if (argc >= 2) {
    j = 1;
    sds options = sdsempty();
    char *configfile = NULL;

    // 第一个参数如果不是 --开头,就当配置文件路径
    if (argv[j][0] != '-' || argv[j][1] != '-') {
        configfile = argv[j];
        server.configfile = getAbsolutePath(configfile);
        j++;
    }

    // 剩下的参数转成配置字符串
    // 比如 --port 6380 转成 "port 6380\n"
    while(j != argc) {
        if (argv[j][0] == '-' && argv[j][1] == '-') {
            if (sdslen(options)) options = sdscat(options,"\n");
            options = sdscat(options,argv[j]+2);
            options = sdscat(options," ");
        } else {
            options = sdscatrepr(options,argv[j],strlen(argv[j]));
            options = sdscat(options," ");
        }
        j++;
    }
    
    // 加载配置
    loadServerConfig(configfile, options);
}

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

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

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

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

第六步:守护进程化

首先检测是否被 supervisor 管理:

server.supervised = redisIsSupervised(server.supervised_mode);
int background = server.daemonize && !server.supervised;
if (background) daemonize();

如果配置了 daemonize yes 且不在supervisor模式下,Redis会调用 daemonize() 函数:

void daemonize(void) {
    int fd;

    if (fork() != 0) exit(0);  // 父进程退出
    setsid();                   // 创建新会话
    
    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);
    }
}

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

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

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

7.1 信号处理

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

7.2 创建共享对象

createSharedObjects();

创建一些常用的小整数、常用字符串(如OK、ERR、命令回复等)的共享对象,避免重复分配。

7.3 调整文件描述符限制

adjustOpenFilesLimit();

根据 maxclients 配置调整进程能打开的最大文件描述符数量。

7.4 创建事件循环和各种链表

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

server.clients = listCreate();              // 客户端列表
server.clients_index = raxNew();            // 客户端索引(rax树)
server.clients_to_close = listCreate();     // 待关闭客户端
server.slaves = listCreate();               // 从节点列表
server.monitors = listCreate();             // monitor客户端
server.clients_pending_write = listCreate();// 待写回客户端
server.unblocked_clients = listCreate();    // 已取消阻塞客户端
server.ready_keys = listCreate();           // 就绪的key
server.clients_waiting_acks = listCreate(); // 等待ACK的客户端

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

7.5 监听端口

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

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

如果配置了Unix socket:

if (server.unixsocket != NULL) {
    unlink(server.unixsocket);
    server.sofd = anetUnixServer(server.neterr, server.unixsocket,
        server.unixsocketperm, server.tcp_backlog);
    if (server.sofd == ANET_ERR) {
        serverLog(LL_WARNING, "Opening Unix socket: %s", server.neterr);
        exit(1);
    }
    anetNonBlock(NULL, server.sofd);
}

如果没有监听任何端口或socket,直接退出:

if (server.ipfd_count == 0 && server.sofd < 0) {
    serverLog(LL_WARNING, "Configured to not listen anywhere, exiting.");
    exit(1);
}

7.6 初始化数据库

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

server.db = zmalloc(sizeof(redisDb)*server.dbnum);

for (j = 0; j < server.dbnum; j++) {
    server.db[j].dict = dictCreate(&dbDictType,NULL);        // 数据字典
    server.db[j].expires = dictCreate(&keyptrDictType,NULL); // 过期时间字典
    server.db[j].blocking_keys = dictCreate(&keylistDictType,NULL);
    server.db[j].ready_keys = dictCreate(&objectKeyPointerValueDictType,NULL);
    server.db[j].watched_keys = dictCreate(&keylistDictType,NULL);
    server.db[j].id = j;
    server.db[j].avg_ttl = 0;
    server.db[j].defrag_later = listCreate();
}

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

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

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

7.7 注册时间事件 - serverCron

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

serverCron 是 Redis 的定时任务中心,负责下面这几件事:

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

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

关于这个方法我们会在后面的文章中专门分析,敬请期待~

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

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

// Unix socket也注册
if (server.sofd > 0 && aeCreateFileEvent(server.el,server.sofd,AE_READABLE,
                                         acceptUnixHandler,NULL) == AE_ERR)
    serverPanic("Unrecoverable error creating server.sofd file event.");

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

7.9 打开 AOF 文件

if (server.aof_state == AOF_ON) {
    server.aof_fd = open(server.aof_filename,
                         O_WRONLY|O_APPEND|O_CREAT,0644);
    if (server.aof_fd == -1) {
        serverLog(LL_WARNING, "Can't open the append-only file: %s",
                  strerror(errno));
        exit(1);
    }
}

7.10 32 位实例的内存限制

if (server.arch_bits == 32 && server.maxmemory == 0) {
    serverLog(LL_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 = MAXMEMORY_NO_EVICTION;
}

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

7.11 初始化其他模块

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

第八步:启动后的收尾工作

if (background || server.pidfile) createPidFile();  // 创建pid文件
redisSetProcTitle(argv[0]);                         // 设置进程标题
redisAsciiArt();                                    // 打印ASCII art
checkTcpBacklogSettings();                          // 检查TCP backlog设置

进程标题

redisSetProcTitle(argv[0]);

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

ASCII Art Logo

redisAsciiArt();

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

第九步:loadDataFromDisk - 加载数据

数据加载只在非 Sentinel 模式下执行, 因为Sentinel 不存储业务数据,
它只维护自己的状态信息(监控的主节点列表、其他 Sentinel 节点、failover 状态等)。这些状态信息存在 sentinel.masters 字典中,而不是 Redis 的数据库(server.db[])。

if (!server.sentinel_mode) {
    serverLog(LL_WARNING,"Server initialized");
    moduleLoadFromQueue();      // 加载模块
    loadDataFromDisk();         // 加载数据
} else {
    sentinelIsRunning();
}
void loadDataFromDisk(void) {
    long long start = ustime();
    
    if (server.aof_state == AOF_ON) {
        // AOF模式,加载AOF文件
        if (loadAppendOnlyFile(server.aof_filename) == C_OK)
            serverLog(LL_NOTICE,"DB loaded from append only file: %.3f seconds",
                (float)(ustime()-start)/1000000);
    } else {
        // RDB模式,加载RDB文件
        rdbSaveInfo rsi = RDB_SAVE_INFO_INIT;
        if (rdbLoad(server.rdb_filename,&rsi) == C_OK) {
            serverLog(LL_NOTICE,"DB loaded from disk: %.3f seconds",
                (float)(ustime()-start)/1000000);
        }
    }
}

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

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

如果是集群模式,还会验证数据是否都在DB 0:

if (server.cluster_enabled) {
    if (verifyClusterConfigWithData() == C_ERR) {
        serverLog(LL_WARNING,
                  "You can't have keys in a DB different than DB 0 when in "
                  "Cluster mode. Exiting.");
        exit(1);
    }
}

第十步:进入事件循环

aeSetBeforeSleepProc(server.el, beforeSleep);
aeSetAfterSleepProc(server.el, afterSleep);
aeMain(server.el);
aeDeleteEventLoop(server.el);

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

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

afterSleep 在事件循环唤醒后执行。

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);
    }
}

请记住上面的这三个方法,在后续的文章中我们会反复提到,哈哈哈。

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

启动流程梳理

在这里插入图片描述

欢迎各位同学关注我哦~
在这个 AI 喧嚣的时代
不忘初心,戒骄戒躁,认真沉淀
Deepincode
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值