关于redis:三-从servermain再次看redis启动流程

2次阅读

共计 36996 个字符,预计需要花费 93 分钟才能阅读完成。

三 从 server.main 再次看 redis 启动流程

明天呢,咱们从整体的角度来看看 redis 的启动过程,后面两章内容曾经将 redis 启动时重要的过程具体的讲了一遍,当初呢,咱们从整体的过程来看看 redis 的启动。

main 函数是程序启动的入口,当初呢,咱们一步一步的去剖析他,开掘他。

1 变量定义

main 函数的前三行

 struct timeval tv;
    int j;
    char config_from_stdin = 0;

这段代码定义了一个名为 tvtimeval 构造体变量和一个 int 类型的变量 j,以及一个 char 类型的变量 config_from_stdin,初值为 0。

timeval 是 C 语言规范库中的一个构造体,示意工夫值,由两个成员组成:tv_sec 示意秒数,tv_usec 示意微秒数。在这段代码中,咱们定义了一个 timeval 类型的变量 tv,通常用于计算时间差和设置超时等场景。

int 类型的变量 j 能够用于计数,或者存储整数值。

char 类型的变量 config_from_stdin 用于标记程序是否须要从规范输出读取配置。该变量的初值为 0,示意程序不须要从规范输出读取配置。当该变量的值为 1 时,示意程序须要从规范输出读取配置。

NAME
       timeval - time in seconds and microseconds

LIBRARY
       Standard C library (libc)

SYNOPSIS
       #include <sys/time.h>

       struct timeval {
           time_t       tv_sec;   /* Seconds */
           suseconds_t  tv_usec;  /* Microseconds */
       };

DESCRIPTION
       Describes times in seconds and microseconds.

看到这里,你是不是想起后面读取配置的三种形式了,其中是否从终端中读取就在此处定义了。

2 预编译指令 - 条件测试

#ifdef REDIS_TEST
    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;
        }

        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

这段代码是一段预编译指令,次要用于判断以后是否处于测试模式下。

#ifdef 指令是一个条件编译指令,用于判断某个宏是否曾经被定义过。在这里,REDIS_TEST 宏是否被定义过将决定 #ifdef REDIS_TEST#endif 之间的代码段是否须要被编译。

如果 REDIS_TEST 宏曾经被定义过,则该代码段将被编译。否则,该代码段将被疏忽。

条件编译指令罕用于编译时针对不同状况进行不同解决,例如调试模式和公布模式下的代码解决不同,或者针对不同平台进行解决等。在 Redis 中,测试模式下的代码与正式公布版本的代码可能存在一些差别,因而须要应用条件编译指令进行辨别。

条件测试这个模块咱们放到前面进行解说。

3 预编译指令 - 过程题目

#ifdef INIT_SETPROCTITLE_REPLACEMENT
    spt_init(argc, argv);
#endif

在这里,#ifdef INIT_SETPROCTITLE_REPLACEMENT#endif 之间的代码段将会被编译,如果在编译 Redis 时定义了 INIT_SETPROCTITLE_REPLACEMENT 宏。该宏在 Redis 的 Makefile 中定义,用于告知编译器应用一个自定义的过程题目设置函数 spt_init()

过程题目是指过程在操作系统中显示的名称,通常用于标识过程的用处。在 Redis 中,过程题目默认为 Redis 的运行参数(如端口号、工作目录等)的组合,然而在某些状况下(如在服务端部署时),咱们可能心愿过程题目可能更直观地反映 Redis 的用处,这时能够应用自定义的过程题目设置函数来实现。

INIT_SETPROCTITLE_REPLACEMENT 宏的定义与具体实现无关,能够参考 Redis 的源代码和 Makefile 文件来理解。在这里,咱们假如该宏定义了一个值为 1 的整数常量,示意须要启用过程题目的自定义设置。如果该宏被定义,那么就会调用 spt_init() 函数来初始化过程题目,该函数的参数为 argcargv,表示命令行参数的数量和内容。

4 设置程序本地信息

    setlocale(LC_COLLATE,"");

这行代码应用了 C 规范库中的 setlocale 函数,用于设置程序的本地化(locale)信息。

locale 是一个 C 语言规范库中的概念,用于形容一组与特定地区和语言相干的参数和规定,例如工夫格局、货币符号、字符编码等等。程序的本地化信息通常由操作系统决定,然而程序也能够通过调用 setlocale 函数来被动设置本地化信息,以便更好地适应不同的用户环境。

在这里,setlocale 函数的第一个参数是 LC_COLLATE,示意须要设置的本地化分类信息。LC_COLLATE 是一个用于排序和比拟字符串的本地化分类,用于定义字符排序的规定。当设置 LC_COLLATE 为一个空字符串时,示意应用默认的本地化分类信息,这通常是指操作系统的默认设置。

该函数的第二个参数为一个空字符串,示意应用默认的本地化信息。

在 Redis 中,该代码的作用是设置程序的本地化信息,以便更好地适应不同的语言和地区。这在 Redis 中尤其重要,因为 Redis 是一个全球性的软件,须要反对不同的语言和字符编码。

该函数承受两个参数,别离是本地化分类信息 category 和须要设置的本地化信息 localecategory 示意须要设置的本地化分类信息,它能够是以下常量之一:

#include <locale.h>

char *setlocale(int category, const char *locale);
  • LC_ALL:示意设置所有本地化分类信息。
  • LC_COLLATE:示意设置字符排序和比拟的本地化分类信息。
  • LC_CTYPE:示意设置字符分类和转换的本地化分类信息。
  • LC_MONETARY:示意设置货币格局的本地化分类信息。
  • LC_NUMERIC:示意设置数字格局的本地化分类信息。
  • LC_TIME:示意设置工夫格局的本地化分类信息。

locale 示意须要设置的本地化信息,它的格局通常是一个字符串,用于指定语言、地区、字符编码等信息。例如,"en_US.UTF-8" 示意英语语言、美国地区、UTF-8 编码。

setlocale 函数返回一个字符串指针,指向以后的本地化信息字符串。如果设置本地化信息胜利,返回值将与第二个参数 locale 雷同,否则返回一个空指针。

在程序中调用 setlocale 函数能够设置本地化信息,以便更好地适应不同的语言和地区。例如,如果程序须要解决多语言的用户界面,就能够依据用户的语言偏好来设置本地化信息,以便正确地显示日期、工夫、货币符号等内容。

5 初始化时区信息

tzset(); /* Populates 'timezone' global. */.

tzset() 函数是 C 规范库中的一个函数,用于初始化时区信息。

在 Unix/Linux 零碎中,工夫和时区是密切相关的。当零碎初始化时,须要读取环境变量 TZ,以确定以后所在时区。tzset() 函数的作用就是依据 TZ 环境变量的值来设置时区信息,并将相应的信息保留到全局变量 timezonedaylight 中。

timezone 是一个整型变量,示意以后时区与 UTC(协调世界时)的时差,以秒为单位。如果以后时区比 UTC 早,那么 timezone 将是一个正值;如果以后时区比 UTC 晚,那么 timezone 将是一个负值。

daylight 是一个布尔型变量,示意以后时区是否应用夏令时。如果以后时区应用夏令时,那么 daylight 的值为非零(通常为 1);否则 daylight 的值为零。

在调用 tzset() 函数之后,程序就能够通过拜访全局变量 timezonedaylight 来获取以后的时区信息了。须要留神的是,tzset() 函数只须要在程序启动时调用一次即可,通常不须要反复调用。

6 内存调配失败回调函数

zmalloc_set_oom_handler(redisOutOfMemoryHandler);
void redisOutOfMemoryHandler(size_t allocation_size) {
    serverLog(LL_WARNING,"Out Of Memory allocating %zu bytes!",
        allocation_size);
    serverPanic("Redis aborting for OUT OF MEMORY. Allocating %zu bytes!",
        allocation_size);
}

zmalloc_set_oom_handler 函数是 Redis 中一个用于解决内存调配失败的回调函数,它用于设置内存调配失败时的处理函数。

在 Redis 中,内存调配失败时会调用 zmalloc 函数,如果调配失败,就会触发 Redis 的 out of memory (OOM) 事件。默认状况下,Redis 的 OOM 事件处理形式是间接调用 exit(1) 函数,退出 Redis 过程。然而,如果你心愿在产生内存调配失败时执行一些特定的操作,例如记录日志或者开释一些资源,就能够应用 zmalloc_set_oom_handler 函数来设置自定义的处理函数。

