17

一直很羡慕那些能读 Redis 源码的童鞋,也一直想自己解读一遍,但迫于 C 大魔王的压力,解读日期遥遥无期。

相信很多小伙伴应该也都对或曾对源码感兴趣,但一来觉得自己不会 C 语言,二来也不知从何入手,结果就和博主一样,一拖再拖。

但正所谓,种一棵树的最好时间是十年前,其次就是现在。如果你真的想了解 Redis 源码,又有缘看到了这系列博文,何不跟着博主一起解读 Redis 源码,做个同行人呢?接下来,就让我们一起走入 Redis 的源码世界吧。

决定要读了,下一步就是如何读。从 github 上克隆下来源码,一看 src 目录,望天,104 个文件,我该从哪个文件开始呢?一个个文件看?不行不行,这样对我毫无诱惑力,没有诱惑力,怎么能战胜游戏、小说对我的吸引呢?苦苦思考,不得其解。然后突然想起来 HTTP 协议的那个经典面试题:从浏览器输入网址,到页面展示,这个过程发生了什么?

把这个面试题换成 Redis:输入开启 Redis 服务的命令,回车,到成功启动 Redis 服务,这个过程发生了什么?

很好,这个问题成功吸引到我了。就让我们从源码中找出这个问题的答案吧。后续的所有文章我们都尝试通过提出问题,解答问题的步骤,来深入了解 Redis。

要了解 Redis 命令的执行过程,首先要安装 Redis 服务,搭建 debug 环境。如果我们能一行行的看到命令在代码中的执行过程,解读源码也就没任何阻碍了。

后续所有文章均基于 redis3.2.13 版本。

1 搭建 debug 环境

1、下载编译文件
在 linux 上,下载源码文件,编译,使用 gdb(cgdb) 进行 debug。

# bash
wget https://github.com/antirez/redis/archive/3.2.13.tar.gz
tar -zxvf 3.2.13.tar.gz
mv redis-3.2.13 /opt/
cd redis-3.2.13
make                 # 编译文件,得到可执行文件 redis-server、redis-cli 等

2、开启 debug

# bash
gdb src/redis-server # 在 redis 安装目录,进入 gdb 调试环境

按我们平时调试的习惯,找到一个函数设置断点,然后一步步运行调试。对于 Redis 也一样,我们找到 server.c 文件,服务器运行的 main 函数就在此文件中。我们对 main 函数设置断点:

# gdb
(gdb) b main
Breakpoint 1 at 0x42ed05: file server.c, line 3962.

页面会提示我们在 server.c 文件的 3962 行设置了断点,也就是我们指定的 main 函数的位置。

设置好断点,下一步就是启动服务:

// 启动服务
(gdb) r ./redis.conf
Starting program: /opt/redis-3.2.13/src/redis-server ./redis.conf
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Breakpoint 1, main (argc=2, argv=0x7fffffffe5a8) at server.c:3962
3962    int main(int argc, char **argv) {

通过页面输出信息,我们会发现程序已经运行到我们设置的断点了。但是我们看不到运行处的代码,这可不行,看不到源码的调试,没法接受使用以下命令”召唤“源码:

(gdb) layout src

出现下图所示的界面:

图 1 - gdb 的 src 和 cmd 并存

到了这一步,我们已经正式开始踏上 Redis 源码解读之路了。

2 初始化服务

继续往下走,使用 n 命令,执行下一步,然后不断回车、回车、回车,好像每一行都看不懂什么意思。不管了,继续走。咦,好像发现个能看懂的 initServerConfig()。没看错的话,这个应该是初始化服务器配置的,让我们进到这个函数里确认下:

(gdb) s

回车,走你。然后我们就看到了下面这个界面:

图 2 - 进入初始化服务器配置函数

提示我们进入了 server.c 1464 行的 initServerConfig 函数中。 n 命令,继续走。我们会发现在这个函数里对服务器的各种基础参数进行初始化。这里的参数详见 server.h/redisServer 结构体。

回到 main 函数后,我们继续前进,还会发现一个 initServer() 的函数。这个函数是进行驱动事件的注册,以及绑定回调函数等。

继续走,直到执行 aeMain(),如下图:

图 3 - Redis 服务已开启

程序执行到 4133 行时,Redis 服务已成功开启了。此时服务器处于休眠状态,并使用 aeMain() 进行事件轮询,等待监听事件的发生。

上述整个过程,我们只是跟着程序的运行,大概看了一遍执行流程。下面,我们来详细解读上面叙述的关键步骤:初始化基础配置初始化服务器数据结构

3 初始化详细解读

3.1 初始化基础配置

初始化服务器的第一步就是创建一个 `redisServer 类型的实例变量 server 作为服务器的状态,并为结构中的各个属性设置默认值。

void initServerConfig(void) {
    int j;
    // 设置服务器运行 ID
    getRandomHexChars(server.runid,CONFIG_RUN_ID_SIZE);

    // 为运行 ID 加上结尾字符
    server.runid[CONFIG_RUN_ID_SIZE] = '\0';

    // 设置服务器默认运行架构
    server.arch_bits = (sizeof(long) == 8) ? 64 : 32;

    // 设置服务器默认配置文件路径
    server.configfile = NULL;

    // 设置服务器默认运行频率
    server.hz = CONFIG_DEFAULT_HZ;

    // 设置服务器默认端口
    server.port = CONFIG_DEFAULT_SERVER_PORT;
    
    // ...
}

对于 initServerConfig 函数来说,它主要完成以下主要工作:

  • 设置服务器的运行 ID。
  • 设置服务器的默认运行频率。
  • 设置服务器的默认配置文件路径。
  • 设置服务器的运行架构。
  • 设置服务器的默认端口号

initServerConfig 函数设置的服务器状态属性基本上都是一些整数、浮点数或者字符串属性。除了命令吧之外,initServerConfig 函数没有创建服务器状态的其它数据结构。像数据库、慢查询日志、Lua 环境、共享对象等这些数据结构是在之后的步骤中创建的。

当初始化基础配置参数后,下一步就要开始载入配置选项

3.2 载入配置选项

在启动服务器时,用户可以通过给定配置参数或者知道配置文件来修改服务器的默认配置。就像我们可以在启动服务时指定端口:

# bash
./src/redis-server --port 7379

通过给定配置参数的方式,修改了服务器的运行端口号。

除了给定配置参数的方式,我们可以通过指定配置文件的形式启动服务:

# bash
./src/redis-server ./redis.conf

通过指定配置文件的形式启动服务时,我们实际上就是通过配置文件的形式修改了服务器的数据库配置。

服务器在用 initServerConfig 函数初始完 server 变量后,就会开始载入用户给定的配置参数和配置文件,并根据用户设定的配置,对 server 变量相关属性进行修改。

关于命令行指定配置、配置文件配置、默认配置,这三种配置中:

  • 如果有指定配置,服务器就是有用户指定的值来更新对应的属性。
  • 如果没有指定值,则沿用 initServerConfig 函数设置的默认值。

3.3 初始化服务器数据结构

在执行 initServerConfig 函数初始化配置时,程序只创建了命令表一个数据结构,而服务器除了命令表还包括其他数据结构,比如:

  • server.clients 链表。这个链表记录了所有与服务器相连的客户端的状态结构。链表的每个节点都包含了一个 RedisClient 结构实例。
  • server.db 数组。数组中包含了服务器所有的数据库。
  • server.pubsub_channels 字典。字典中保存频道订阅信息。
  • server.pubsub_patterna 链表。链表中保存模式订阅信息。
  • server.lua 属性。用来执行 Lua 脚本。
  • server.slowlog 属性。用来保存慢日志。

上述这些数据结构会在 initServer 函数为其分配内存,并在有需要时为这些数据结构设置或关联初始化值。

之所以在载入用户配置之后才初始化数据结构,就是因为服务器要先载入用户的配置选项,才能根据选项正确的对数据结构进行初始化。避免再根据用户配置修改数据结构相关属性。

所以,我们可以看出,服务器对状态的初始化分为两步进行:

  1. initServerConfig 函数是初始化一般属性。
  2. initServer 初始化数据结构。

除了初始化数据结构之外,initServer 还进行了一些非常重要的设置操作,包括:

  • 为服务器设置进程信号处理器。
  • 创建共享对象。这些对象包含 Redis 服务器常用到的一些只,比如包含 "OK" 回复的字符串对象,包含 "ERR" 回复的字符串对象,包含整数 1 到 10000 的字符串对象等等。服务器正是通过重用这些共享对象来避免反复创建相同的对象,节约内存。
  • 打开服务器的监听端口,并为监听套接字关联应答事件处理器,等待服务器正式运行时接受客户端的连接。
  • 为服务器创建时间事件,等待服务器正是运行时执行 serverCron 函数。
  • 如果开启了 AOF 持久化功能,打开现有的 AOF 文件。如果 AOF 文件不存在,就创建并打开新的 AOF 文件,为 AOF 写入做好准备。
  • 初始化服务器的后台 IO 模块,为 IO 操作做好准备。

initServer 函数执行完毕之后,服务器将用 ASCII 字符在日志中打印出我们常见到的 Redis 图标,以及 Redis 的版本号信息等。

图 4 - 服务器启动后打印的 Redis 图标和版本信息等

4 其它操作

4.1 还原数据库

在完成了对服务器状态 server 变量的初始化之后,服务器需要载入 RDB 文件或者 AOF 文件(数据持久化保存文件),并根据文件记录的内容来还原服务器的数据库状态。

还原过程中,服务器会判断是否启用了 AOF 持久化功能:

  • 如果启用了 AOF 持久化功能,服务器将使用 AOF 文件来还原数据库状态。
  • 如果没有启用 AOF,服务器使用 RDB 文件来还原数据库状态。

当服务器完成数据库状态还原工作之后,会在日志中打印出载入文件和还原数据库状态所耗费的时长。

8189:M 31 May 13:12:47.971 * DB loaded from disk: 0.000 seconds

4.2 执行事件循环

在初始化的最后一步,服务器将打印出以下日志:

8189:M 31 May 13:12:47.971 * The server is now ready to accept connections on port 8379

并开始执行服务器的事件循环。

至此,服务器的初始化工作全部完成。

5 gdb 基础使用

命令 解释 示例
gdb file 加载被调试的可执行程序文件 gdb src/redis-server
r Run 的缩写,运行被调试的程序。 r ./redis.conf
c Continue 的缩写。继续执行被调试程序,直至下一个断点或程序结束 c
b Breakpoint 缩写。设置断点。可以使用 行号、函数名称、执行地址等方式指定断点位置 b main
s/n s 相当于“单步跟踪并进入”,也就是说进入到执行的函数内部。n 相当于“单步跟踪”,不进入到执行函数内部 s/n
p 变量名称 Print 缩写。显示指定变量的值。 p server

总结

  1. 搭建环境三步走:下载、编译、gdb。
  2. 服务启动包括:初始化基础配置、数据结构、对外提供服务的准备工作、还原数据库、执行事件循环等。
  3. gdb 基础命令:r c b n p。

北国风光
1.7k 声望279 粉丝

做一个靠谱的人,凡事有交代,件件有着落,事事有回音!