Redis源码-BFS方式浏览main函数(转载)

tech2022-10-01  69

文章目录 前言 看代码的方式 Redis 服务器的 main 函数 main 函数分段解释 函数名及参数 启动测试程序 程序环境初始化 初始化配置信息 存储参数信息 根据参数确定启动方式 处理并加载命令行参数 打印启动和警告信息 守护模式和初始化 哨兵模式判断启动并加载持久化数据 打印内存警告并启动事件监听 彩蛋 总结 redis-server

前言 欠下的技术债慢慢还,继续为去年吹过的牛而努力。去年年末的时候意识到自己掌握的知识还不够深入,决定开始看一些开源项目的源码,因为当时 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,因为只是学习源码,没有特殊情况就不更新版本了,保证环境的统一,我先把代码贴一份在这,后面再来慢慢看。

int main(int argc, char **argv) { struct timeval tv; int j;

#ifdef REDIS_TEST if (argc == 3 && !strcasecmp(argv[1], “test”)) { if (!strcasecmp(argv[2], “ziplist”)) { return ziplistTest(argc, argv); } else if (!strcasecmp(argv[2], “quicklist”)) { quicklistTest(argc, argv); } else if (!strcasecmp(argv[2], “intset”)) { return intsetTest(argc, argv); } else if (!strcasecmp(argv[2], “zipmap”)) { return zipmapTest(argc, argv); } else if (!strcasecmp(argv[2], “sha1test”)) { return sha1Test(argc, argv); } else if (!strcasecmp(argv[2], “util”)) { return utilTest(argc, argv); } else if (!strcasecmp(argv[2], “endianconv”)) { return endianconvTest(argc, argv); } else if (!strcasecmp(argv[2], “crc64”)) { return crc64Test(argc, argv); } else if (!strcasecmp(argv[2], “zmalloc”)) { return zmalloc_test(argc, argv); }

return -1; /* test not found */ }

#endif

/* We need to initialize our libraries, and the server configuration. */

#ifdef INIT_SETPROCTITLE_REPLACEMENT spt_init(argc, argv); #endif setlocale(LC_COLLATE,""); tzset(); /* Populates ‘timezone’ global. */ zmalloc_set_oom_handler(redisOutOfMemoryHandler); srand(time(NULL)^getpid()); gettimeofday(&tv,NULL); crc64_init();