例如,zmalloc_set_oom_handler(redisOutOfMemoryHandler) 的作用是将 redisOutOfMemoryHandler 函数注册为 Redis 内存调配失败时的处理函数。如果 Redis 在内存调配时遇到问题,将会主动调用该处理函数,并执行其中的逻辑。这样,你就能够在 Redis 产生内存调配失败时进行特定的操作了。

static void (*zmalloc_oom_handler)(size_t) = zmalloc_default_oom;
static void zmalloc_default_oom(size_t size) {
    fprintf(stderr, "zmalloc: Out of memory trying to allocate %zu bytes\n",
        size);
    fflush(stderr);
    abort();}
void zmalloc_set_oom_handler(void (*oom_handler)(size_t)) {zmalloc_oom_handler = oom_handler;}

Redis 实现在内存调配失败时可能执行谬误处理函数的关键在于两个方面:

  1. 应用了 zmalloc 函数库作为 Redis 的内存分配器:Redis 应用 zmalloc 作为内存分配器而不是 C 语言规范库中的 malloc 函数。zmalloc 函数库是一个基于 malloc 的封装,它在 malloc 的根底上增加了一些个性,例如内存调配跟踪、内存对齐等。同时,zmalloc 还提供了 zmalloc_set_oom_handler 函数,它可能让开发者注册一个函数,当 Redis 在内存调配失败时会调用该函数。
  2. zmalloc 函数库中,重写了 mallocfree 函数:zmalloc 函数库重写了 C 语言规范库中的 mallocfree 函数,以实现本人的内存分配器。在 zmalloc 中,重写的 malloc 函数在内存调配失败时不是返回 NULL,而是调用了谬误处理函数。而且在 zmalloc 中还提供了一个 zmalloc_oom 函数,它会触发 Redis 的 OOM 事件,这个事件将会使 Redis 执行谬误处理函数

因而,当 Redis 在分配内存时发现内存不足,就会调用谬误处理函数。这个谬误处理函数能够是开发者本人编写的、记录谬误日志、尝试开释内存或者其余操作,以确保 Redis 不会因为内存不足而解体。

zmalloc 函数库中,重写的 malloc 函数被称为 zmalloc 函数,它是 Redis 中用于分配内存的函数。

zmalloc 函数与规范的 malloc 函数十分类似,但有一些区别。次要区别如下:

  1. zmalloc 函数会对调配的内存进行额定的跟踪和解决,例如记录调配的内存大小和地址,内存对齐等。
  2. zmalloc 函数会在分配内存时查看是否有足够的内存可用,如果没有足够的内存,它会调用 Redis 的谬误处理函数,而不是返回 NULL
  3. zmalloc 函数会保护一个内存池,用于进步调配和开释内存的效率。
  4. zmalloc 函数还提供了其余一些性能,例如重置内存池、获取内存池的统计信息等。

因而,通过应用 zmalloc 函数库,Redis 可能更好地管制内存的调配和开释,并且在内存调配失败时可能执行谬误处理函数。

void *zmalloc(size_t size) {void *ptr = ztrymalloc_usable(size, NULL);
    if (!ptr) zmalloc_oom_handler(size);
    return ptr;
}

这段代码是 Redis 中的 zmalloc 函数的实现。它的次要作用是调配一块指定大小的内存,并返回一个指向该内存块的指针。

具体来说,zmalloc 函数首先调用 ztrymalloc_usable 函数尝试调配指定大小的内存。如果调配胜利,则间接返回指向该内存块的指针。如果调配失败,则调用 zmalloc_oom_handler 函数来解决内存不足的状况。

zmalloc_oom_handler 函数是 Redis 中的内存不足处理函数。它的次要作用是执行一些错误处理操作,例如打印谬误音讯、开释一些已调配的内存等。这样能够确保 Redis 不会在内存不足的状况下解体,而是可能优雅地解决该问题。

总之,zmalloc 函数是 Redis 中用于分配内存的外围函数之一,它确保了 Redis 在分配内存时的安全性和可靠性。

7 初始化随机数发生器

      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());
    crc64_init();

这段代码次要用于初始化随机数发生器。具体来说,它执行以下操作:

  1. 调用 gettimeofday 函数获取以后工夫,并存储到 tv 构造体中。
  2. 应用以后过程的过程 ID、工夫戳、以及微秒数作为种子,通过 srandsrandom 函数初始化随机数发生器。
  3. 调用 init_genrand64 函数,应用以后工夫的秒数和微秒数、以及过程 ID 作为种子,初始化一个 64 位随机数发生器。
  4. 调用 crc64_init 函数初始化 CRC64 校验表。

这些操作的目标是为了在 Redis 运行期间产生各种随机数,以及在须要进行校验和计算时应用 CRC64 校验表。这样能够减少 Redis 的安全性和可靠性,确保 Redis 在各种状况下都能失常运行。

8 设置文件权限

 umask(server.umask = umask(0777));

umask 函数用于设置以后过程的文件创建屏蔽字。在 Redis 中,该语句的作用是将以后过程的文件创建屏蔽字设置为 0777,并将返回值赋值给 server.umask 变量。

umask 函数的作用是屏蔽掉过程的某些权限,比方读、写、执行等权限,使其不能被文件创建零碎调用所继承。例如,如果文件创建屏蔽字设置为 022,则示意屏蔽掉组和其余用户的写权限,使得创立的文件默认权限为 -rw-r--r--

在 Redis 中,通过将文件创建屏蔽字设置为 0777,能够确保 Redis 在创立文件时不会受到任何限度,这对于一些须要频繁创立和操作文件的场景十分重要,比方 AOF 长久化和 RDB 长久化等。

9 哈希种子

      uint8_t hashseed[16];
    getRandomBytes(hashseed,sizeof(hashseed));
    dictSetHashFunctionSeed(hashseed)

在这段代码中,Redis 应用 getRandomBytes 函数生成一个 16 字节的随机字节数组 hashseed,而后将其作为种子调用 dictSetHashFunctionSeed 函数,设置哈希函数的种子。哈希函数的种子是一个常量,它影响到哈希函数的散列后果,用于缩小哈希抵触,进步哈希表的效率。

在 Redis 中,哈希表被宽泛用于实现各种数据结构,如字典、汇合、有序汇合等,哈希函数的性能对于 Redis 的性能有很大的影响。为了避免哈希碰撞,Redis 在启动时生成一个随机的种子,并将其用作哈希函数的种子。这样,即便在同一组键值对中,如果键的散列值雷同,那么哈希函数也会以不同的形式解决这些键值对,缩小哈希碰撞的可能性,进步 Redis 的性能。

10 检测哨兵模式

     char *exec_name = strrchr(argv[0], '/');
    if (exec_name == NULL) exec_name = argv[0];
    server.sentinel_mode = checkForSentinelMode(argc,argv, exec_name);
  
  1. 通过 strrchr() 函数获取程序名 argv[0] 中最初一个 / 字符之后的字符串(即程序名),并将其保留到 exec_name 变量中。
  2. 如果程序名中没有 / 字符,阐明 argv[0] 自身就是程序名,将其间接保留到 exec_name 变量中。
  3. 调用 checkForSentinelMode() 函数查看是否以 Sentinel 模式运行,并将查看后果保留到 server.sentinel_mode 变量中。
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;
}

具体实现是,首先依据参数 argv[0] 中的可执行文件名(不包含门路)判断是否以 redis-sentinel 命名,如果是,返回 1;否则,遍历命令行参数argv,如果发现 ”–sentinel” 参数,返回 1,否则返回 0。

11 初始化服务器配置

initServerConfig();

相熟吧,这就是咱们前两章讲得配置文件, 在这里我就不详细描述了,简略的概述一下

initServerConfig()是 Redis 中用来初始化服务器配置构造体的函数。在该函数中,会将服务器配置构造体的各个成员变量设置为默认值,而后依据启动参数以及配置文件的设置批改这些默认值。

具体来说,该函数会先将所有的配置项都设置为默认值,而后读取 redis.conf 配置文件中的配置项,如果启动参数中存在与配置文件中雷同的配置项,则以启动参数中的值为准。最初,依据 sentinel_mode 变量的值来决定是否进行哨兵模式下的配置。

12 ACL 权限管制模块

ACLInit()

ACLInit()函数是 Redis 中用来初始化 ACL 权限管制模块的函数。在该函数中,会初始化一些数据结构,以反对后续的 ACL 操作。

