前言
欠下的技术债慢慢还,继续为去年吹过的牛而努力。去年年末的时候意识到自己掌握的知识还不够深入,决定开始看一些开源项目的源码,因为当时 Redis
的兴起,所以瞄准了准备从它下手,之后确实看了一部分内容,比如跳表、网络事件库等等,后来过年就鸽了。今年开始一直熟悉新的业务,比较懒没跟进,最近间歇性踌躇满志又发作了,准备抽时间再捋顺一遍,老规矩,还是从 main()
函数下手。
对于 C/C++
程序一定是从 main()
函数开头的,这是我们切入的一个点,至于怎么找到 main
函数,每个人有不同的方法,最暴力的方法当然就是全文搜索了,不过较为成熟的项目一般搜索出来都不止一个 main
函数,因为整个项目完整构建下来不止一个程序。
像 redis
这个项目最起码有服务器和客户端两个程序,源码中至少包含了两个 main
函数,再加上一些测试程序,main
函数在源码中会有很多。再比如 Lua
的源代码中包含和解释器和编译器,如果直接搜索至少会找到两个 main
函数。
redis
服务器程序的 main
函数在文件 src/server.c
中,之前好像是在 redis.c
文件中后来改名了,这都不重要,反正你需要从搜索出来的 main
函数中找到一个开始的地方,这个花不了多少时间。
看代码的方式
标题中提到了 BFS
方式看代码,而 BFS
指的是广度优先搜索,与之相对应的是 DFS
深度优先搜索,对于不含异步调用的单线程程序来说,执行代码是以深度优先搜索的方式,遇到一个函数就调用进去,在函数中又遇到另一个函数再调用进去,当函数执行完成返回到上一层。
为什么选择 BFS
方式看代码呢?因为这样可以在短时间内更全面的了解代码结构,我们先看第一层,当第一层浏览完成之后再进入到第二层,比如我们先看 main
函数,即使 main
函数调用了很多不认识的函数也不要去管,从名字大概判断一些作用就可以了,不用纠结具体的实现内容,当 main
函数全部看完了再进入到第二层去了解它调用的那些函数。
总之使用 BFS
方式看代码就要有一种“不懂装懂”的态度,不然容易陷入细节,无法整体把握。
Redis 服务器的 main 函数
redis
服务器的 main
函数代码量不是很大,总共 200 行左右,我选择了 6.0.6
这个版本 7bf665f125a4771db095c83a7ad6ed46692cd314
,因为只是学习源码,没有特殊情况就不更新版本了,保证环境的统一,我先把代码贴一份在这,后面再来慢慢看。
1 | int main(int argc, char **argv) { |
main 函数分段解释
函数名及参数
1 | int main(int argc, char **argv) { |
这就是一个标准的 main
函数,参数 argc
和 argv
对于一个命令行程序来说可以是重头戏,肯定会拿来做重度解析的,函数开头还定义了 tv
和 j
两个变量,不知道干嘛的,接着往下看吧。
启动测试程序
1 |
|
当宏定义 REDIS_TEST
存在,并且参数合适的情况下启动测试程序,argv[0]
肯定是指 redis
服务器喽,那 argv[1]
的值如果是 test
,而 argv[2]
的值是 ziplist
,那么会调用 ziplist
的测试函数 ziplistTest
,如果 argv[2]
的值是 zmalloc
,那么会调用测试函数 zmalloc_test
,为啥这里函数名命名规范不统一呢?挠头。
程序环境初始化
1 | /* We need to initialize our libraries, and the server configuration. */ |
- 当
INIT_SETPROCTITLE_REPLACEMENT
这个宏存在的时候,调用spt_init
函数来为设置程序标题做准备 setlocale()
用来设置地点信息,这一句应该是设置成依赖操作系统的地点信息,比如中国,韩国等等tzset()
设置时区,这里可能影响到程序运行后,调整时区是否对程序产生影响srand(time(NULL)^getpid());
初始化随机种子gettimeofday(&tv,NULL);
这里用到了函数开头定义的一个变量tv
,用来获取当前时间crc64_init();
循环冗余校验初始化,crc
神奇的存在
初始化配置信息
1 | uint8_t hashseed[16]; |
- 定一个16字节的空间用来存放哈希种子
- 随机获取一段16字节数据作为种子
- 将刚刚获取的种子数据设置到hash函数中
- 分析命令行参数,判断是否是哨兵模式
- 初始化服务器配置
ACL
初始化,不用管它具体是什么,进入下一层时自然会看到- 初始化模块系统
tls
初始化,存疑,好奇的话进去看看也可以,好吧,原来是ssl
那一套,够喝一壶的
存储参数信息
1 | /* Store the executable path and arguments in a safe place in order |
这一小节比较简单,注释写的也很清楚,就是将命令行参数存储起来,方便重启 redis
服务
根据参数确定启动方式
1 | /* We need to init sentinel right now as parsing the configuration file |
当启用哨兵模式的时候初始化额外的配置,啥是哨兵,现在还不用知道啊,从字面上来看就好了,反正知道命令行里如果指定了哨兵模式就要额外初始化一点东西。
下面这两个参数有点意思,简单扩展下,rdb
和 aof
是 redis
的两种数据落地的持久化方式,这里有意思的地方是判断了 argv[0]
这个参数,一般 argv[0]
是程序的名字,这个是固定不变的,而 redis
这里将程序名字作为参数来判断,也就是说你把可执行程序换个名字运行,它的行为就会发生变化。
处理并加载命令行参数
1 | if (argc >= 2) { |
这段内容很长,但是核心的内容不多,前一部分是判断特殊参数,用来显示程序使用方法,启动内存测试等等,中间部分是分析命令行参数保存到字符串中,最后几行是读取服务器配置文件,并使用字符串中的参数选项覆盖文件中的部分配置。
打印启动和警告信息
1 | serverLog(LL_WARNING, "oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo"); |
打印 redis
服务器启动信息,比如版本号,pid,警告信息等等,没有实际修改数据。
守护模式和初始化
1 | server.supervised = redisIsSupervised(server.supervised_mode); |
根据守护进程配置和是否受监督来决定是否作为守护进程,什么是受监督,到现在还不知道,但是本着不懂装懂的方式看代码,可以认为我们懂了,后面自然还会有解释的地方。
接着就调用了 initServer();
函数,这个初始化函数内容是比较长的,之前版本中很多 mian
函数中的内容都移到了这里面,初始化完成后创建 Pid
文件,设置进程名字,显示 redis
的Logo,检查一些配置,这个 backlog
参数之前面试的时候还被问到过,好奇的话可以提前了解一下。
哨兵模式判断启动并加载持久化数据
1 | if (!server.sentinel_mode) { |
这段代码看起来像是再做一些通知提醒,其中比较重要的几个函数是moduleLoadFromQueue()
、 InitServerLast()
和 loadDataFromDisk()
,第一个函数是加载模块的,第二个函数是在模块加载完成之后才能初始化的部分内容,最后一个是从磁盘加载数据到内存,这也是 redis
支持持久化的必要保证。
打印内存警告并启动事件监听
1 | /* Warning the user about suspicious maxmemory setting. */ |
看到这段代码我们就来到了 main
函数结尾的部分,redisSetCpuAffinity()
是要做些和 CPU
相关的设置或配置,aeMain()
是主逻辑,对于提供服务的程序来说里面大概率是一个死循环,再满足指定的条件下才会打断退出,而 aeDeleteEventLoop()
就是循环结束时清理事件的操作,到此为止 main
函数就执行完啦。
彩蛋
这个 main
函数的代码中有一个神奇的用法不知道大家有没有发现,就是下面这句话:
1 | serverLog(LL_WARNING, |
是不是看起来有些奇怪,不用管这个函数的定义是怎样的,可以告诉大家这个函数的定义类似于 printf
函数,只不过在最前面加了一个整型参数,那么调用这个函数时传了几个参数呢?3个?2个?,这个地方很神奇的会把两个字符串拼接到一起,类似于下面的写法:
1 | serverLog(LL_WARNING, |
这样的字符串不仅可以分成两行,实际上可以分成任意行,最后都会拼接在一起,是不是很神奇。
总结
j
这个变量在redis
的源码中经常出现,应该是作者的行为习惯吧,有些人爱用i
,而这个作者antirez
爱用j
。- 不能一口吃个胖子,看代码也是一样,不能期望一次性把所有的内容都看懂,一段时间后自己的代码都看不懂了,跟别说别人写的了。
redis
代码中频繁使用server
这个变量,从main
函数分析中也能看到,这个是个全局变量,代表了整个redis
服务器程序数据。- 不懂装懂或者说不求甚解是熟悉代码整体结构的一项优秀品质,这时候只要看个大概就可以了,真到熟悉细节的时候才是需要钻研的时候。
- 代码风格完全统一还是比较难实现的,从一个
main
函数中也可以看到,大部分函数是驼峰命名法,还要少量的下划线命名和帕斯卡命名。
你微笑的模样,提醒着我不要躲藏,坚持原来的方向,哪怕最后遍体鳞伤,困难只会让坚持的人越来越强,共勉~
2020-8-15 23:48:53