DEV Community

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

Posted on

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

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

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

先看 main 函数的全貌

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

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

代码骨架是这样的:

int main(int argc, char **argv) {
    // 1. 基础初始化
    initServerConfig();
    moduleInitModulesSystem();

    // Sentinel 模式初始化(如果是)
    if (server.sentinel_mode) {
        initSentinelConfig();
        initSentinel();
    }

    // 2. 加载配置文件和命令行参数
    resetServerSaveParams();
    loadServerConfig(configfile, options);

    // 3. 以守护进程方式运行(如果配置了)
    if (background) daemonize();

    // 4. 初始化服务器
    initServer();

    // 5. 从磁盘加载数据
    loadDataFromDisk();

    // 6. 进入事件循环
    aeMain(server.el);

    return 0;
}
Enter fullscreen mode Exit fullscreen mode

下面逐个展开。

第一步:基础初始化

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);
Enter fullscreen mode Exit fullscreen mode

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

然后判断是否是 Sentinel 模式:

server.sentinel_mode = checkForSentinelMode(argc,argv);
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

    // 生成运行 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;

    // ... 还有很多
}
Enter fullscreen mode Exit fullscreen mode

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

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

还有命令表的初始化:

server.commands = dictCreate(&commandTableDictType,NULL);
server.orig_commands = dictCreate(&commandTableDictType,NULL);
populateCommandTable();
Enter fullscreen mode Exit fullscreen mode

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

第三步:加载配置

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

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);
}
Enter fullscreen mode Exit fullscreen mode

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

redis-server /etc/redis.conf --port 6380 --maxmemory 1gb
Enter fullscreen mode Exit fullscreen mode

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

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

第四步:守护进程化

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

int background = server.daemonize && !server.supervised;
if (background) daemonize();
Enter fullscreen mode Exit fullscreen mode

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

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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

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

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

5.1 信号处理

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

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

server.clients = listCreate();              // 客户端列表
server.clients_to_close = listCreate();     // 待关闭客户端
server.slaves = listCreate();               // 从节点列表
server.monitors = listCreate();             // monitor 客户端
server.clients_pending_write = listCreate();// 待写回客户端
server.unblocked_clients = listCreate();    // 已取消阻塞客户端
Enter fullscreen mode Exit fullscreen mode

5.3 创建事件循环

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

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

5.4 监听端口

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

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

如果配置了 Unix socket:

if (server.unixsocket != NULL) {
    server.sofd = anetUnixServer(server.neterr, server.unixsocket,
        server.unixsocketperm, server.tcp_backlog);
}
Enter fullscreen mode Exit fullscreen mode

5.5 初始化数据库

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;
}
Enter fullscreen mode Exit fullscreen mode

每个 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

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

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

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

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

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

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.");
    }
}
Enter fullscreen mode Exit fullscreen mode

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

5.8 初始化其他模块

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

第六步:loadDataFromDisk - 加载数据

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);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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

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

第七步:进入事件循环

aeSetBeforeSleepProc(server.el, beforeSleep);
aeSetAfterSleepProc(server.el, afterSleep);
aeMain(server.el);
aeDeleteEventLoop(server.el);
Enter fullscreen mode Exit fullscreen mode

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

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

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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

启动流程梳理

main()
  │
  ├─ 基础初始化(时区、随机种子、哈希种子)
  │
  ├─ initServerConfig()    ← 默认配置
  │
  ├─ loadServerConfig()    ← 加载配置文件和命令行参数
  │
  ├─ daemonize()           ← 守护进程化(可选)
  │
  ├─ initServer()          ← 核心!
  │     ├─ 信号处理
  │     ├─ 创建事件循环
  │     ├─ 监听端口
  │     ├─ 初始化数据库
  │     ├─ 注册 serverCron 时间事件
  │     ├─ 注册 acceptTcpHandler 文件事件
  │     └─ 初始化集群、Lua、慢日志等模块
  │
  ├─ loadDataFromDisk()    ← 加载 RDB/AOF
  │
  └─ aeMain()              ← 进入事件循环,开始服务
Enter fullscreen mode Exit fullscreen mode

一些有意思的细节

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);
    server.maxmemory_policy = MAXMEMORY_NO_EVICTION;
}
Enter fullscreen mode Exit fullscreen mode

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

进程标题

redisSetProcTitle(argv[0]);
Enter fullscreen mode Exit fullscreen mode

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

ASCII Art Logo

redisAsciiArt();
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)