具体来说,该函数会初始化一个全局的 ACL 权限列表,以及多个用于 ACL 操作的数据结构,如命令表(command table)、用户表(user table)、角色表(role table)等。在初始化过程中,该函数会从配置文件中读取默认的 ACL 规定,并将其退出到全局的 ACL 权限列表中。同时,该函数还会将 defaultUser 用户和 defaultPassword 明码设置为默认的登录凭据,以便在没有指定具体凭据的状况下,应用这些凭据进行登录。

这一块具体的代码还是比较复杂的,在后续中,我会具体的为大家进行解说。

13 Redis 初始化模块零碎

moduleInitModulesSystem();

moduleInitModulesSystem() 是 Redis 5.0 引入的新函数,它的作用是初始化 Redis 的模块零碎。

Redis 模块零碎容许开发者通过动静加载模块,扩大 Redis 的性能。模块零碎容许开发者通过 Redis 提供的 API 进行模块开发,同时模块也能够在 Redis 运行时加载、卸载,而无需进行 Redis 服务。

moduleInitModulesSystem() 函数在 Redis 启动时被调用,用于初始化模块零碎。这个函数会在 Redis 的服务器状态中创立一个模块列表,而后遍历加载所有曾经在 Redis 配置文件中配置的模块。

如果开发者心愿编写本人的 Redis 模块,能够通过调用 Redis 模块 API 进行编写,并将编写好的模块编译成动静库文件。在 Redis 启动时,将这些动静库文件加载到 Redis 的模块零碎中,就能够在 Redis 中应用本人编写的模块。

14 TLS 层初始化

  tlsInit();

tlsInit()函数是 Redis 服务器 TLS(传输层安全性)反对的一部分。它在服务器初始化期间调用,用于设置 TLS 上下文。

在以下是 TLS 初始化的步骤概述:

  1. 调用 OpenSSL 库的初始化函数 SSL_library_init()OpenSSL_add_all_algorithms(),以初始化和加载 OpenSSL 反对库。
  2. 调用 SSL_CTX_new() 函数创立一个新的 TLS 上下文对象,该对象用于 TLS 连贯的创立和配置。
  3. 调用 SSL_CTX_set_ecdh_auto() 函数以启用主动抉择椭圆曲线密钥替换算法,这是一种更平安的形式来生成加密密钥。
  4. 调用 SSL_CTX_set_session_cache_mode() 函数启用 TLS 会话缓存,以进步 TLS 连贯的性能。
  5. 调用 SSL_CTX_set_verify() 函数设置 TLS 连贯的验证模式,能够配置为验证服务器证书或客户端证书,或者不进行证书验证。
  6. 调用 SSL_CTX_use_certificate_chain_file()SSL_CTX_use_PrivateKey_file()函数加载服务器证书和私钥,以便应用 TLS 进行加密通信。
  7. 调用 SSL_CTX_set_cipher_list() 函数设置 TLS 连贯反对的加密算法列表,以确保 TLS 连贯的安全性。
  8. 最初,为了提供更好的性能和安全性,Redis 应用 setsockopt() 函数启用 TCP Fast Open(TFO)个性,这将容许连贯更快地建设。

通过以上步骤,tlsInit()函数初始化了 Redis 服务器的 TLS 反对,并为加密通信创立了平安的 TLS 上下文。

15 保留服务器信息

 /* 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 服务器启动时的可执行文件门路和命令行参数,以便在重启服务器时应用。具体步骤如下:

首先通过调用 getAbsolutePath 函数获取以后可执行文件的绝对路径,而后将其保留在 server.executable 变量中。接着应用 zmalloc 函数动静分配内存,调配 argc+1 个指向 char 类型的指针,将其保留在 server.exec_argv 变量中。最初应用 zstrdup 函数为每个命令行参数动静分配内存,并将指针保留在 server.exec_argv 数组中,最初将 server.exec_argv 数组的最初一个元素设置为 NULL

这样做的目标是为了在 Redis 服务器异样退出后,能够通过从新执行可执行文件并传入雷同的命令行参数,来复原 Redis 服务器的状态。这个机制通常用于主动重启 Redis 服务器,以进步 Redis 服务器的可用性。

16 哨兵模式启动

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

这段代码在判断 Redis 是否是运行在 Sentinel 模式下。如果是,那么会先执行 initSentinelConfig() 来初始化 Sentinel 相干的配置信息,再调用 initSentinel() 来初始化 Sentinel 相干的数据结构和网络连接等。否则,不做任何解决,间接进入失常的 Redis 模式。这里所说的 Sentinel 是 Redis 高可用计划中的一个组件,用于实现主动故障转移等性能。

这个性能将会在后续的哨兵处具体解说

17 检测 RDB 或者 AOF 文件

  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);

这段代码次要是依据可执行文件名 exec_name 是否蕴含 redis-check-rdb 或者 redis-check-aof 字符串来决定执行对应的函数,即 redis_check_rdb_main() 或者 redis_check_aof_main() 函数。

redis-server 启动时,会传入参数 argv,其中第一个参数是可执行文件名,例如 /usr/local/bin/redis-server。因而,能够通过查找 argv[0] 中是否蕴含特定的字符串来判断以后程序的运行模式。对于 redis-check-rdbredis-check-aof 这两个程序来说,它们都是用来查看 Redis 数据文件的工具程序,因而须要独自的逻辑来解决。

如果 exec_name 蕴含 redis-check-rdb,则会调用 redis_check_rdb_main() 函数;如果蕴含 redis-check-aof,则会调用 redis_check_aof_main() 函数。这两个函数都是用来查看 Redis 数据文件的工具程序,用于查看 RDB 文件和 AOF 文件的正确性和完整性。

须要留神的是,这段代码是在 if (server.sentinel_mode) 的外层,因而它只有在 Redis Server 不是以 Sentinel 模式启动时才会执行。如果以 Sentinel 模式启动,那么不会执行这段代码,而是在 initSentinel() 函数中依据 Sentinel 模式启动的参数来决定是否执行数据文件查看的逻辑。

18 检测用户命令行参数

18.1 version

  if (strcmp(argv[1], "-v") == 0 ||
            strcmp(argv[1], "--version") == 0) version();

这段代码是用于检查用户是否传入了 -v 或 –version 参数,如果传入了这些参数,则调用 version() 函数打印 Redis 的版本信息。

void version(void) {
    printf("Redis server v=%s sha=%s:%d malloc=%s bits=%d build=%llx\n",
        REDIS_VERSION,
        redisGitSHA1(),
        atoi(redisGitDirty()) > 0,
        ZMALLOC_LIB,
        sizeof(long) == 4 ? 32 : 64,
        (unsigned long long) redisBuildId());
    exit(0);
}

这段代码实现了 redis-server 命令的版本号展现性能。当在命令行中执行 redis-server -vredis-server --version 时,会触发 version() 函数,该函数会输入以后 Redis 服务器的版本号、Git SHA1 值、是否为脏数据版本、Redis 服务器应用的内存分配器、运行在 32 位还是 64 位机器上、以及构建 ID 等信息,而后应用 exit() 函数退出程序。

18.2 help

if (strcmp(argv[1], "--help") == 0 ||
            strcmp(argv[1], "-h") == 0) usage();

这段代码是判断用户是否输出了 --help-h参数,并在用户输出时调用 usage() 函数来显示 Redis 的帮忙信息。如果用户输出了这两个参数中的任意一个,strcmp()函数会返回 0,进入 if 语句,执行 usage() 函数,否则继续执行 Redis 的主逻辑。

void usage(void) {fprintf(stderr,"Usage: ./redis-server [/path/to/redis.conf] [options] [-]\n");
    fprintf(stderr,"./redis-server - (read config from stdin)\n");
    fprintf(stderr,"./redis-server -v or --version\n");
    fprintf(stderr,"./redis-server -h or --help\n");
    fprintf(stderr,"./redis-server --test-memory <megabytes>\n");
    fprintf(stderr,"./redis-server --check-system\n");
    fprintf(stderr,"\n");
    fprintf(stderr,"Examples:\n");
    fprintf(stderr,"./redis-server (run the server with default conf)\n");
    fprintf(stderr,"echo'maxmemory 128mb'| ./redis-server -\n");
    fprintf(stderr,"./redis-server /etc/redis/6379.conf\n");
    fprintf(stderr,"./redis-server --port 7777\n");
    fprintf(stderr,"./redis-server --port 7777 --replicaof 127.0.0.1 8888\n");
    fprintf(stderr,"./redis-server /etc/myredis.conf --loglevel verbose -\n");
    fprintf(stderr,"./redis-server /etc/myredis.conf --loglevel verbose\n\n");
    fprintf(stderr,"Sentinel mode:\n");
    fprintf(stderr,"./redis-server /etc/sentinel.conf --sentinel\n");
    exit(1);
}

usage()函数是用来打印 Redis 服务器的应用帮忙信息的。在命令行参数解析过程中,如果用户指定了 --help-h选项,则会调用该函数来打印帮忙信息。

该函数会向规范谬误输入流(stderr)打印一些应用示例和选项阐明。用户能够通过命令行参数来设置 Redis 的配置选项,例如指定配置文件门路、指定日志级别等。最初,函数通过调用 exit() 函数退出程序,返回状态码 1。

18.3 test-memory

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);
            }
        } 

这段代码是解决 --test-memory 参数的,这个参数用于在启动 Redis 服务器之前测试零碎内存的可用性。如果用户在命令行中指定了 --test-memory 参数并且提供了一个数字,那么 Redis 将测试零碎中该数字大小的内存是否可用。如果内存可用,Redis 将打印一条音讯并退出程序。如果用户没有提供数字或提供了谬误的数字,则 Redis 将打印谬误音讯并退出程序。

18.4 零碎查看

  if (strcmp(argv[1], "--check-system") == 0) {exit(syscheck() ? 0 : 1);
        }

这段代码是在查看是否须要执行 Redis 零碎查看(system check)。如果命令行参数中蕴含了 --check-system 参数,则执行零碎查看,如果通过查看,则程序返回 0,否则返回 1,该返回值将被操作系统当作程序的返回值。如果没有蕴含 --check-system 参数,则不进行零碎查看,程序持续向下执行。

int syscheck(void) {
    check *cur_check = checks;
    int ret = 1;
    sds err_msg = NULL;
    while (cur_check->check_fn) {int res = cur_check->check_fn(&err_msg);
        printf("[%s]...", cur_check->name);
        if (res == 0) {printf("skipped\n");
        } else if (res == 1) {printf("OK\n");
        } else {printf("WARNING:\n");
            printf("%s\n", err_msg);
            sdsfree(err_msg);
            ret = 0;
        }
        cur_check++;
    }

    return ret;
}

这段代码实现了 Redis 服务器启动时的零碎查看性能,次要是查看服务器运行所需的一些系统资源和配置参数是否满足要求。

当执行 ./redis-server --check-system 命令时,程序会调用 syscheck 函数进行零碎查看。该函数首先会遍历全局数组 checks,该数组中存储了多个零碎查看函数的指针,每个函数用于查看一个系统资源或参数。而后一一调用每个查看函数,并依据查看后果输入相应的信息,最初返回整个零碎查看的后果。

其中,每个查看函数都须要返回一个整型值,示意查看的后果。如果返回值为 0,示意该查看函数被跳过;如果返回值为 1,示意该查看函数查看通过;如果返回值为其余非零值,示意该查看函数查看失败,此时须要将错误信息保留在 err_msg 指针指向的缓冲区中,并将返回值作为该函数的查看后果。

须要留神的是,在遍历 checks 数组并调用查看函数时,程序会先输入一个相似 [xxx]... 的提示信息,其中 xxx 示意以后正在进行的查看工作的名称,不便用户理解以后查看工作的停顿状况。如果某个查看工作被跳过,则程序不会输入任何信息;如果该工作查看通过,则程序输入 OK 字样;如果该工作查看失败,则程序输入 WARNING 字样,并将错误信息打印进去。最初,如果所有查看工作都查看通过,则 syscheck 函数返回 1;否则返回 0。

18.5 查看配置文件

 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
        }

这段代码次要是解决 Redis 服务器的命令行参数。如果第一个参数不是以“-”结尾,那么这个参数应该是配置文件的门路。此时,服务器会将配置文件的绝对路径存储在 server.configfile 中,并替换 server.exec_argv 数组中的配置文件门路参数为绝对路径。最初,j被设置为 2,这是为了在后续解析选项时跳过这个参数。

19 命令行配置

   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;}
            else if (handled_last_config_arg && argv[j][0] == '-' && argv[j][1] == '-') {
               /*
                           ************
               */
            } else {
                /* Option argument */
                options = sdscatrepr(options,argv[j],strlen(argv[j]));
                options = sdscat(options," ");
                handled_last_config_arg = 1;
            }
            j++;
        }

        