uint8_t hashseed[16]; getRandomBytes(hashseed,sizeof(hashseed)); dictSetHashFunctionSeed(hashseed); server.sentinel_mode = checkForSentinelMode(argc,argv); initServerConfig(); ACLInit(); /* The ACL subsystem must be initialized ASAP because the basic networking code and client creation depends on it. */ moduleInitModulesSystem(); tlsInit(); /* Store the executable path and arguments in a safe place in order * to be able to restart the server later. */ server.executable = getAbsolutePath(argv[0]); server.exec_argv = zmalloc(sizeof(char*)*(argc+1)); server.exec_argv[argc] = NULL; for (j = 0; j < argc; j++) server.exec_argv[j] = zstrdup(argv[j]); /* We need to init sentinel right now as parsing the configuration file * in sentinel mode will have the effect of populating the sentinel * data structures with master nodes to monitor. */ if (server.sentinel_mode) { initSentinelConfig(); initSentinel(); } /* Check if we need to start in redis-check-rdb/aof mode. We just execute * the program main. However the program is part of the Redis executable * so that we can easily execute an RDB check on loading errors. */ if (strstr(argv[0],"redis-check-rdb") != NULL) redis_check_rdb_main(argc,argv,NULL); else if (strstr(argv[0],"redis-check-aof") != NULL) redis_check_aof_main(argc,argv); if (argc >= 2) { j = 1; /* First option to parse in argv[] */ sds options = sdsempty(); char *configfile = NULL; /* Handle special options --help and --version */ if (strcmp(argv[1], "-v") == 0 || strcmp(argv[1], "--version") == 0) version(); if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-h") == 0) usage(); if (strcmp(argv[1], "--test-memory") == 0) { if (argc == 3) { memtest(atoi(argv[2]),50); exit(0); } else { fprintf(stderr,"Please specify the amount of memory to test in megabytes.\n"); fprintf(stderr,"Example: ./redis-server --test-memory 4096\n\n"); exit(1); } } /* First argument is the config file name? */ if (argv[j][0] != '-' || argv[j][1] != '-') { configfile = argv[j]; server.configfile = getAbsolutePath(configfile); /* Replace the config file in server.exec_argv with * its absolute path. */ zfree(server.exec_argv[j]); server.exec_argv[j] = zstrdup(server.configfile); j++; } /* All the other options are parsed and conceptually appended to the * configuration file. For instance --port 6380 will generate the * string "port 6380\n" to be parsed after the actual file name * is parsed, if any. */ while(j != argc) { if (argv[j][0] == '-' && argv[j][1] == '-') { /* Option name */ if (!strcmp(argv[j], "--check-rdb")) { /* Argument has no options, need to skip for parsing. */ j++; continue; } if (sdslen(options)) options = sdscat(options,"\n"); options = sdscat(options,argv[j]+2); options = sdscat(options," "); } else { /* Option argument */ options = sdscatrepr(options,argv[j],strlen(argv[j])); options = sdscat(options," "); } j++; } if (server.sentinel_mode && configfile && *configfile == '-') { serverLog(LL_WARNING, "Sentinel config from STDIN not allowed."); serverLog(LL_WARNING, "Sentinel needs config file on disk to save state. Exiting..."); exit(1); } resetServerSaveParams(); loadServerConfig(configfile,options); sdsfree(options); } serverLog(LL_WARNING, "oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo"); serverLog(LL_WARNING, "Redis version=%s, bits=%d, commit=%s, modified=%d, pid=%d, just started", REDIS_VERSION, (sizeof(long) == 8) ? 64 : 32, redisGitSHA1(), strtol(redisGitDirty(),NULL,10) > 0, (int)getpid()); if (argc == 1) { serverLog(LL_WARNING, "Warning: no config file specified, using the default config. In order to specify a config file use %s /path/to/%s.conf", argv[0], server.sentinel_mode ? "sentinel" : "redis"); } else { serverLog(LL_WARNING, "Configuration loaded"); } server.supervised = redisIsSupervised(server.supervised_mode); int background = server.daemonize && !server.supervised; if (background) daemonize(); initServer(); if (background || server.pidfile) createPidFile(); redisSetProcTitle(argv[0]); redisAsciiArt(); checkTcpBacklogSettings(); if (!server.sentinel_mode) { /* Things not needed when running in Sentinel mode. */ serverLog(LL_WARNING,"Server initialized"); #ifdef __linux__ linuxMemoryWarnings(); #endif moduleLoadFromQueue(); ACLLoadUsersAtStartup(); InitServerLast(); loadDataFromDisk(); if (server.cluster_enabled) { if (verifyClusterConfigWithData() == C_ERR) { serverLog(LL_WARNING, "You can't have keys in a DB different than DB 0 when in " "Cluster mode. Exiting."); exit(1); } } if (server.ipfd_count > 0 || server.tlsfd_count > 0) serverLog(LL_NOTICE,"Ready to accept connections"); if (server.sofd > 0) serverLog(LL_NOTICE,"The server is now ready to accept connections at %s", server.unixsocket); if (server.supervised_mode == SUPERVISED_SYSTEMD) { if (!server.masterhost) { redisCommunicateSystemd("STATUS=Ready to accept connections\n"); redisCommunicateSystemd("READY=1\n"); } else { redisCommunicateSystemd("STATUS=Waiting for MASTER <-> REPLICA sync\n"); } } } else { InitServerLast(); sentinelIsRunning(); if (server.supervised_mode == SUPERVISED_SYSTEMD) { redisCommunicateSystemd("STATUS=Ready to accept connections\n"); redisCommunicateSystemd("READY=1\n"); } } /* Warning the user about suspicious maxmemory setting. */ if (server.maxmemory > 0 && server.maxmemory < 1024*1024) { serverLog(LL_WARNING,"WARNING: You specified a maxmemory value that is less than 1MB (current value is %llu bytes). Are you sure this is what you really want?", server.maxmemory); } redisSetCpuAffinity(server.server_cpulist); aeMain(server.el); aeDeleteEventLoop(server.el); return 0;

}

main 函数分段解释 函数名及参数 int main(int argc, char **argv) { struct timeval tv; int j;

//... //... return 0

}

这就是一个标准的 main 函数,参数 argc 和 argv 对于一个命令行程序来说可以是重头戏,肯定会拿来做重度解析的,函数开头还定义了 tv 和 j 两个变量,不知道干嘛的,接着往下看吧。

