当前位置: 首页 > news >正文

Redis源码-BFS方式浏览main函数

文章目录

  • 前言
  • 看代码的方式
  • 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 函数,参数 argcargv 对于一个命令行程序来说可以是重头戏,肯定会拿来做重度解析的,函数开头还定义了 tvj 两个变量,不知道干嘛的,接着往下看吧。

启动测试程序

#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();
  1. INIT_SETPROCTITLE_REPLACEMENT 这个宏存在的时候,调用 spt_init 函数来为设置程序标题做准备
  2. setlocale() 用来设置地点信息,这一句应该是设置成依赖操作系统的地点信息,比如中国,韩国等等
  3. tzset() 设置时区,这里可能影响到程序运行后,调整时区是否对程序产生影响
  4. srand(time(NULL)^getpid()); 初始化随机种子
  5. gettimeofday(&tv,NULL); 这里用到了函数开头定义的一个变量 tv,用来获取当前时间
  6. 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();
  1. 定一个16字节的空间用来存放哈希种子
  2. 随机获取一段16字节数据作为种子
  3. 将刚刚获取的种子数据设置到hash函数中
  4. 分析命令行参数,判断是否是哨兵模式
  5. 初始化服务器配置
  6. ACL 初始化,不用管它具体是什么,进入下一层时自然会看到
  7. 初始化模块系统
  8. 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);

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

下面这两个参数有点意思,简单扩展下,rdbaofredis 的两种数据落地的持久化方式,这里有意思的地方是判断了 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 < 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 函数结尾的部分,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 函数中也可以看到,大部分函数是驼峰命名法,还要少量的下划线命名和帕斯卡命名。

==>> 反爬链接,请勿点击,原地爆炸,概不负责!<<==

你微笑的模样,提醒着我不要躲藏,坚持原来的方向,哪怕最后遍体鳞伤,困难只会让坚持的人越来越强,共勉~

相关文章:

  • GDB调试指北-启动调试或者附加到进程
  • Python中时间戳、时间字符串、时间结构对象之间的相互转化
  • git log根据特定条件查询日志并统计修改的代码行数
  • C++中优先队列priority_queue的基础用法
  • C++求解组合数的具体实现
  • 东拉西扯01世界的沧海桑田
  • Go语言在解决实际问题时的优点与不便
  • 使用Spreadsheet Compare工具对比Excel文件差异
  • linux环境下使用sort命令完成常见排序操作
  • 关于数据一致性的思考
  • linux环境下sed命令的基础用法
  • 学习cmake从成功编译一个小程序开始
  • linux环境下使用netstat命令查看网络信息
  • 简单聊聊01世界中编码和解码这对磨人的小妖儿
  • C/C++中有符号数隐式类型转换成无符号数需注意的问题
  • [LeetCode] Wiggle Sort
  • Angular 2 DI - IoC DI - 1
  • hadoop入门学习教程--DKHadoop完整安装步骤
  • JavaScript 无符号位移运算符 三个大于号 的使用方法
  • java架构面试锦集:开源框架+并发+数据结构+大企必备面试题
  • magento 货币换算
  • React中的“虫洞”——Context
  • SOFAMosn配置模型
  • vue2.0项目引入element-ui
  • 聊聊sentinel的DegradeSlot
  • 前端技术周刊 2019-01-14:客户端存储
  • 前端路由实现-history
  • 扫描识别控件Dynamic Web TWAIN v12.2发布,改进SSL证书
  • 说说动画卡顿的解决方案
  • 想使用 MongoDB ,你应该了解这8个方面!
  • 移动端唤起键盘时取消position:fixed定位
  • 做一名精致的JavaScripter 01:JavaScript简介
  • nb
  • 东超科技获得千万级Pre-A轮融资,投资方为中科创星 ...
  • #define、const、typedef的差别
  • #图像处理
  • (3)Dubbo启动时qos-server can not bind localhost22222错误解决
  • (二)windows配置JDK环境
  • (附源码)计算机毕业设计SSM在线影视购票系统
  • (九)One-Wire总线-DS18B20
  • (原创) cocos2dx使用Curl连接网络(客户端)
  • (转)拼包函数及网络封包的异常处理(含代码)
  • .form文件_一篇文章学会文件上传
  • .gitignore文件_Git:.gitignore
  • .NET Core中Emit的使用
  • .NET 编写一个可以异步等待循环中任何一个部分的 Awaiter
  • .net 反编译_.net反编译的相关问题
  • .NET3.5下用Lambda简化跨线程访问窗体控件,避免繁复的delegate,Invoke(转)
  • .NetCore 如何动态路由
  • .NET学习教程二——.net基础定义+VS常用设置
  • .NET学习全景图
  • [android]-如何在向服务器发送request时附加已保存的cookie数据
  • [C#]winform部署yolov5-onnx模型
  • [C#]科学计数法(scientific notation)显示为正常数字
  • [c]扫雷