19.1 if

if (argv[j][0] == '-' && argv[j][1] == '\0' && (j == 1 || j == argc-1)) {config_from_stdin = 1;}

如果以后命令行参数的第一个字符为 ‘-‘,第二个字符为 ‘\0’,且该参数是第一个或最初一个参数,那么就示意这是一个 “-” 参数,示意从规范输出中读取配置文件。

这个判断是用来查看命令行参数是否是一个独自的 -,如果是的话,示意程序须要从规范输出中读取配置。例如,在 Linux 命令行中运行 Redis 的命令为 redis-server -,这将使 Redis 从规范输出中读取配置。在这种状况下,程序将通过查看命令行参数是否为单个 - 来判断是否须要从规范输出中读取配置。

19.2 else if

  else if (handled_last_config_arg && argv[j][0] == '-' && argv[j][1] == '-') 

如果曾经解决完最初一个配置参数,而且以后参数以双破折线(–)结尾,则进入以下分支。

 if (sdslen(options)) options = sdscat(options,"\n");

这行代码是将 options 变量的内容开端增加一个换行符,以确保下一行输入的内容和以后行的选项显示在不同行上,以进步输入的可读性。其中 options 变量是在解析命令行参数时用来记录指定的选项的一个字符串。

options = sdscat(options,argv[j]+2);
options = sdscat(options," ");

argv[j]是一个指向命令行参数字符串的指针,argv[j][2]示意这个字符串的第三个字符,因为 C 语言数组的下标从 0 开始。所以 argv[j]+2 示意从字符串的第三个字符开始的子字符串。这里的目标是为了跳过命令行参数的前缀--

这段代码的作用是将输出的参数解析成 redis 的配置选项,例如 --port 6379。在代码中,当检测到参数以-- 结尾时,将该参数作为选项的一部分,并将它退出一个字符串变量 options 中。

首先,该段代码通过 sdslen() 函数获取 options 字符串的长度,若该字符串不为空,则在其开端加上一个换行符\n,用于将不同的选项辨别开来。

而后,该段代码通过 sdslen() 函数获取以后参数的长度,将其作为选项的一部分退出 options 字符串,并在字符串开端加上一个空格,以便将该选项与前面的参数分隔开来。最终生成的 options 字符串就是 redis 的配置选项。

argv_tmp = sdssplitargs(argv[j], &argc_tmp);

sdssplitargs 函数是 Redis 中的一个字符串操作函数,其作用是将给定的字符串依照空格分隔符分成多个子字符串,并返回一个字符串数组,同时批改给定的参数 argc_tmp 来示意字符串数组的长度。

在这里,argv[j] 示意一个命令行参数,它须要依照空格分隔符进行宰割。宰割后失去的字符串数组 argv_tmp 将在接下来的代码中被解决。

 if (argc_tmp == 1) {/* Means that we only have one option name, like --port or "--port" */}
 handled_last_config_arg = 0;

这行代码的作用是将 handled_last_config_arg 变量的值设为 0,它被用来跟踪上一个参数是否是配置文件的路径名,以便下一个参数可能被正确地解析。当它被设置为 0 时,下一个参数将被视为一个新的配置项或选项,而不是上一个配置文件路径名的一部分。

  if ((j != argc-1) && argv[j+1][0] == '-' && argv[j+1][1] == '-' &&
                        !strcasecmp(argv[j], "--save"))
                    {options = sdscat(options, "\"\"");
                        handled_last_config_arg = 1;
                    }

这段代码是解决非凡状况的。如果以后解决的参数是 “–save”,并且前面一个参数以 “–” 结尾(即前面一个参数也是一个选项),那么就将 handled_last_config_arg 标记重置为 0,而后在选项字符串 options 中退出一个空字符串 “”””,以便将其与前面的选项拆散开来。这个非凡解决的起因是为了与晚期版本的 Redis 兼容,因为有些用户会从一个数组生成一个命令行,而当它为空时就会产生这种状况。

  else if ((j == argc-1) && !strcasecmp(argv[j], "--save")) {options = sdscat(options, "\"\"");
                    }

