编辑
2023-11-14
Redis源码阅读
00
请注意,本文编写于 542 天前,最后修改于 542 天前,其中某些信息可能已经过时。

整个Redis启动过程为以下几个步骤

1.基本初始化(随机种子和时区设置)

2.哨兵模式设置,RDB,AOF检测

3.配置参数解析

4.Server初始化

5.执行EventLoop开始接受请求

整个过程都是单线程完成。

直接看server.c里面的main函数,里面就是整个redis的入口,首先会检查是不是测试,这个宏只有需要对Redis进行二开或者Debug源码的时候用到。

c
int main(int argc, char **argv) { struct timeval tv; int j; char config_from_stdin = 0; #ifdef REDIS_TEST monotonicInit(); /* Required for dict tests, that are relying on monotime during dict rehashing. */ if (argc >= 3 && !strcasecmp(argv[1], "test")) { int flags = 0; for (j = 3; j < argc; j++) { char *arg = argv[j]; if (!strcasecmp(arg, "--accurate")) flags |= REDIS_TEST_ACCURATE; else if (!strcasecmp(arg, "--large-memory")) flags |= REDIS_TEST_LARGE_MEMORY; else if (!strcasecmp(arg, "--valgrind")) flags |= REDIS_TEST_VALGRIND; } if (!strcasecmp(argv[2], "all")) { int numtests = sizeof(redisTests)/sizeof(struct redisTest); for (j = 0; j < numtests; j++) { redisTests[j].failed = (redisTests[j].proc(argc,argv,flags) != 0); } /* Report tests result */ int failed_num = 0; for (j = 0; j < numtests; j++) { if (redisTests[j].failed) { failed_num++; printf("[failed] Test - %s\n", redisTests[j].name); } else { printf("[ok] Test - %s\n", redisTests[j].name); } } printf("%d tests, %d passed, %d failed\n", numtests, numtests-failed_num, failed_num); return failed_num == 0 ? 0 : 1; } else { redisTestProc *proc = getTestProcByName(argv[2]); if (!proc) return -1; /* test not found */ return proc(argc,argv,flags); } return 0; } #endif

以下是对代码逻辑的分析:

  1. 首先,monotonicInit()函数被调用,用于初始化计时器,以确保在字典重哈希期间的字典测试中使用准确的时间。

  2. 然后,检查命令行参数(argcargv)是否满足特定条件,以确定是否执行测试模式。

    • 如果argc大于等于3且参数argv[1]的值不区分大小写地等于"test",则进入测试模式的逻辑。
  3. 在测试模式下,根据命令行参数的不同选项设置标志(flags)的值。

    • 可能的选项为:
      • "--accurate":启用精确模式。
      • "--large-memory":启用大内存模式。
      • "--valgrind":在Valgrind工具下运行测试。
  4. 如果参数argv[2]的值不区分大小写地等于"all",则执行所有测试。

    • 遍历redisTests数组中的每个测试,并调用相应的测试函数(redisTests[j].proc)执行测试。
    • 每个测试的结果都会被记录在redisTests[j].failed字段中。
    • 完成所有测试后,报告每个测试的结果(通过或失败)。
  5. 最后,打印总体的测试结果,包括测试的总数、通过的测试数和失败的测试数。

    • 如果没有测试失败,则返回0;否则返回1。

接下来是正式启动过程

c
/* We need to initialize our libraries, and the server configuration. */ #ifdef INIT_SETPROCTITLE_REPLACEMENT spt_init(argc, argv); // 初始化进程标题的替代实现 #endif tzset(); // 设置时区,更新 'timezone' 全局变量 zmalloc_set_oom_handler(redisOutOfMemoryHandler); // 设置内存分配失败的处理函数 gettimeofday(&tv, NULL); // 获取当前时间 srand(time(NULL) ^ getpid() ^ tv.tv_usec); // 设置随机数种子 srandom(time(NULL) ^ getpid() ^ tv.tv_usec); // 设置随机数种子 init_genrand64(((long long)tv.tv_sec * 1000000 + tv.tv_usec) ^ getpid()); // 初始化 Mersenne Twister 随机数生成器 crc64_init(); // 初始化 CRC64 校验值计算器 // 保存当前的 umask 值,并设置新的 umask umask(server.umask = umask(0777)); uint8_t hashseed[16]; getRandomBytes(hashseed, sizeof(hashseed)); // 生成随机字节序列 dictSetHashFunctionSeed(hashseed); // 设置字典的哈希函数种子

主要做了以下事情、

1.设置时区

2.设置内存分配失败处理函数

3.生成随机种子(用于siphash算法)

4.初始化CRC64

c
char *exec_name = strrchr(argv[0], '/'); if (exec_name == NULL) exec_name = argv[0]; server.sentinel_mode = checkForSentinelMode(argc,argv, exec_name); initServerConfig(); ACLInit(); /* The ACL subsystem must be initialized ASAP because the basic networking code and client creation depends on it. */ moduleInitModulesSystem(); connTypeInitialize(); /* 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;

初始化配置还有很多其他子系统,检查是否开启哨兵模式

c
/* Returns 1 if there is --sentinel among the arguments or if * executable name contains "redis-sentinel". */ int checkForSentinelMode(int argc, char **argv, char *exec_name) { if (strstr(exec_name,"redis-sentinel") != NULL) return 1; for (int j = 1; j < argc; j++) if (!strcmp(argv[j],"--sentinel")) return 1; return 0; }

如果开启,初始化哨兵模式需要的配置

c
if (server.sentinel_mode) { initSentinelConfig(); initSentinel(); }

检查需不需要RDB和AOF持久化数据恢复

c
if (strstr(exec_name,"redis-check-rdb") != NULL) redis_check_rdb_main(argc,argv,NULL); else if (strstr(exec_name,"redis-check-aof") != NULL) redis_check_aof_main(argc,argv); //***************************************************************** int redis_check_rdb_main(int argc, char **argv, FILE *fp) { struct timeval tv; if (argc != 2 && fp == NULL) { fprintf(stderr, "Usage: %s <rdb-file-name>\n", argv[0]); exit(1); } else if (!strcmp(argv[1],"-v") || !strcmp(argv[1], "--version")) { sds version = checkRdbVersion(); printf("redis-check-rdb %s\n", version); sdsfree(version); exit(0); } gettimeofday(&tv, NULL); init_genrand64(((long long) tv.tv_sec * 1000000 + tv.tv_usec) ^ getpid()); /* In order to call the loading functions we need to create the shared * integer objects, however since this function may be called from * an already initialized Redis instance, check if we really need to. */ if (shared.integers[0] == NULL) createSharedObjects(); server.loading_process_events_interval_bytes = 0; server.sanitize_dump_payload = SANITIZE_DUMP_YES; rdbCheckMode = 1; rdbCheckInfo("Checking RDB file %s", argv[1]); rdbCheckSetupSignals(); int retval = redis_check_rdb(argv[1],fp); if (retval == 0) { rdbCheckInfo("\\o/ RDB looks OK! \\o/"); rdbShowGenericInfo(); } if (fp) return (retval == 0) ? C_OK : C_ERR; exit(retval); } //*************三种AOF文件********************************************** input_file_type type = getInputFileType(filepath); switch (type) { case AOF_MULTI_PART: checkMultiPartAof(dirpath, filepath, fix); break; case AOF_RESP: checkOldStyleAof(filepath, fix, 0); break; case AOF_RDB_PREAMBLE: checkOldStyleAof(filepath, fix, 1); break; }

接下来往下走

c
if (argc >= 2) { j = 1; /* First option to parse in argv[] */ sds options = sdsempty(); /* 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); } } if (strcmp(argv[1], "--check-system") == 0) { exit(syscheck() ? 0 : 1); } /* Parse command line options * Precedence wise, File, stdin, explicit options -- last config is the one that matters. * * First argument is the config file name? */ if (argv[1][0] != '-') { /* Replace the config file in server.exec_argv with its absolute path. */ server.configfile = getAbsolutePath(argv[1]); zfree(server.exec_argv[1]); server.exec_argv[1] = zstrdup(server.configfile); j = 2; // Skip this arg when parsing options } sds *argv_tmp; int argc_tmp; int handled_last_config_arg = 1; while(j < argc) { /* Either first or last argument - Should we read config from stdin? */ if (argv[j][0] == '-' && argv[j][1] == '\0' && (j == 1 || j == argc-1)) { config_from_stdin = 1; } /* 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 config file * and stdin input are parsed (if they exist). * Only consider that if the last config has at least one argument. */ else if (handled_last_config_arg && argv[j][0] == '-' && argv[j][1] == '-') { /* Option name */ if (sdslen(options)) options = sdscat(options,"\n"); /* argv[j]+2 for removing the preceding `--` */ options = sdscat(options,argv[j]+2); options = sdscat(options," "); argv_tmp = sdssplitargs(argv[j], &argc_tmp); if (argc_tmp == 1) { /* Means that we only have one option name, like --port or "--port " */ handled_last_config_arg = 0; if ((j != argc-1) && argv[j+1][0] == '-' && argv[j+1][1] == '-' && !strcasecmp(argv[j], "--save")) { /* Special case: handle some things like `--save --config value`. * In this case, if next argument starts with `--`, we will reset * handled_last_config_arg flag and append an empty "" config value * to the options, so it will become `--save "" --config value`. * We are doing it to be compatible with pre 7.0 behavior (which we * break it in #10660, 7.0.1), since there might be users who generate * a command line from an array and when it's empty that's what they produce. */ options = sdscat(options, "\"\""); handled_last_config_arg = 1; } else if ((j == argc-1) && !strcasecmp(argv[j], "--save")) { /* Special case: when empty save is the last argument. * In this case, we append an empty "" config value to the options, * so it will become `--save ""` and will follow the same reset thing. */ options = sdscat(options, "\"\""); } else if ((j != argc-1) && argv[j+1][0] == '-' && argv[j+1][1] == '-' && !strcasecmp(argv[j], "--sentinel")) { /* Special case: handle some things like `--sentinel --config value`. * It is a pseudo config option with no value. In this case, if next * argument starts with `--`, we will reset handled_last_config_arg flag. * We are doing it to be compatible with pre 7.0 behavior (which we * break it in #10660, 7.0.1). */ options = sdscat(options, ""); handled_last_config_arg = 1; } else if ((j == argc-1) && !strcasecmp(argv[j], "--sentinel")) { /* Special case: when --sentinel is the last argument. * It is a pseudo config option with no value. In this case, do nothing. * We are doing it to be compatible with pre 7.0 behavior (which we * break it in #10660, 7.0.1). */ options = sdscat(options, ""); } } else { /* Means that we are passing both config name and it's value in the same arg, * like "--port 6380", so we need to reset handled_last_config_arg flag. */ handled_last_config_arg = 1; } sdsfreesplitres(argv_tmp, argc_tmp); } else { /* Option argument */ options = sdscatrepr(options,argv[j],strlen(argv[j])); options = sdscat(options," "); handled_last_config_arg = 1; } j++; } loadServerConfig(server.configfile, config_from_stdin, options); if (server.sentinel_mode) loadSentinelConfigFromQueue(); sdsfree(options); }

这里就加载配置文件的逻辑,注意,Redis会给配置变量赋值三次

  • 默认值

  • 命令行

  • 配置文件

都是互相覆盖,配置文件优先级可以说最高。

c
initServer(); // 初始化服务器 if (background || server.pidfile) createPidFile(); // 如果是后台运行或者设置了pid文件路径,则创建pid文件 if (server.set_proc_title) redisSetProcTitle(NULL); // 如果设置了进程标题,则设置Redis进程的标题 redisAsciiArt(); // 打印Redis的ASCII艺术字 checkTcpBacklogSettings(); // 检查TCP连接的backlog设置 if (server.cluster_enabled) { clusterInit(); // 如果启用了集群模式,则进行集群初始化 } if (!server.sentinel_mode) { moduleInitModulesSystemLast(); // 如果不是哨兵模式,则在最后阶段初始化模块系统 moduleLoadFromQueue(); // 从模块加载队列中加载模块 }

到这里其实Redis的启动就进入尾声了,最后一个阶段就是启动EventLoop开始监听客户端链接。

c
aeMain(server.el); aeDeleteEventLoop(server.el); return 0; }

其中还有一段代码

c
server.supervised = redisIsSupervised(server.supervised_mode);//监控模式 int background = server.daemonize && !server.supervised; if (background) daemonize(); //*********************************************************************** void daemonize(void) { int fd; if (fork() != 0) exit(0); /* 父进程退出 */ setsid(); /* 创建一个新的会话 */ /* 所有输出都被重定向到 /dev/null。如果 Redis 被设置为守护进程, * 但是配置文件中的 'logfile' 参数被设置为 'stdout',那么将不会有任何日志输出。 */ if ((fd = open("/dev/null", O_RDWR, 0)) != -1) { dup2(fd, STDIN_FILENO); dup2(fd, STDOUT_FILENO); dup2(fd, STDERR_FILENO); if (fd > STDERR_FILENO) close(fd); } }

会决定是否要以守护进程方式运行。

本文作者:yowayimono

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!