启动测试程序 #ifdef REDIS_TEST if (argc == 3 && !strcasecmp(argv[1], “test”)) { if (!strcasecmp(argv[2], “ziplist”)) { return ziplistTest(argc, argv); } else if (!strcasecmp(argv[2], “quicklist”)) { quicklistTest(argc, argv); } else if (!strcasecmp(argv[2], “intset”)) { return intsetTest(argc, argv); } else if (!strcasecmp(argv[2], “zipmap”)) { return zipmapTest(argc, argv); } else if (!strcasecmp(argv[2], “sha1test”)) { return sha1Test(argc, argv); } else if (!strcasecmp(argv[2], “util”)) { return utilTest(argc, argv); } else if (!strcasecmp(argv[2], “endianconv”)) { return endianconvTest(argc, argv); } else if (!strcasecmp(argv[2], “crc64”)) { return crc64Test(argc, argv); } else if (!strcasecmp(argv[2], “zmalloc”)) { return zmalloc_test(argc, argv); }

return -1; /* test not found */ }

#endif

当宏定义 REDIS_TEST 存在,并且参数合适的情况下启动测试程序,argv[0] 肯定是指 redis 服务器喽,那 argv[1] 的值如果是 test,而 argv[2] 的值是 ziplist,那么会调用 ziplist 的测试函数 ziplistTest,如果 argv[2] 的值是 zmalloc,那么会调用测试函数 zmalloc_test,为啥这里函数名命名规范不统一呢?挠头。

程序环境初始化 /* We need to initialize our libraries, and the server configuration. / #ifdef INIT_SETPROCTITLE_REPLACEMENT spt_init(argc, argv); #endif setlocale(LC_COLLATE,""); tzset(); / Populates ‘timezone’ global. */ zmalloc_set_oom_handler(redisOutOfMemoryHandler); srand(time(NULL)^getpid()); gettimeofday(&tv,NULL); crc64_init();

当 INIT_SETPROCTITLE_REPLACEMENT 这个宏存在的时候,调用 spt_init 函数来为设置程序标题做准备 setlocale() 用来设置地点信息,这一句应该是设置成依赖操作系统的地点信息,比如中国,韩国等等 tzset() 设置时区,这里可能影响到程序运行后,调整时区是否对程序产生影响 srand(time(NULL)^getpid()); 初始化随机种子 gettimeofday(&tv,NULL); 这里用到了函数开头定义的一个变量 tv,用来获取当前时间 crc64_init(); 循环冗余校验初始化,crc 神奇的存在 初始化配置信息 uint8_t hashseed[16]; getRandomBytes(hashseed,sizeof(hashseed)); dictSetHashFunctionSeed(hashseed); server.sentinel_mode = checkForSentinelMode(argc,argv); initServerConfig(); ACLInit(); /* The ACL subsystem must be initialized ASAP because the basic networking code and client creation depends on it. */ moduleInitModulesSystem(); tlsInit();

定一个16字节的空间用来存放哈希种子 随机获取一段16字节数据作为种子 将刚刚获取的种子数据设置到hash函数中 分析命令行参数,判断是否是哨兵模式 初始化服务器配置 ACL 初始化,不用管它具体是什么,进入下一层时自然会看到 初始化模块系统 tls 初始化,存疑,好奇的话进去看看也可以,好吧,原来是 ssl 那一套,够喝一壶的 存储参数信息 /* Store the executable path and arguments in a safe place in order * to be able to restart the server later. / server.executable = getAbsolutePath(argv[0]); server.exec_argv = zmalloc(sizeof(char)*(argc+1)); server.exec_argv[argc] = NULL; for (j = 0; j < argc; j++) server.exec_argv[j] = zstrdup(argv[j]);

这一小节比较简单,注释写的也很清楚,就是将命令行参数存储起来,方便重启 redis 服务

根据参数确定启动方式 /* We need to init sentinel right now as parsing the configuration file * in sentinel mode will have the effect of populating the sentinel * data structures with master nodes to monitor. */ if (server.sentinel_mode) { initSentinelConfig(); initSentinel(); }

/* Check if we need to start in redis-check-rdb/aof mode. We just execute * the program main. However the program is part of the Redis executable * so that we can easily execute an RDB check on loading errors. */ if (strstr(argv[0],"redis-check-rdb") != NULL) redis_check_rdb_main(argc,argv,NULL); else if (strstr(argv[0],"redis-check-aof") != NULL) redis_check_aof_main(argc,argv);

当启用哨兵模式的时候初始化额外的配置,啥是哨兵,现在还不用知道啊,从字面上来看就好了,反正知道命令行里如果指定了哨兵模式就要额外初始化一点东西。