如果命令行最初一个参数是 ”–save”,且没有任何配置信息紧随其后,这里会将一个空字符串 "" 追加到 options 字符串的开端。这是为了使其与下面提到的非凡状况保持一致,从而能够解决空配置参数的状况。例如,”–save” 选项被应用而没有任何配置信息紧随其后。

 else if ((j != argc-1) && argv[j+1][0] == '-' && argv[j+1][1] == '-' &&
                        !strcasecmp(argv[j], "--sentinel"))
                    {options = sdscat(options, "");
                        handled_last_config_arg = 1;
                    }

这段代码是在解析 Redis 的命令行参数时,解决一些非凡的选项的状况。在这个 else if 语句中,它会查看是否遇到了 --sentinel 这个选项,并且如果下一个参数也是以 -- 结尾,就将 handled_last_config_arg 标记位设置为 1。

--sentinel 是一个伪配置选项,它没有值。如果下一个参数也是以 -- 结尾,就阐明它不是 --sentinel 选项的值,这时就须要将 handled_last_config_arg 标记位重置,以便前面的参数能够被正确解析。options = sdscat(options, ""); 的作用是向 options 字符串中增加一个空字符串,示意 --sentinel 选项的存在。

  else if ((j == argc-1) && !strcasecmp(argv[j], "--sentinel")) {options = sdscat(options, "");
                    }

这段代码是针对解析 Redis 服务器启动参数的逻辑。在这个逻辑中,Redis 应用 argv[]数组存储启动参数,并依据参数类型进行不同的解决。

具体来说,当解析到一个形如 ”- –config” 的参数时,Redis 须要将该参数作为一个配置文件门路解决。如果该参数前面紧跟着的是另一个参数,则阐明该参数并不是最初一个参数,Redis 须要将下一个参数作为一个一般参数解决,并将该参数设置为“已解决完最初一个配置文件参数”。如果下一个参数也是一个“–”类型的参数,则阐明该参数前面没有其余参数了,因而 Redis 须要将该参数设置为最初一个参数,并将该参数设置为“已解决完最初一个配置文件参数”。

当解析到一个形如 ”–save” 或 ”–sentinel” 的参数时,Redis 须要将该参数作为一个配置项名解决。如果该参数前面紧跟着的是另一个参数,则阐明该参数并不是最初一个参数,Redis 须要将下一个参数作为该配置项的值解决,并将该参数设置为“已解决完最初一个配置文件参数”。如果下一个参数也是一个“–”类型的参数,则阐明该参数前面没有其余参数了,因而 Redis 须要将该参数设置为最初一个参数,并将该参数设置为“已解决完最初一个配置文件参数”。如果该参数是最初一个参数,则 Redis 须要将该参数设置为该配置项的值。

 else {handled_last_config_arg = 1;}
                sdsfreesplitres(argv_tmp, argc_tmp);
            } 

如果以后参数以 “–” 结尾,且同时蕴含配置选项名和选项值(如 “–port 6380″),则须要将 handled_last_config_arg 标记位设置为 1,以表明以后参数是一个新的配置选项。

19.3 else

else {
                /* Option argument */
                options = sdscatrepr(options,argv[j],strlen(argv[j]));
                options = sdscat(options," ");
                handled_last_config_arg = 1;
            }

这段代码是在解决命令行参数时的一种状况,即以后参数既不是配置文件的参数,也不是选项参数,而是一个选项参数的值。在这种状况下,将以后参数作为选项参数的值增加到 options 字符串中,并将 handled_last_config_arg 标记设置为 1,示意已解决完上一个配置参数,能够持续解决下一个参数。

   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++;
        }

20 loadServerConfig

loadServerConfig(server.configfile, config_from_stdin, options);

这段函数大家也比拟相熟了,我在简略概述一下,具体请参考后面文章

loadServerConfig() 函数用于加载 Redis 服务器的配置。它承受三个参数:

  • configfile:Redis 配置文件的门路。
  • config_from_stdin:是否从规范输出中读取配置。这个值会在解析命令行参数时被设置。
  • options:Redis 配置选项。这个字符串蕴含了从命令行读取到的 Redis 配置选项和对应的值。

当 Redis 服务器启动时,它须要读取一个配置文件来确定一些根本配置参数。Redis 反对从命令行传递参数来笼罩这些根本配置参数,也反对从规范输出中读取配置,然而这些配置会笼罩命令行参数和配置文件参数。loadServerConfig() 函数的作用就是依据这些参数来加载 Redis 服务器的配置。

21 哨兵模式运行

if (server.sentinel_mode) loadSentinelConfigFromQueue();
sdsfree(options);

这段代码次要是查看 Redis 是否在 sentinel 模式下运行,如果是,则调用 loadSentinelConfigFromQueue() 函数从 sentinel.conf 中加载配置。接着,开释 options 字符串的内存空间,因为在配置加载后,这个字符串就不再须要了。

哨兵模式会在前面具体介绍

21 查看哨兵配置文件

 if (server.sentinel_mode) sentinelCheckConfigFile();

sentinelCheckConfigFile() 函数是用于查看 Sentinel 配置文件的函数。在 Redis Sentinel 模式下,Redis 实例是由 Sentinel 治理的,它须要一个 Sentinel 配置文件。该函数用于查看 Sentinel 配置文件是否正确配置。如果文件中没有配置 Redis Sentinel 的运行条件,则该函数会输入错误信息并导致 Redis Sentinel 启动失败。

具体来说,sentinelCheckConfigFile() 函数首先查看配置文件是否存在。如果不存在,则函数会输入谬误音讯并导致 Sentinel 无奈启动。如果存在,则函数会读取配置文件并查看其内容。配置文件应该至多蕴含以下内容:

  1. Sentinel 运行的端口号。
  2. 连贯到 Redis 服务器的地址和端口。
  3. 在启动 Sentinel 之前,须要 Sentinel 所监控的 Redis 实例数量。
  4. Sentinel 的其余配置参数。

如果配置文件中存在上述内容,则函数会返回 1。如果短少其中任何一个内容,则函数会输入相应的谬误音讯并返回 0,导致 Sentinel 启动失败。

总之,sentinelCheckConfigFile() 函数用于查看 Sentinel 配置文件的正确性,以确保 Sentinel 能够正确地监控 Redis 实例。

void sentinelCheckConfigFile(void) {if (server.configfile == NULL) {
        serverLog(LL_WARNING,
            "Sentinel needs config file on disk to save state. Exiting...");
        exit(1);
    } else if (access(server.configfile,W_OK) == -1) {
        serverLog(LL_WARNING,
            "Sentinel config file %s is not writable: %s. Exiting...",
            server.configfile,strerror(errno));
        exit(1);
    }
}

22 监控模式运行

  server.supervised = redisIsSupervised(server.supervised_mode);

这行代码会依据 server.supervised_mode 的值来设置 server.supervised 的值,即 Redis 是否以监控模式运行。监控模式是指在启动 Redis 时将其交给一个监控过程(如 systemd)来治理,由监控过程负责主动重启 Redis 服务,以保障 Redis 服务的高可用性。

具体来说,redisIsSupervised 函数会查看以后 Redis 过程是否被某个监控过程治理,查看形式因平台而异,Linux 上的实现是查看以后过程的父过程是否为 1(init 过程)。如果是,阐明 Redis 过程被启动在监控模式下,否则不是。

23 服务端后盾运行

int background = server.daemonize && !server.supervised;
    if (background) daemonize();

这段代码的作用是判断以后 Redis 是否须要在后盾运行,并在须要时执行 daemonize() 函数使其在后盾运行。

首先,通过 server.daemonize 变量来判断是否须要在后盾运行。如果该变量的值为 1,示意须要在后盾运行,否则为 0。

其次,如果 Redis 是以 supervised 模式运行的,即 server.supervised 变量的值为 1,示意 Redis 在被 supervisord 过程治理,因而不须要在此处进行后盾运行,因为 supervisord 会管制 Redis 过程的生命周期。如果 server.supervised 变量的值为 0,则示意 Redis 不是以 supervised 模式运行的,此时须要在后盾运行,因而执行 daemonize() 函数将 Redis 过程 daemonize 到后盾。

最终,如果 Redis 须要在后盾运行,则 background 变量的值为 1,否则为 0。

