第 21 章

源码阅读环境:编译、调试与核心文件地图

第21章 源码阅读环境:编译、调试与核心文件地图

深入理解 Redis 必须直面源码。本章建立完整的源码阅读环境,从编译选项到 GDB 单步调试,从文件地图到启动链路,帮助读者以工程师视角而非用户视角理解 Redis。


21.1 获取源码

Redis 源码托管于 GitHub,主仓库地址为 https://github.com/redis/redis。生产环境建议锁定具体版本标签,避免使用不稳定的 unstable 分支。

git clone https://github.com/redis/redis.git
cd redis
git checkout 7.2.0        # 锁定到 7.2.0 稳定版
git log --oneline -10     # 查看最近提交,了解版本历史

7.x 系列相对 6.x 的主要变化:


21.2 编译:保留调试信息

默认 make 使用 -O2 优化,编译器会内联函数、重排代码,调试体验极差。源码阅读时强烈建议关闭优化:

# 关闭优化,保留完整调试符号
make CFLAGS="-g -O0" MALLOC=libc

# 如果使用 jemalloc(Linux 默认)
make CFLAGS="-g -O0" MALLOC=jemalloc

# 并行编译加速
make -j$(nproc) CFLAGS="-g -O0"

# 运行测试套件(需要 Tcl 8.5+)
make test

# 仅运行特定测试
./runtest tests/unit/type/hash.tcl
./runtest tests/unit/expire.tcl

编译产物在 src/ 目录:

启动服务端(调试模式):

src/redis-server --loglevel debug --save "" --appendonly no
# --save ""        禁用 RDB,避免 fork 影响调试
# --appendonly no  禁用 AOF
# --loglevel debug 输出详细日志

21.3 核心文件地图

Redis 7.2 源码约 15 万行 C 代码,分布在 src/ 目录的 150+ 个文件中。以下是最重要的文件分类:

核心框架层

文件 行数(约) 作用
server.h 3800 全局数据结构定义:redisServer、client、redisObject、dict、listpack 等所有核心结构声明
server.c 7500 服务端主体:main()、initServer()、serverCron()、beforeSleep()、命令表
ae.c 800 事件循环:aeEventLoop、aeProcessEvents、aeCreateFileEvent、aeCreateTimeEvent
ae_epoll.c 150 epoll 具体实现(Linux)
ae_kqueue.c 150 kqueue 具体实现(macOS/BSD)
ae_select.c 100 select 降级实现
ae_evport.c 150 Solaris event port 实现

网络与协议层

文件 行数(约) 作用
networking.c 4200 客户端连接管理、RESP 协议解析、readQueryFromClient、addReply 系列函数
resp3.c 200 RESP3 协议扩展(Redis 6+)
tls.c 1200 TLS/SSL 支持(需要 OpenSSL)
unix.c 100 Unix Domain Socket 支持

数据类型命令层

文件 行数(约) 作用
t_string.c 900 String 命令:set/get/incr/append/getrange/setrange 等
t_hash.c 800 Hash 命令:hset/hget/hmset/hgetall 等
t_list.c 700 List 命令:lpush/rpush/lrange/blpop 等
t_set.c 900 Set 命令:sadd/smembers/sinter/sunion 等
t_zset.c 2800 ZSet 命令 + 跳表完整实现(zskiplist/zskiplistNode)
t_stream.c 3500 Stream 命令:xadd/xread/xgroup/xack 等
t_xset.c 100 扩展集合

底层数据结构层

文件 行数(约) 作用
sds.c / sds.h 1000 SDS 动态字符串:sdsnew/sdscat/sdsMakeRoomFor
dict.c / dict.h 1500 渐进式哈希表:dictCreate/dictAdd/dictFind/dictRehash
listpack.c / listpack.h 1100 listpack 紧凑列表(7.0 主力编码格式)
ziplist.c 1500 ziplist(7.0 起逐步废弃,保留兼容性)
quicklist.c 1200 quicklist:listpack 节点的双向链表
intset.c 400 intset:整数集合(小 Set 编码)
skiplist(内嵌 t_zset.c) - 跳表实现
rax.c / rax.h 1600 Radix Tree(前缀树),用于 Stream、集群路由

持久化层

文件 行数(约) 作用
rdb.c 3500 RDB 持久化:rdbSave/rdbLoad/rdbSaveObject
rdb.h 200 RDB 格式常量定义
aof.c 3000 AOF 持久化:feedAppendOnlyFile/rewriteAppendOnlyFile
rio.c / rio.h 500 流式 I/O 抽象:支持 file/buffer/fd 多种后端

复制与集群层

文件 行数(约) 作用
replication.c 5500 主从复制:PSYNC 协议、backlog、部分同步
sentinel.c 5000 哨兵模式:故障检测、选举、通知
cluster.c 10000+ 集群模式(最大文件):Gossip、槽迁移、故障转移
cluster.h 500 集群数据结构定义