下面这两个参数有点意思,简单扩展下,rdb 和 aof 是 redis 的两种数据落地的持久化方式,这里有意思的地方是判断了 argv[0] 这个参数,一般 argv[0] 是程序的名字,这个是固定不变的,而 redis 这里将程序名字作为参数来判断,也就是说你把可执行程序换个名字运行,它的行为就会发生变化。

处理并加载命令行参数 if (argc >= 2) { j = 1; /* First option to parse in argv[] */ sds options = sdsempty(); char *configfile = NULL;

/* Handle special options --help and --version */ if (strcmp(argv[1], "-v") == 0 || strcmp(argv[1], "--version") == 0) version(); if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-h") == 0) usage(); if (strcmp(argv[1], "--test-memory") == 0) { if (argc == 3) { memtest(atoi(argv[2]),50); exit(0); } else { fprintf(stderr,"Please specify the amount of memory to test in megabytes.\n"); fprintf(stderr,"Example: ./redis-server --test-memory 4096\n\n"); exit(1); } } /* First argument is the config file name? */ if (argv[j][0] != '-' || argv[j][1] != '-') { configfile = argv[j]; server.configfile = getAbsolutePath(configfile); /* Replace the config file in server.exec_argv with * its absolute path. */ zfree(server.exec_argv[j]); server.exec_argv[j] = zstrdup(server.configfile); j++; } /* All the other options are parsed and conceptually appended to the * configuration file. For instance --port 6380 will generate the * string "port 6380\n" to be parsed after the actual file name * is parsed, if any. */ while(j != argc) { if (argv[j][0] == '-' && argv[j][1] == '-') { /* Option name */ if (!strcmp(argv[j], "--check-rdb")) { /* Argument has no options, need to skip for parsing. */ j++; continue; } if (sdslen(options)) options = sdscat(options,"\n"); options = sdscat(options,argv[j]+2); options = sdscat(options," "); } else { /* Option argument */ options = sdscatrepr(options,argv[j],strlen(argv[j])); options = sdscat(options," "); } j++; } if (server.sentinel_mode && configfile && *configfile == '-') { serverLog(LL_WARNING, "Sentinel config from STDIN not allowed."); serverLog(LL_WARNING, "Sentinel needs config file on disk to save state. Exiting..."); exit(1); } resetServerSaveParams(); loadServerConfig(configfile,options); sdsfree(options); }

这段内容很长,但是核心的内容不多,前一部分是判断特殊参数,用来显示程序使用方法,启动内存测试等等,中间部分是分析命令行参数保存到字符串中,最后几行是读取服务器配置文件,并使用字符串中的参数选项覆盖文件中的部分配置。

打印启动和警告信息 serverLog(LL_WARNING, “oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo”); serverLog(LL_WARNING, “Redis version=%s, bits=%d, commit=%s, modified=%d, pid=%d, just started”, REDIS_VERSION, (sizeof(long) == 8) ? 64 : 32, redisGitSHA1(), strtol(redisGitDirty(),NULL,10) > 0, (int)getpid());

if (argc == 1) { serverLog(LL_WARNING, "Warning: no config file specified, using the default config. In order to specify a config file use %s /path/to/%s.conf", argv[0], server.sentinel_mode ? "sentinel" : "redis"); } else { serverLog(LL_WARNING, "Configuration loaded"); }

打印 redis 服务器启动信息,比如版本号,pid,警告信息等等,没有实际修改数据。

守护模式和初始化 server.supervised = redisIsSupervised(server.supervised_mode); int background = server.daemonize && !server.supervised; if (background) daemonize();

initServer(); if (background || server.pidfile) createPidFile(); redisSetProcTitle(argv[0]); redisAsciiArt(); checkTcpBacklogSettings();

根据守护进程配置和是否受监督来决定是否作为守护进程,什么是受监督,到现在还不知道,但是本着不懂装懂的方式看代码,可以认为我们懂了,后面自然还会有解释的地方。

接着就调用了 initServer(); 函数,这个初始化函数内容是比较长的,之前版本中很多 mian 函数中的内容都移到了这里面,初始化完成后创建 Pid 文件,设置进程名字,显示 redis 的Logo,检查一些配置,这个 backlog 参数之前面试的时候还被问到过,好奇的话可以提前了解一下。

哨兵模式判断启动并加载持久化数据 if (!server.sentinel_mode) { /* Things not needed when running in Sentinel mode. */ serverLog(LL_WARNING,“Server initialized”); #ifdef linux linuxMemoryWarnings(); #endif moduleLoadFromQueue(); ACLLoadUsersAtStartup(); InitServerLast(); loadDataFromDisk(); if (server.cluster_enabled) { if (verifyClusterConfigWithData() == C_ERR) { serverLog(LL_WARNING, "You can’t have keys in a DB different than DB 0 when in " “Cluster mode. Exiting.”); exit(1); } } if (server.ipfd_count > 0 || server.tlsfd_count > 0) serverLog(LL_NOTICE,“Ready to accept connections”); if (server.sofd > 0) serverLog(LL_NOTICE,“The server is now ready to accept connections at %s”, server.unixsocket); if (server.supervised_mode == SUPERVISED_SYSTEMD) { if (!server.masterhost) { redisCommunicateSystemd(“STATUS=Ready to accept connections\n”); redisCommunicateSystemd(“READY=1\n”); } else { redisCommunicateSystemd(“STATUS=Waiting for MASTER <-> REPLICA sync\n”); } } } else { InitServerLast(); sentinelIsRunning(); if (server.supervised_mode == SUPERVISED_SYSTEMD) { redisCommunicateSystemd(“STATUS=Ready to accept connections\n”); redisCommunicateSystemd(“READY=1\n”); } }

这段代码看起来像是再做一些通知提醒,其中比较重要的几个函数是moduleLoadFromQueue()、 InitServerLast() 和 loadDataFromDisk() ,第一个函数是加载模块的,第二个函数是在模块加载完成之后才能初始化的部分内容,最后一个是从磁盘加载数据到内存,这也是 redis 支持持久化的必要保证。

打印内存警告并启动事件监听 /* Warning the user about suspicious maxmemory setting. / if (server.maxmemory > 0 && server.maxmemory < 10241024) { serverLog(LL_WARNING,“WARNING: You specified a maxmemory value that is less than 1MB (current value is %llu bytes). Are you sure this is what you really want?”, server.maxmemory); }

redisSetCpuAffinity(server.server_cpulist); aeMain(server.el); aeDeleteEventLoop(server.el); return 0;

看到这段代码我们就来到了 main 函数结尾的部分,redisSetCpuAffinity() 是要做些和 CPU 相关的设置或配置,aeMain() 是主逻辑,对于提供服务的程序来说里面大概率是一个死循环,再满足指定的条件下才会打断退出,而 aeDeleteEventLoop() 就是循环结束时清理事件的操作,到此为止 main 函数就执行完啦。

彩蛋 这个 main 函数的代码中有一个神奇的用法不知道大家有没有发现,就是下面这句话:

serverLog(LL_WARNING, "You can't have keys in a DB different than DB 0 when in " "Cluster mode. Exiting.");

是不是看起来有些奇怪,不用管这个函数的定义是怎样的,可以告诉大家这个函数的定义类似于 printf 函数,只不过在最前面加了一个整型参数,那么调用这个函数时传了几个参数呢?3个?2个?,这个地方很神奇的会把两个字符串拼接到一起,类似于下面的写法:

serverLog(LL_WARNING, “You can’t have keys in a DB different than DB 0 when in Cluster mode. Exiting.”);

这样的字符串不仅可以分成两行,实际上可以分成任意行,最后都会拼接在一起,是不是很神奇。

总结 j 这个变量在 redis 的源码中经常出现,应该是作者的行为习惯吧,有些人爱用 i,而这个作者 antirez 爱用 j。 不能一口吃个胖子,看代码也是一样,不能期望一次性把所有的内容都看懂,一段时间后自己的代码都看不懂了,跟别说别人写的了。 redis 代码中频繁使用 server 这个变量,从 main 函数分析中也能看到,这个是个全局变量,代表了整个 redis 服务器程序数据。 不懂装懂或者说不求甚解是熟悉代码整体结构的一项优秀品质,这时候只要看个大概就可以了,真到熟悉细节的时候才是需要钻研的时候。 代码风格完全统一还是比较难实现的,从一个 main 函数中也可以看到,大部分函数是驼峰命名法,还要少量的下划线命名和帕斯卡命名。

原文链接:https://blog.csdn.net/albertsh/article/details/108030540?utm_medium=distribute.pc_feed.none-task-blog-personrec_tag-8.nonecase&depth_1-utm_source=distribute.pc_feed.none-task-blog-personrec_tag-8.nonecase&request_id=5f4396310388ae0b5643c61f

最新回复(0)