void daemonize(void) {
    int fd;

    if (fork() != 0) exit(0); /* parent exits */
    setsid(); /* create a new session */

    /* Every output goes to /dev/null. If Redis is daemonized but
     * the 'logfile' is set to 'stdout' in the configuration file
     * it will not log at all. */
    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);
    }
}

24 服务端输入启动信息

  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());

这段代码是 Redis 服务器启动时输入的一些启动信息,包含 Redis 版本、以后机器是 32 位还是 64 位、Redis 代码的 git commit ID、Redis 代码是否被批改过以及 Redis 服务器的过程 ID。这些信息能够帮忙管理员在呈现问题时更快地定位问题所在,也能够不便管理员确认 Redis 服务器启动后的状态。

 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/redis.conf", argv[0]);
    } else {serverLog(LL_WARNING, "Configuration loaded");
    }

这段代码是用来记录 Redis 的启动信息的。如果在命令行中没有指定配置文件门路,则记录正告音讯,示意应用默认配置。如果指定了配置文件,则记录“Configuration loaded”信息。

25 initServer

initServer();

这个函数也非常相熟了,我不在赘述了。

initServer()函数用于初始化服务器相干的构造体和参数,其次要执行以下操作:

  1. 初始化服务器状态构造体 server 的各个字段,如各个数据库的状态构造体、命令表、统计信息等;
  2. 设置服务器的各种默认参数值,如最大客户端数量、默认的最大内存限度等;
  3. 初始化随机数生成器,以便后续应用;
  4. 读取零碎的配置参数,包含最大关上文件数量限度等;
  5. 设置解体处理函数,以便程序产生谬误时可能及时处理;
  6. 初始化慢查问日志;
  7. 加载用户自定义的扩大命令;
  8. 初始化事件循环机制。

总的来说,initServer()函数为服务器的失常运行打下了根底,后续的代码将会在此基础上进一步执行初始化和启动服务器的操作。

26 创立 PID 文件

if (background || server.pidfile) createPidFile();

这段代码用于创立 PID 文件。PID 文件是一个文本文件,其中蕴含 Redis 服务器过程的过程 ID。通过查看该文件,其余程序能够确定 Redis 服务器正在运行的过程 ID,以便进行治理。如果服务器被配置为以守护过程形式运行,或者服务器须要将 PID 写入文件,则该函数将创立一个 PID 文件。如果服务器正在以监督模式运行,则不会创立 PID 文件。

void createPidFile(void) {
    /* If pidfile requested, but no pidfile defined, use
     * default pidfile path */
    if (!server.pidfile) server.pidfile = zstrdup(CONFIG_DEFAULT_PID_FILE);

    /* Try to write the pid file in a best-effort way. */
    FILE *fp = fopen(server.pidfile,"w");
    if (fp) {fprintf(fp,"%d\n",(int)getpid());
        fclose(fp);
    }
}

27 批改过程名称

if (server.set_proc_title) redisSetProcTitle(NULL);

这行代码是在调用 redisSetProcTitle()函数,用来批改以后过程的名称,以便于更好地标识以后正在执行的程序。redisSetProcTitle()函数在不同的平台上会有不同的实现,这里不再赘述。在这里,如果 server.set_proc_title 变量的值为 1,则执行 redisSetProcTitle(NULL)函数,将以后过程的名称设置为 NULL,即不显示名称。

28 ASCII 艺术字符画

  redisAsciiArt();

redisAsciiArt()是 Redis 启动时打印的 ASCII 艺术字符画,相似于 Logo。这个函数次要是为了让 Redis 启动时输入更加好看,减少用户体验。

29 查看以后 backlog

 checkTcpBacklogSettings();
void checkTcpBacklogSettings(void) {#if defined(HAVE_PROC_SOMAXCONN)
    FILE *fp = fopen("/proc/sys/net/core/somaxconn","r");
    char buf[1024];
    if (!fp) return;
    if (fgets(buf,sizeof(buf),fp) != NULL) {int somaxconn = atoi(buf);
        if (somaxconn > 0 && somaxconn < server.tcp_backlog) {serverLog(LL_WARNING,"WARNING: The TCP backlog setting of %d cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of %d.", server.tcp_backlog, somaxconn);
        }
    }
    fclose(fp);
#elif defined(HAVE_SYSCTL_KIPC_SOMAXCONN)
    int somaxconn, mib[3];
    size_t len = sizeof(int);

    mib[0] = CTL_KERN;
    mib[1] = KERN_IPC;
    mib[2] = KIPC_SOMAXCONN;

    if (sysctl(mib, 3, &somaxconn, &len, NULL, 0) == 0) {if (somaxconn > 0 && somaxconn < server.tcp_backlog) {serverLog(LL_WARNING,"WARNING: The TCP backlog setting of %d cannot be enforced because kern.ipc.somaxconn is set to the lower value of %d.", server.tcp_backlog, somaxconn);
        }
    }
#elif defined(HAVE_SYSCTL_KERN_SOMAXCONN)
    int somaxconn, mib[2];
    size_t len = sizeof(int);

    mib[0] = CTL_KERN;
    mib[1] = KERN_SOMAXCONN;

    if (sysctl(mib, 2, &somaxconn, &len, NULL, 0) == 0) {if (somaxconn > 0 && somaxconn < server.tcp_backlog) {serverLog(LL_WARNING,"WARNING: The TCP backlog setting of %d cannot be enforced because kern.somaxconn is set to the lower value of %d.", server.tcp_backlog, somaxconn);
        }
    }
#elif defined(SOMAXCONN)
    if (SOMAXCONN < server.tcp_backlog) {serverLog(LL_WARNING,"WARNING: The TCP backlog setting of %d cannot be enforced because SOMAXCONN is set to the lower value of %d.", server.tcp_backlog, SOMAXCONN);
    }
#endif
}

在 Redis 中,当客户端连贯 Redis 服务器时,Redis 会创立一个 TCP 套接字以进行通信。在 Linux 零碎中,这个套接字有一个 backlog 参数,用于指定容许挂起的未实现连贯的最大数量。

checkTcpBacklogSettings 函数中,Redis 会查看以后 backlog 设置是否过低,如果过低,将打印一条正告日志。

这是为了防止在客户端连接数减少时可能产生的连贯超时或回绝连贯等问题。倡议在高流量场景下将 backlog 设置得较高。

30 初始化实现日志