内存管理层

文件 行数(约) 作用
object.c 1200 redisObject 创建/释放/引用计数/类型检查/编码转换
db.c 1000 dbAdd/dbDelete/expireIfNeeded/lookupKey/selectDb
expire.c 300 定期删除:activeExpireCycle
evict.c 600 内存淘汰:8种策略实现,performEvictions
lazyfree.c 400 后台惰性删除:bioCreateLazyFreeJob
zmalloc.c 400 内存分配封装:zmalloc/zfree/used_memory 统计
memory.c 500 MEMORY 命令、碎片统计、activedefrag

后台任务层

文件 行数(约) 作用
bio.c 400 后台 I/O 线程:3类任务(fsync/lazyFree/closeFd)
threads_mngr.c 200 I/O 多线程管理(Redis 6+)

21.4 GDB 调试实战

调试 SET 命令

# 终端1:启动 GDB
gdb src/redis-server

(gdb) break setCommand           # 在 t_string.c 的 setCommand 设置断点
(gdb) break t_string.c:100       # 按行号设置断点(需先查看行号)
(gdb) run redis.conf             # 启动 server

# 终端2:发送命令
redis-cli SET hello world

# 终端1:GDB 停在断点
(gdb) bt                         # 查看调用栈(backtrace)
(gdb) frame 0                    # 切换到第0帧
(gdb) info args                  # 查看函数参数
(gdb) p c->argc                  # 查看客户端参数数量(应为3: SET hello world)
(gdb) p c->argv[0]->ptr          # 查看第一个参数(命令名)
(gdb) p c->argv[1]->ptr          # 查看 key
(gdb) p c->argv[2]->ptr          # 查看 value
(gdb) n                          # 单步执行(next,不进入函数)
(gdb) s                          # 单步进入函数(step)
(gdb) c                          # 继续执行(continue)

查看 redisObject 结构

(gdb) p *c->argv[1]              # 打印 redisObject(key)
# 输出类似:
# {type = 0, encoding = 8, lru = 12345678, refcount = 1, ptr = 0x7f...}

(gdb) p (char*)c->argv[1]->ptr  # 如果是 embstr/raw,直接打印字符串
(gdb) x/10c c->argv[1]->ptr     # 以字符格式查看内存(前10字节)

调试时间事件(serverCron)

(gdb) break serverCron
(gdb) commands                   # 设置断点命中时自动执行命令
> print server.hz
> print server.unixtime
> continue
> end

常用 GDB 命令速查

break func      在函数入口设断点
break file:line 在指定行设断点
watch expr      监视表达式变化(写断点)
rwatch expr     监视表达式被读取(读断点)
info break      列出所有断点
delete 1        删除断点1
print expr      打印表达式值
x/Nfu addr      检查内存(N=数量, f=格式, u=单位)
ptype var       打印变量类型
whatis var      查看变量类型(简版)
set var=value   修改变量值(注入数据)
finish          执行完当前函数并返回
up/down         切换调用栈帧
info locals     显示当前帧所有局部变量

21.5 启动链路全解析

Redis 的启动链路从 main() 开始,理解这个链路对理解 Redis 架构至关重要。

// server.c
int main(int argc, char **argv) {
    // 1. 时区初始化
    setlocale(LC_COLLATE, "");
    tzset();

    // 2. 内存分配异常处理器
    zmalloc_set_oom_handler(redisOutOfMemoryHandler);

    // 3. 随机种子初始化
    srand(time(NULL)^getpid());

    // 4. 默认配置初始化(填充 server 全局结构)
    initServerConfig();

    // 5. 模块系统初始化(Module API)
    moduleInitModulesSystem();

    // 6. 解析命令行参数,加载配置文件
    loadServerConfig(configfile, options);

    // 7. 哨兵/集群模式检测
    if (server.sentinel_mode) initSentinelConfig();

    // 8. ACL 系统初始化
    ACLInit();

    // 9. 正式初始化服务端(创建 eventloop、监听端口等)
    initServer();

    // 10. 模块加载
    moduleLoadFromQueue();

    // 11. 从磁盘加载数据(RDB 或 AOF)
    loadDataFromDisk();

    // 12. 集群/哨兵模式特殊初始化
    if (server.cluster_enabled) clusterInit();

    // 13. 进入事件循环主体(永不返回)
    aeSetBeforeSleepProc(server.el, beforeSleep);
    aeSetAfterSleepProc(server.el, afterSleep);
    aeMain(server.el);

    // 14. 事件循环结束(shutdown时)
    aeDeleteEventLoop(server.el);
    return 0;
}

initServer() 关键步骤

