一直很羡慕那些能读 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

出现下图所示的界面:

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

2 初始化服务

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

(gdb) s

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

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

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

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

程序执行到 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 其它操作

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。

跟着大彬读源码 - Redis 1 - 启动服务,程序都干了什么?的更多相关文章

  1. 跟着大彬读源码 - Redis 2 - 服务器如何响应客户端请求?(上)

    上次我们通过问题"启动服务器,程序都干了什么?",跟着源码,深入了解了 Redis 服务器的启动过程. 既然启动了 Redis 服务器,那我们就要连上 Redis 服务干些事情.这 ...

  2. 跟着大彬读源码 - Redis 3 - 服务器如何响应客户端请求?(下)

    继续我们上一节的讨论.服务器启动了,客户端也发送命令了.接下来,就要到服务器"表演"的时刻了. 1 服务器处理 服务器读取到命令请求后,会进行一系列的处理. 1.1 读取命令请求 ...

  3. 跟着大彬读源码 - Redis 4 - 服务器的事件驱动有什么含义?(上)

    众所周知,Redis 服务器是一个事件驱动程序.那么事件驱动对于 Redis 而言有什么含义?源码中又是如何实现事件驱动的呢?今天,我们一起来认识下 Redis 服务器的事件驱动. 对于 Redis ...

  4. 跟着大彬读源码 - Redis 6 - 对象和数据类型(下)

    继续撸我们的对象和数据类型. 上节我们一起认识了字符串和列表,接下来还有哈希.集合和有序集合. 1 哈希对象 哈希对象的可选编码分别是:ziplist 和 hashtable. 1.1 ziplist ...

  5. 跟着大彬读源码 - Redis 7 - 对象编码之简单动态字符串

    Redis 没有直接使用 C 语言传统的字符串表示(以空字符串结尾的字符数组),而是构建了一种名为简单动态字符串(simple dynamic string)的抽象类型,并将 SDS 用作 Redis ...

  6. 跟着大彬读源码 - Redis 8 - 对象编码之字典

    目录 1 字典的实现 2 插入算法 3 rehash 与 渐进式 rehash 总结 字典,是一种用于保存键值对的抽象数据结构.由于 C 语言没有内置字典这种数据结构,因此 Redis 构建了自己的字 ...

  7. 跟着大彬读源码 - Redis 9 - 对象编码之 三种list

    目录 1 ziplist 2 skiplist 3 quicklist 总结 Redis 底层使用了 ziplist.skiplist 和 quicklist 三种 list 结构来实现相关对象.顾名 ...

  8. 跟着大彬读源码 - Redis 10 - 对象编码之整数集合

    [TOC] 整数集合是 Redis 集合键的底层实现之一.当一个集合只包含整数值元素,并且元素数量不多时,Redis 就会使用整数集合作为集合键的底层实现. 1 整数集合的实现 整数集合是 Redis ...

  9. 跟着大彬读源码 - Redis 5 - 对象和数据类型(上)

    相信很多人应该都知道 Redis 有五种数据类型:字符串.列表.哈希.集合和有序集合.但这五种数据类型是什么含义?Redis 的数据又是怎样存储的?今天我们一起来认识下 Redis 这五种数据结构的含 ...

随机推荐

  1. 线程池;java的线程池的实现原理;适用于频繁互动(如电商网站)

    线程池是一种多线程处理形式,处理过程中将任务加入到队列,然后在创建线程后自己主动启动这些任务.线程池线程都是后台线程.每一个线程都使用默认的堆栈大小,以默认的优先级执行.并处于多线程单元中. 假设某个 ...

  2. 运行该脚本出现/bin/sh^M: bad interpreter: No such file or directory

    错误中脚本文件的一个非常可能的原因是DOS格的, 即每一行的行尾以\r\n来标识, 其ASCII码各自是0x0D, 0x0A.  能够有非常多种办法看这个文件是DOS格式的还是UNIX格式的, 还是M ...

  3. Raw-OS备用事件源代码分析

    作为分析的内核版本2014-04-15,基于1.05正式版,blogs我们会跟上的内核开发进度的最新版本,如果出现源代码的目光"???"的话.没有深究的部分是理解. Raw-OS官 ...

  4. IdentityServer学习目录

    IdentityServer IdentityServer的基本概念与特性 IdentityServer流程图与相关术语 最简单的IdentityServer实现 最简单的IdentityServer ...

  5. C# 桌面软件开发-深入学习[2]- AY-C#人爱学不学-aaronyang技术分享

    原文:C# 桌面软件开发-深入学习[2]- AY-C#人爱学不学-aaronyang技术分享 1 : C# Assembly.GetEntryAssembly().GetName().Version. ...

  6. WPF属性(二)附加属性

    原文:WPF属性(二)附加属性 附加属性是说一个属性本来不属于某个对象,但由于某种需求而被后来附加上,也就是把对象放入一个特定环境后对象才具有的属性就称为附加属性,附加属性的作用就是将属性与数据类型解 ...

  7. Activity跳转通过EventBus传值问题

    根据阿里发布的Android开发规范:下载地址:https://102.alibaba.com/downloadFile.do?file=1520478361732/Android_v9.pdf Ac ...

  8. Android零基础入门第12节:熟悉Android Studio界面,开始装逼卖萌

    原文:Android零基础入门第12节:熟悉Android Studio界面,开始装逼卖萌 通过前两期的学习,我们可以正确搭建好Android Studio的开发环境,也创建了HelloWorld工程 ...

  9. wsl相关总结

    启用WSL VirtualMachinePlatform是WSL2依赖功能,需要系统支持(build 18917+),硬件支持VM功能并开启,安装完成后要重启计算机. Enable-WindowsOp ...

  10. Visual studio调试Web发生未能正常启动IIS express

    今天调试web时,不知道怎么搞的,昨天还好好的,结果今天怎么也没法调试了.VS里报的错误是进程号为**的未能正常启动,看了下调试时IIS压根就没启动起来,没关系,看看事件管理器里发生了什么 找到个最关 ...