if (!server.sentinel_mode) {
        /* Things not needed when running in Sentinel mode. */
        serverLog(LL_WARNING,"Server initialized");

这段代码中,首先通过判断 server.sentinel_mode 是否为真来确定是否是在 Sentinel 模式下运行 Redis,如果不是,就打印一条日志,示意 Redis 服务器已初始化结束。在 Sentinel 模式下,该语句不会被执行,因为在 Sentinel 模式下 Redis 服务器只会执行 Sentinel 相干的操作,不会执行一般 Redis 服务器的操作。

31 Linux 查看

 #ifdef __linux__
        linuxMemoryWarnings();
        sds err_msg = NULL;
        if (checkXenClocksource(&err_msg) < 0) {serverLog(LL_WARNING, "WARNING %s", err_msg);
            sdsfree(err_msg);
        }
    #if defined (__arm64__)
        int ret;
        if ((ret = checkLinuxMadvFreeForkBug(&err_msg)) <= 0) {if (ret < 0) {serverLog(LL_WARNING, "WARNING %s", err_msg);
                sdsfree(err_msg);
            } else
                serverLog(LL_WARNING, "Failed to test the kernel for a bug that could lead to data corruption during background save."
                                      "Your system could be affected, please report this error.");
            if (!checkIgnoreWarning("ARM64-COW-BUG")) {
                serverLog(LL_WARNING,"Redis will now exit to prevent data corruption."
                                     "Note that it is possible to suppress this warning by setting the following config: ignore-warnings ARM64-COW-BUG");
                exit(1);
            }
        }
    #endif /* __arm64__ */

这段代码是对 Linux 操作系统下一些特定的问题进行检查和预警,其中包含:

  • linuxMemoryWarnings:在内存应用达到某个阈值时收回正告
  • checkXenClocksource:查看零碎时钟源是否正确,如果不正确则可能导致性能降落
  • checkLinuxMadvFreeForkBug:查看零碎是否受到特定内核 bug 影响,可能导致在后盾保留数据时呈现数据损坏

如果发现问题,会记录日志,并在某些状况下终止 Redis 的运行。

32 Redis 模块

      moduleInitModulesSystemLast();

moduleInitModulesSystemLast()是一个 Redis 模块的函数,它会在 Redis 服务器启动时执行,次要用于初始化所有已加载的 Redis 模块,并在 Redis 服务器胜利启动时调用各个模块的回调函数。

该函数次要实现以下操作:

  1. 遍历 Redis 模块列表,并调用每个模块的 RedisModule_OnLoad() 函数进行初始化。
  2. 在 Redis 服务器胜利启动后,遍历 Redis 模块列表,并调用每个模块的 RedisModule_OnAfterFork() 函数进行初始化。
  3. 在 Redis 服务器胜利启动后,遍历 Redis 模块列表,并调用每个模块的 RedisModule_EventuallyFree() 函数进行资源开释。

这些模块的回调函数是通过 Redis 模块 API 在模块的代码中定义的,并在模块初始化时通过 RedisModuleTypeMethods 构造体注册到 Redis 服务器中。每个模块的回调函数的实现不同,能够在模块代码中自定义。

该函数的作用是确保所有的 Redis 模块都曾经正确初始化,并且曾经被胜利加载到 Redis 服务器中。这是 Redis 模块化架构的外围之一,使得 Redis 能够轻松地扩大和增加新性能。

33 加载模块

 moduleLoadFromQueue();

moduleLoadFromQueue()函数用于从队列中加载模块。在 Redis 服务器启动时,会从服务器配置文件中读取模块的相干配置,而后依据配置来加载模块。模块加载时,会查看模块的版本信息和依赖关系,并将模块的 API 函数注册到 Redis 服务器中,从而让模块能够被调用。

在 Redis 服务器运行期间,如果须要加载新的模块或卸载现有的模块,能够通过发送 MODULE LOADMODULE UNLOAD命令来实现。这些命令会将指定的模块信息发送到队列中,而后由 moduleLoadFromQueue() 函数来解决。如果加载或卸载胜利,函数会返回相应的胜利信息,否则会返回错误信息。

34 ACL 用户信息

     ACLLoadUsersAtStartup();

ACLLoadUsersAtStartup() 是 Redis 在启动时加载 ACL 用户信息的函数。Redis 的 ACL (Access Control List) 性能能够用于管制用户对 Redis 实例的拜访权限,从而保障 Redis 实例的安全性。

ACLLoadUsersAtStartup() 的作用是读取 Redis 配置文件中指定的 ACL 配置,而后依据配置信息加载所有的用户和用户组到 Redis 实例中。如果没有配置 ACL,则默认应用 “default” 用户和 “allkeys” 用户组。

在 Redis 实例启动时调用 ACLLoadUsersAtStartup() 能够确保 Redis 实例在加载完所有须要的用户和用户组之后再开始对外提供服务,从而保障 Redis 实例的安全性。

35 InitServerLast

  InitServerLast();

InitServerLast() 是 Redis 初始化的最初一个步骤,它实现了一些初始化工作:

  • 初始化慢日志,通过调用 slowlogInit() 函数实现。
  • 初始化服务器统计信息,通过调用 statsMetricInit() 函数实现。
  • 初始化工夫事件,通过调用 aeCreateTimeEvent() 函数实现。
  • 初始化事件循环,通过调用 aeMain() 函数实现。

总之,InitServerLast() 函数是 Redis 初始化的最初一步,在这里,Redis 实现了许多重要的初始化工作,筹备好承受客户端的连贯并开始工作。

36 AOF 元数据

  aofLoadManifestFromDisk();

aofLoadManifestFromDisk()是 Redis 服务器启动后,加载长久化 AOF 模式的元数据文件的函数。AOF 是 Redis 的一种长久化形式,会将每个批改 Redis 数据库的命令追加到 AOF 文件的开端,以保证数据的长久化。

该函数会在服务器启动时调用,用于读取 redis.aof_manifest 文件中的数据,并将数据解析成 AOF 文件的元数据结构。

这个元数据蕴含了 AOF 文件的大小、最初一次执行 AOF 重写的工夫、AOF 文件的存储地位等信息。这些信息对于服务器进行 AOF 重写操作十分重要,因为服务器须要依据这些信息来决定是否须要执行 AOF 重写操作,以及应该如何进行 AOF 重写操作。

在函数外部,首先尝试关上 redis.aof_manifest 文件,如果文件关上失败,阐明以后服务器并不是第一次启动,也没有执行过 AOF 重写操作,能够间接返回。如果文件关上胜利,则从文件中读取元数据信息,并依据这些信息来更新服务器状态。

最初,如果有必要,函数会将新的 AOF 文件挪动到指定地位。这样,AOF 文件就曾经加载结束,并筹备好接管新的命令追加。

37 从磁盘回复文件

 loadDataFromDisk();

loadDataFromDisk()是 Redis 在启动时从磁盘中加载数据的函数,次要性能是读取 RDB 文件(如果存在)并将其中的数据恢复到 Redis 内存中。

具体来说,该函数首先尝试从配置文件中指定的 RDB 文件门路中加载 RDB 文件,如果找不到该文件,则尝试从主动备份文件中加载 RDB 文件。如果找到 RDB 文件,则执行 rdbLoad() 函数将其中的数据读取到 Redis 内存中。

须要留神的是,如果 Redis 配置中开启了 AOF 长久化,那么在 loadDataFromDisk() 函数加载 RDB 文件之后,还须要调用 aofRewriteIfNeeded() 函数执行 AOF 重写操作。该操作会依据 AOF 文件中的数据从新生成 RDB 文件,并将其中的数据写入到新的 AOF 文件中。这样能够保障 RDB 文件和 AOF 文件中的数据统一。

38 是否关上 aof?

   aofOpenIfNeededOnServerStart();

aofOpenIfNeededOnServerStart()函数次要用于在 Redis 服务器启动时关上 AOF 文件,如果服务器以后没有关上 AOF 长久化,则会依据配置项进行决定是否须要关上。

具体来说,该函数会依据以下几个方面进行判断:

  • 是否在 AOF 重写过程中,如果是,则不须要关上 AOF 文件。
  • 是否开启了 AOF 长久化性能,如果没有,则不须要关上 AOF 文件。
  • 是否开启了 AOF 主动重写性能,如果开启了,则依据条件判断是否须要触发 AOF 主动重写,如果须要,则不须要关上 AOF 文件。
  • 如果以上条件都不满足,则须要关上 AOF 文件。

在关上 AOF 文件之前,该函数还会尝试从 AOF 文件中载入数据到内存中。

总的来说,aofOpenIfNeededOnServerStart()函数的作用就是在 Redis 服务器启动时复原 AOF 长久化所保留的数据,使得 Redis 服务器可能复原到上一次停机时的状态。

39 删除旧的 AOF 文件

aofDelHistoryFiles()

函数 aofDelHistoryFiles() 的作用是删除旧的 AOF 文件。在 AOF 长久化模式下,Redis 会将所有写入命令都追加到 AOF 文件中,以保证数据长久化。当 AOF 文件过大时,Redis 会对其进行压缩和重写,生成新的 AOF 文件。此时,旧的 AOF 文件能够被删除,以开释磁盘空间。

在 Redis 启动时,aofDelHistoryFiles() 函数会查看是否开启了 AOF 长久化模式,并且是否启用了 AOF 压缩。如果开启了 AOF 压缩,那么 Redis 会保留多个历史版本的 AOF 文件,以便呈现问题时能够回滚到之前的版本。此时,aofDelHistoryFiles() 函数会删除旧的 AOF 文件,只保留最近几个版本的 AOF 文件,以开释磁盘空间。

须要留神的是,删除旧的 AOF 文件并不会影响 Redis 的失常运行。在下一次 AOF 压缩时,Redis 会主动清理不再应用的 AOF 文件。

40 集群模式查看 0 号数据库

   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);
            }
        }

这段代码的作用是在 Redis 以集群模式启动时,查看是否有非 0 号数据库中存在数据。在 Redis 集群模式下,每个节点只保护数据库 0,因而如果在其余数据库中有数据,则无奈将该节点增加到集群中。如果发现存在非 0 号数据库中存在数据,程序将输入错误信息并退出。

41 来连贯吧!

if (server.ipfd.count > 0 || server.tlsfd.count > 0)
            serverLog(LL_NOTICE,"Ready to accept connections");