void initServer(void) {
    // 创建事件循环(核心!)
    server.el = aeCreateEventLoop(server.maxclients + CONFIG_FDSET_INCR);

    // 创建数据库数组(默认16个DB)
    server.db = zmalloc(sizeof(redisDb) * server.dbnum);
    for (int i = 0; i < server.dbnum; i++) {
        server.db[i].dict = dictCreate(&dbDictType);
        server.db[i].expires = dictCreate(&dbExpiresDictType);
        server.db[i].blocking_keys = dictCreate(&keylistDictType);
        server.db[i].watched_keys = dictCreate(&keylistDictType);
        server.db[i].id = i;
    }

    // 监听 TCP 端口
    listenToPort(server.port, &server.ipfd);

    // 注册 serverCron 时间事件(每1ms检查一次,内部控制实际执行频率)
    aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL);

    // 注册 accept 文件事件
    for (int i = 0; i < server.ipfd.count; i++) {
        aeCreateFileEvent(server.el, server.ipfd.fd[i], AE_READABLE,
                          acceptTcpHandler, NULL);
    }

    // 初始化 LRU 时钟
    server.lruclock = getLRUClock();
}

serverCron 任务调度

serverCron 是 Redis 的心跳函数,每次 aeProcessEvents 循环都会检查是否到期执行:

// 实际执行频率由 server.hz 控制(默认10,即100ms一次)
// 但通过 run_with_period 宏控制不同任务的执行间隔
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
    // 每次都执行
    server.unixtime = time(NULL);
    server.mstime = mstime();
    server.ustime = ustime();
    updateCachedTime(0);

    // 每100ms
    run_with_period(100) {
        trackInstantaneousMetric(STATS_METRIC_COMMAND, ...);
        activeExpireCycle(ACTIVE_EXPIRE_CYCLE_FAST);  // 快速过期扫描
    }

    // 每1000ms(1秒)
    run_with_period(1000) {
        dbCron();          // DB 相关维护
        replicationCron(); // 主从复制心跳
    }

    // 每5000ms
    run_with_period(5000) {
        // ... 统计数据更新
    }

    // BGSAVE / BGREWRITEAOF 子进程检查
    if (server.rdb_child_pid != -1 || server.aof_child_pid != -1) {
        // waitpid 检查子进程是否完成
    }

    return 1000 / server.hz;  // 返回下次执行的毫秒数
}

21.6 RESP 协议解析源码

理解 networking.c 中的 readQueryFromClient 是理解 Redis 网络模型的基础:

// 客户端发送 "SET hello world\r\n" 后触发
void readQueryFromClient(connection *conn) {
    client *c = connGetPrivateData(conn);

    // 从连接读取数据到 querybuf
    nread = connRead(c->conn, c->querybuf + qblen, readlen);

    // 解析 RESP 协议
    processInputBuffer(c);
}

void processInputBuffer(client *c) {
    while (c->qb_pos < sdslen(c->querybuf)) {
        if (c->reqtype == PROTO_REQ_MULTIBULK) {
            // 解析 *3\r\n$3\r\nSET\r\n$5\r\nhello\r\n$5\r\nworld\r\n
            if (processMultibulkBuffer(c) != C_OK) break;
        }
        // 命令解析完成后调用 processCommand
        if (c->argc > 0) {
            processCommand(c);
        }
    }
}

21.7 阅读源码的方法论

1. 从命令入手:每个 Redis 命令对应一个 C 函数,在 server.credisCommandTable 数组中定义:

struct redisCommand redisCommandTable[] = {
    {"get",    getCommand,    2, "read-only fast @string", ...},
    {"set",    setCommand,   -3, "write use-memory @string", ...},
    {"hset",   hsetCommand,  -4, "write use-memory fast @hash", ...},
    // ...
};

2. 利用 ctags/cscope

ctags -R src/           # 生成 tags 文件
# Vim 中:Ctrl+] 跳转定义,Ctrl+T 返回

3. 使用 VSCode + clangd:配置 compile_commands.json,获得精确的跳转和类型推导。

4. 关注 redisObject:几乎所有数据最终存储在 robj(redisObject)中,理解它的 type/encoding/ptr 三元组是理解所有数据结构编码转换的基础。

typedef struct redisObject {
    unsigned type:4;      // OBJ_STRING/OBJ_LIST/OBJ_SET/OBJ_ZSET/OBJ_HASH
    unsigned encoding:4;  // OBJ_ENCODING_RAW/EMBSTR/INT/LISTPACK/SKIPLIST...
    unsigned lru:24;      // LRU 时间戳 或 LFU 计数器
    int refcount;         // 引用计数(共享对象:0-9999整数)
    void *ptr;            // 指向实际数据(或直接存整数值)
} robj;

本章小结

本章评分
4.7  / 5  (9 评分)

💬 留言讨论