跟着大彬读源码 - Redis 1 - 启动服务,程序都干了什么?

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

相信不少小伙伴应该也都对或曾对源码感兴趣,但一来以为本身不会 C 语言,二来也不知从何入手,结果就和博主同样,一拖再拖。git

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

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

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

很好,这个问题成功吸引到我了。就让咱们从源码中找出这个问题的答案吧。后续的全部文章咱们都尝试经过提出问题,解答问题的步骤,来深刻了解 Redis。数据库

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

后续全部文章均基于 redis3.2.13 版本。浏览器

1 搭建 debug 环境

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

# 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 等

二、开启 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。
相关文章
相关标签/搜索