这段代码是在 Redis 启动胜利后,当 IP 套接字或 TLS 套接字的数量大于 0 时,打印一个日志记录,示意 Redis 曾经筹备好承受客户端连贯了。其中,LL_NOTICE 是日志级别,示意一般提示信息。

 if (server.sofd > 0)
            serverLog(LL_NOTICE,"The server is now ready to accept connections at %s", server.unixsocket);

这段代码的作用是在 Redis 启动胜利后,如果 Redis 实例绑定了 TCP 端口或 TLS 端口,或者绑定了 Unix socket,就会在日志中打印出相应的提示信息,表明 Redis 曾经筹备好接管连贯。在这里,LL_NOTICE 级别的日志示意该信息是一个失常的告诉信息,表明 Redis 启动胜利并已筹备好接管连贯

 if (server.supervised_mode == SUPERVISED_SYSTEMD) {if (!server.masterhost) {redisCommunicateSystemd("STATUS=Ready to accept connections\n");
            } else {redisCommunicateSystemd("STATUS=Ready to accept connections in read-only mode. Waiting for MASTER <-> REPLICA sync\n");
            }
            redisCommunicateSystemd("READY=1\n");
        }
    } else {ACLLoadUsersAtStartup();
        InitServerLast();
        sentinelIsRunning();
        if (server.supervised_mode == SUPERVISED_SYSTEMD) {redisCommunicateSystemd("STATUS=Ready to accept connections\n");
            redisCommunicateSystemd("READY=1\n");
        }
    }

这段代码是在 Redis 服务器启动后,执行各种初始化工作之后,最终筹备好承受客户端连贯之前执行的。在这段代码中,依据 Redis 是否在 systemd 监管模式下运行,服务器会与 systemd 进行通信以告诉其已筹备好承受连贯。在非 systemd 监管模式下,Redis 会加载 ACL 用户信息,而后执行 InitServerLast 函数,该函数执行了一些其余的初始化工作,包含激活模块、开启 RDB/AOF 长久化、初始化 Sentinel 等。最初,Redis 会查看 Sentinel 是否正在运行,如果正在运行,将通过 Sentinel 的 API 向其它 Sentinel 实例发送音讯,以告诉它们以后实例曾经启动。如果服务器在 systemd 监管模式下运行,则服务器也会通过 systemd 的 API 与其通信以告诉其已筹备好承受连贯。

42 内存限度

 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);
    }

这段代码是在查看服务器是否设置了最大内存限度,如果设置了限度但限度值小于 1MB,会输入一个正告信息。正告信息中会显示以后设置的最大内存限度值。

其中,server.maxmemory示意设置的最大内存限度值,单位为字节。在这里,用 1024*1024 示意 1MB 的字节数。如果 server.maxmemory 小于 1MB 的字节数,就会输入正告信息。输入信息应用了 serverLog() 函数,以 LL_WARNING 级别记录在日志中。

43 CPU 亲和性

  redisSetCpuAffinity(server.server_cpulist);

redisSetCpuAffinity是 Redis 中的一个函数,用于设置 Redis 过程的 CPU 亲和性,即指定过程在哪些 CPU 上运行。

在调用该函数之前,Redis 会从配置文件中读取 server_cpulist 参数,该参数的值是一个以逗号分隔的 CPU 外围列表,例如 ”0,1,2,3″。如果该参数为空,则示意不设置 CPU 亲和性,Redis 过程能够在任何 CPU 上运行。

在 Linux 零碎中,CPU 亲和性能够通过 sched_setaffinity 零碎调用来实现。Redis 通过该零碎调用将过程绑定到指定的 CPU 外围。具体来说,redisSetCpuAffinity函数会先查看以后零碎是否反对 sched_setaffinity 零碎调用,如果反对则依据 server_cpulist 参数的值调用 sched_setaffinity 函数,将 Redis 过程绑定到指定的 CPU 外围上。如果不反对,则该函数不执行任何操作。

设置 CPU 亲和性能够进步 Redis 的性能,因为它能够防止过程在不同的 CPU 外围之间频繁切换,从而缩小上下文切换的开销。另外,设置 CPU 亲和性还能够防止 CPU 缓存的生效,从而进一步提高 Redis 的性能。

44 Redis 在内存不足的稳定性

  setOOMScoreAdj(-1);

setOOMScoreAdj(-1) 是 Redis 在启动时设置过程的 OOM Score(out-of-memory score)的值为 -1,这个值示意零碎不应该轻易地杀死这个过程。OOM Score 是 Linux 内核用来确定一个过程在内存不足的状况下是否应该被杀死的一个指标,它的值越低,示意这个过程越不容易被杀死。在 Redis 中,通过设置 OOM Score 为 -1,能够使得 Redis 过程在零碎内存不足的状况下更难被零碎杀死,从而进步 Redis 的稳定性和可靠性。

45 Redis 事件循环

 aeMain(server.el);
void aeMain(aeEventLoop *eventLoop) {
    eventLoop->stop = 0;
    while (!eventLoop->stop) {
        aeProcessEvents(eventLoop, AE_ALL_EVENTS|
                                   AE_CALL_BEFORE_SLEEP|
                                   AE_CALL_AFTER_SLEEP);
    }
}

aeMain(server.el)是 Redis 事件循环的次要函数,它通过循环遍历所有注册的文件事件和工夫事件来实现事件的解决。参数 server.el 是 Redis 的事件状态构造体,其中蕴含了事件循环的状态信息。aeMain()函数会阻塞以后线程,直到有事件就绪,而后调用相应的事件处理函数。在事件处理完毕后,aeMain()函数会持续循环解决其余事件。这个函数在 Redis 启动后始终运行,直到 Redis 过程被敞开。

46 END

 aeDeleteEventLoop(server.el);

void aeDeleteEventLoop(aeEventLoop *eventLoop) {aeApiFree(eventLoop);
    zfree(eventLoop->events);
    zfree(eventLoop->fired);

    /* Free the time events list. */
    aeTimeEvent *next_te, *te = eventLoop->timeEventHead;
    while (te) {
        next_te = te->next;
        zfree(te);
        te = next_te;
    }
    zfree(eventLoop);
}

函数 aeDeleteEventLoop(server.el) 用于删除事件循环,开释事件循环所占用的内存。

在 Redis 中,事件循环采纳了第三方库 ae 提供的事件驱动框架。Redis 的事件循环被封装在 aeEventLoop 构造体中,而函数 aeDeleteEventLoop(server.el) 则用于删除这个构造体。

在 Redis 中,当过程完结时,会调用 aeDeleteEventLoop(server.el) 来开释事件循环所占用的内存。此外,当 Redis 过程须要进行重启时,也会先调用aeDeleteEventLoop(server.el),再创立新的事件循环。

须要留神的是,在 Redis 中,如果开启了 AOF 长久化性能,那么在 Redis 过程启动时,也会调用函数 aeDeleteEventLoop(server.el) 来删除旧的事件循环,而后从新创立新的事件循环。这个过程是在函数 redisServeCommands() 中实现的。

47 总结

Redis 服务器启动过程大抵能够分为以下几步:

  1. 初始化服务器的各个配置选项、日志零碎和随机数生成器等,加载服务器所需的所有模块和库,并执行模块的初始化操作。
  2. 加载服务器的数据,包含配置文件中指定的数据、AOF 文件、RDB 文件等。
  3. 查看服务器是否在集群模式下运行,并进行相干的配置查看。
  4. 设置服务器的 CPU 亲和性,使其运行在指定的 CPU 上。
  5. 设置服务器的 OOM 分数,以管制内存应用。
  6. 如果服务器是在 supervised 模式下启动的,与 systemd 通信以报告服务器已筹备好承受连贯。
  7. 如果开启了 maxmemory 选项,查看其值是否大于 1MB。
  8. 启动事件循环,开始解决客户端申请和其余事件。
  9. 在服务器敞开时,清理资源并开释内存。

其中,步骤 3 和步骤 8 是比拟要害的,因为它们波及到了 Redis 的外围性能。在查看服务器是否在集群模式下运行时,如果服务器配置有误,将会强制退出。在启动事件循环后,服务器将开始监听客户端申请,并依据申请类型调用相应的处理函数进行解决,最终返回响应后果。在事件循环过程中,如果遇到了其余事件(如定时工作等),也会依据事件类型调用相应的处理函数进行解决。

前面呢,我还会持续向大家具体的介绍外面重要的函数。心愿大家持续关注哦。

正文完
 0