曹工说Redis源码(2)-- redis server 启动过程解析及简单c语言基础知识补充

文章导航

Redis源码系列的初衷,是帮助咱们更好地理解Redis,更懂Redis,而怎么才能懂,光看是不够的,建议跟着下面的这一篇,把环境搭建起来,后续能够本身阅读源码,或者跟着我这边一块儿阅读。因为我用c也是好几年之前了,些许错误在所不免,但愿读者能不吝指出。html

曹工说Redis源码(1)-- redis debug环境搭建,使用clion,达到和调试java同样的效果java

一些补充知识

项目结构及入口

除了大学那些玩具,一个真正的项目,都是由大量源代码文件组成一个工程。在Java里,一个 java 文件要使用其余 java 文件中的函数、类型、变量等,都须要使用import语句来引入。在c语言里,也是同样的,在c语言中,要引入其余文件的功能,须要使用include语句。node

好比,在redis的主入口,redis.c文件中,就包含了以下一堆语句:linux

#include "redis.h"
#include "cluster.h"
#include "slowlog.h"
#include "bio.h"

#include <time.h>
#include <signal.h>

其中,以<开头的,好比<time.h>是标准库的头文件,会在系统指定的路径下查找,可类比为jdk官方的class;"bio.h"这种,以""包裹的,则是工程里自定义的。c++

好比,time.h,我在linux的如下路径查找到了:redis

[root@mini1 src]# locate time.h
/usr/include/time.h

其余include相关知识,能够参考:shell

https://www.runoob.com/cprogramming/c-header-files.html数据库

我对头文件的理解

通常来讲,咱们会在.c文件中,去编写咱们的业务逻辑方法,其中,一些方法,多是只在本文件内部用到的,相似于java class的private方法;一些方法呢,多是须要在外部的其余源码文件中,也须要用到的,这些方法,要怎么才能让外部可使用呢?编程

就是经过头文件机制,能够理解为各大高级语言中的接口,在java中,定义一个class,虽然能够直接把方法设为public,其余类能够直接访问;可是,在平时的业务开发中,咱们通常并不会直接访问一个实现类,而是经过它实现的接口去访问;一个好的实现类,也不该该把没在接口中定义的方法,设为public权限。centos

说回头文件,好比有个源码文件test.c 以下:

long long ustime(void) {
    struct timeval tv;
    long long ust;

    gettimeofday(&tv, NULL);
    ust = ((long long)tv.tv_sec)*1000000;
    ust += tv.tv_usec;
    return ust;
}
/* Return the UNIX time in milliseconds */
// 返回毫秒格式的 UNIX 时间
// 1 秒 = 1 000 毫秒
long long mstime(void) {
    return ustime()/1000;
}

这个文件里,定义了2个方法,但假设咱们只须要对外暴露mstime(void)方法,那么,头文件test.h应该是下面这样的:

long long mstime(void);

这样的话,咱们的另外一个方法,ustime,对外就不可见了。

总之,你们能够把头文件理解为实现类要对外暴露的接口;你们可能以为个人比喻不恰当,为啥把c文件,说成实现类,实际上,咱们以前在华为的时候,确实是用c++的思想,面向对象的思想,来写c语言的。

我看到网上一篇文章,这里引用一下(https://zhuanlan.zhihu.com/p/57882822):

反观Redis,他是纯C编码,可是融入了面向对象的思想。和上述观点截然相反,可谓是『用C++去设计,用C编码』。固然本文目的并不是挑起语言之争,各类语言自有其利弊,开源项目的语言选择也主要是因为项目做者的我的经历和主观意愿。

可是c语言中的头文件,和java这些语言中的接口,仍是不一样的;在java中,接口和实现类同样,最终都是编译为独立的class文件。

在c语言中,在编译实现类以前,会有一个预处理的过程,预处理的过程,就是把include语句,直接替换为被include的头文件的内容,好比,以菜鸟教程中的例子举例:

header.h
 char *test (void);

在以下的 program.c中,须要使用上面的header.h中的test方法,则须要include:

int x;
#include "header.h"

int main (void)
{
   puts (test ());
}

通过预处理后,(就是进行简单的replace),效果以下:

int x;
char *test (void);

int main (void)
{
   puts (test ());
}

咱们可使用以下命令,来演示这个过程:

[root@mini1 test]# gcc -E program.c 
int x;
# 1 "header.h" 1

char *test (void);
# 3 "program.c" 2

int main (void)
{
   puts (test ());
}

从上面能够看到,已经replace进去了;若是咱们include两次,会怎样?

[root@mini1 test]# gcc -E program.c 
int x;
# 1 "header.h" 1

char *test (void);
# 3 "program.c" 2
# 1 "header.h" 1

char *test (void);
# 4 "program.c" 2
int main (void)
{
   puts (test ());
}

能够发现,这个header的内容,出现了2次,重复了。可是上面这种状况,并不会报错,无非是方法被定义了两次。

为何头文件里都要来一句ifndef

你们看头文件,都会发现以下语句,好比在redis.h中:

#ifndef __REDIS_H
#define __REDIS_H

#include "fmacros.h"
#include "config.h"

...
    
typedef struct redisObject {

    // 类型
    unsigned type:4;

    // 编码
    unsigned encoding:4;

    // 对象最后一次被访问的时间
    unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */

    // 引用计数
    int refcount;

    // 指向实际值的指针
    void *ptr;

} robj;

...
    
#endif

能够看到,最开始,有一句:

#ifndef __REDIS_H
#define __REDIS_H

结尾有一句:

#endif

这个就是为了解决以下问题:

在头文件被重复引入时(间接地,或直接地,被include了两次),若是不加这个,就会致使头文件里的内容,被引入两次;加了这个以后呢,即便被include了两次,程序在运行时,一开始,发现没有定义__REDIS_H这个宏,而后定义它;等到程序遇到第二次include的内容时,发现__REDIS_H这个宏已经被定义了,就直接跳过了,这样保证了同一个头文件,即便被屡次include,也能保证其内容,只被解析一次。

另外,像方法声明这种,定义屡次可能没事,可是,若是在头文件里,有以下类型定义呢:

typedef char my_char;
char *test (void);

若是重复include同一个头文件的话,就会形成类型重复定义。不过,很奇怪的是,我在centos 7.3.1611上试了,gcc版本:gcc (GCC) 4.8.5 20150623 (Red Hat 4.8.5-16),居然没报错。看来我以前的c语言知识,也没学到家。

我在网上暂时也没找到重复include,具体的害处是啥,网上找到的答案就两种:

  1. 在header文件里定义了全局变量;
  2. 浪费编译时间

可是,第一个答案,严格来讲 ,是不存在的,由于公司通常禁止在头文件中定义变量。

有个知乎问题,你们能够看看:头文件被重复包含究竟有哪些危害?

华为c语言编程规范中,对头文件的部分规定

你们能够自行搜索:华为技术有限公司c语言编程规范

我这里仅截取部分:

规则1.6 禁止在头文件中定义变量。
说明: 在头文件中定义变量,将会因为头文件被其余.c文件包含而致使变量重复定义。
    
规则1.7 只能经过包含头文件的方式使用其余.c提供的接口,禁止在.c中经过extern的方式使用外部
函数接口、变量。
说明:若a.c使用了b.c定义的foo()函数,则应当在b.h中声明extern int foo(int input);并在a.c
中经过#include <b.h>来使用foo。禁止经过在a.c中直接写extern int foo(int input);来使用foo,
后面这种写法容易在foo改变时可能致使声明和定义不一致。

这里的1.7,也是和咱们的理解是一致的,头文件就是一个实现模块的对外接口,在里面通常只能容许放如下内容:

  • 类型定义
  • 宏定义
  • 函数的声明(不包括实现)
  • 变量的声明(不是定义)

最后这一点,我要补充下。咱们刚才禁止了,在头文件中定义变量,因此,咱们的变量,是在c文件中定义。好比,在redis.c中,定义了一个全局变量:

/* Global vars */
struct redisServer server; /* server global state */

这么一个重要的全局变量,基本维护了redis-server的一个实例的所有状态值,只在本身redis.c中使用,是不可能的。那要怎么在其余文件使用呢,就要在redis.h头文件中进行以下声明:

/*-----------------------------------------------------------------------------
 * Extern declarations
 *----------------------------------------------------------------------------*/

extern struct redisServer server;

关于类型定义

通常使用struct来定义一个结构体,相似高级语言中的class。

好比,redis中的字符串,通常会使用sds这个数据结构来存储,其结构体定义就像下面这样:

struct sdshdr {

    // buf 中已占用空间的长度
    int len;

    // buf 中剩余可用空间的长度
    int free;

    // 数据空间
    char buf[];
};

另外,c语言中,会大量使用typedef来定义一个类型的别名。

具体能够参考这个教程看看:

https://www.runoob.com/cprogramming/c-typedef.html

关于指针

基础知识:https://www.runoob.com/cprogramming/c-pointers.html

我这里说下我对指针的理解,指针通常指向一个内存地址,你们能够先无论这个指针是什么类型,事实上,当咱们不关心其指向的地址上,是什么数据类型时,能够直接定义为 void * ptr。

这个指针,假设指向A这个地址,当咱们认为上面存储的是一个char时,就能够把这个指针,从void *强转为char * 类型,而后对该指针解引用的话,由于char类型只占用一个字节,因此只须要,从该指针指向的位置开始,取当前这个字节的内容,而后解析为char,就能获取到这个地址上的char值。

若是咱们把void * 强转为int *的话,对其解引用时,就会取当前指针位置开始的4个字节,由于整数占4个字节,而后将其转为整数。

总的来讲,对一个指针解引用时,首先就是看当前指针的数据类型,好比 int *指针,那么说明指向int,就会取4个字节来进行解引用;若是是指向一个结构体,就会计算结构体占用的字节数,而后取对应的字节,来解引用为结构体类型的变量。

这部分,你们能够看看这块:

https://www.runoob.com/cprogramming/c-data-types.html

https://www.runoob.com/cprogramming/c-pointer-arithmetic.html

redis启动过程之配置项初始化

前面说了不少,咱们本讲也不够讲彻底部的redis启动过程了,可能还要两讲的样子,本讲先讲解一部分。

启动入口在:redis.c中的main 方法,若是使用我这边的代码来搭建调试环境,能够直接启动redis-server。

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

    /**
     * 1 设置时区
     */
    setlocale(LC_COLLATE,"");
    /**
     *2
     */
    zmalloc_enable_thread_safeness();
    // 3
    zmalloc_set_oom_handler(redisOutOfMemoryHandler);
    // 4
    srand(time(NULL)^getpid());
    // 5
    gettimeofday(&tv,NULL);
    // 6
    dictSetHashFunctionSeed(tv.tv_sec^tv.tv_usec^getpid());

    // 检查服务器是否以 Sentinel 模式启动
    server.sentinel_mode = checkForSentinelMode(argc,argv);

    // 7 初始化服务器
    initServerConfig();
  • 1处,设置时区

  • 2处,设置进行内存分配的线程的数量,这里会设为1

  • 3处,设置oom发生时的函数指针,函数指针指向一个函数,相似于java 8中,lambda表达式中,丢一个方法的引用给流;函数指针会在oom时,被回调,整体来讲,就相似于java中的模板设计模式或者策略模式。

  • 4处,设置随机数的种子

  • 5处,获取当前时间,设置到 tv这个变量中

    注意,这里把tv的地址传进去了,这是c语言中典型的用法,相似于java中传一个对象的引用进去,而后在方法内部,会修改该对象的内部field等

  • 6处,设置hash函数的种子

  • 7处,初始化服务器。

这里重点说下7处:

void initServerConfig() {
    int j;

    // 服务器状态

    // 设置服务器的运行 ID
    getRandomHexChars(server.runid,REDIS_RUN_ID_SIZE);
    // 设置默认配置文件路径
    server.configfile = NULL;
    // 设置默认服务器频率
    server.hz = REDIS_DEFAULT_HZ;
    // 为运行 ID 加上结尾字符
    server.runid[REDIS_RUN_ID_SIZE] = '\0';
    // 设置服务器的运行架构
    server.arch_bits = (sizeof(long) == 8) ? 64 : 32;
    // 设置默认服务器端口号
    server.port = REDIS_SERVERPORT;
    // tcp 全链接队列的长度
    server.tcp_backlog = REDIS_TCP_BACKLOG;
    // 绑定的地址的数量
    server.bindaddr_count = 0;
    // UNIX socket path
    server.unixsocket = NULL;
    server.unixsocketperm = REDIS_DEFAULT_UNIX_SOCKET_PERM;
    // 绑定的 TCP socket file descriptors
    server.ipfd_count = 0;
    server.sofd = -1;
    // redis可以使用的redis db的数量
    server.dbnum = REDIS_DEFAULT_DBNUM;
    // redis 日志级别
    server.verbosity = REDIS_DEFAULT_VERBOSITY;
    // Client timeout in seconds,客户端最大空闲时间;超过这个时间的客户端,会被强制关闭
    server.maxidletime = REDIS_MAXIDLETIME;
    // Set SO_KEEPALIVE if non-zero. 若是设为非0,则开启tcp的SO_KEEPALIVE
    server.tcpkeepalive = REDIS_DEFAULT_TCP_KEEPALIVE;
    // 打开这个选项,会周期性地清理过时key
    server.active_expire_enabled = 1;
    // 客户端发来的请求中,查询缓存的最大值;好比一个set命令,value的大小就会和这个缓冲区大小比较,
    // 若是大了,就根本放不进缓冲区
    server.client_max_querybuf_len = REDIS_MAX_QUERYBUF_LEN;

    // rdb保存参数,好比每60s保存,n个键被修改了保存,之类的
    server.saveparams = NULL;
    // 若是为1,表示服务器正在从磁盘载入数据: We are loading data from disk if true
    server.loading = 0;
    // 日志文件位置
    server.logfile = zstrdup(REDIS_DEFAULT_LOGFILE);
    // 开启syslog等机制
    server.syslog_enabled = REDIS_DEFAULT_SYSLOG_ENABLED;
    server.syslog_ident = zstrdup(REDIS_DEFAULT_SYSLOG_IDENT);
    server.syslog_facility = LOG_LOCAL0;
    // 后台运行
    server.daemonize = REDIS_DEFAULT_DAEMONIZE;
    // aof状态
    server.aof_state = REDIS_AOF_OFF;
    // aof的刷磁盘策略,默认每秒刷盘
    server.aof_fsync = REDIS_DEFAULT_AOF_FSYNC;
    // 正在rewrite时,不刷盘
    server.aof_no_fsync_on_rewrite = REDIS_DEFAULT_AOF_NO_FSYNC_ON_REWRITE;
    // Rewrite AOF if % growth is > M and...
    server.aof_rewrite_perc = REDIS_AOF_REWRITE_PERC;
    // the AOF file is at least N bytes. aof达到多大时,触发rewrite
    server.aof_rewrite_min_size = REDIS_AOF_REWRITE_MIN_SIZE;
    //  最后一次执行 BGREWRITEAOF 时, AOF 文件的大小
    server.aof_rewrite_base_size = 0;
    // Rewrite once BGSAVE terminates.开启该选项时,BGSAVE结束时,触发rewrite
    server.aof_rewrite_scheduled = 0;
    // 最近一次aof进行fsync的时间
    server.aof_last_fsync = time(NULL);
    // 最近一次aof重写,消耗的时间
    server.aof_rewrite_time_last = -1;
    //  Current AOF rewrite start time.
    server.aof_rewrite_time_start = -1;
    // 最后一次执行 BGREWRITEAOF 的结果
    server.aof_lastbgrewrite_status = REDIS_OK;
    // 记录 AOF 的 fsync 操做被推迟了多少次
    server.aof_delayed_fsync = 0;
    //  File descriptor of currently selected AOF file
    server.aof_fd = -1;
    // AOF 的当前目标数据库
    server.aof_selected_db = -1; /* Make sure the first time will not match */
    // UNIX time of postponed AOF flush
    server.aof_flush_postponed_start = 0;
    // fsync incrementally while rewriting? 重写过程当中,增量触发fsync
    server.aof_rewrite_incremental_fsync = REDIS_DEFAULT_AOF_REWRITE_INCREMENTAL_FSYNC;
    // pid文件
    server.pidfile = zstrdup(REDIS_DEFAULT_PID_FILE);
    // rdb 文件名
    server.rdb_filename = zstrdup(REDIS_DEFAULT_RDB_FILENAME);
    // aof 文件名
    server.aof_filename = zstrdup(REDIS_DEFAULT_AOF_FILENAME);
    // 是否要密码
    server.requirepass = NULL;
    // 是否进行rdb压缩
    server.rdb_compression = REDIS_DEFAULT_RDB_COMPRESSION;
    // rdb checksum
    server.rdb_checksum = REDIS_DEFAULT_RDB_CHECKSUM;
    // bgsave失败,中止写入
    server.stop_writes_on_bgsave_err = REDIS_DEFAULT_STOP_WRITES_ON_BGSAVE_ERROR;
    // 在执行 serverCron() 时进行渐进式 rehash
    server.activerehashing = REDIS_DEFAULT_ACTIVE_REHASHING;

    server.notify_keyspace_events = 0;
    // 支持的最大客户端数量
    server.maxclients = REDIS_MAX_CLIENTS;
    // bpop阻塞的客户端
    server.bpop_blocked_clients = 0;
    // 可使用的最大内存
    server.maxmemory = REDIS_DEFAULT_MAXMEMORY;
    // 内存淘汰策略,也就是key的过时策略
    server.maxmemory_policy = REDIS_DEFAULT_MAXMEMORY_POLICY;
    server.maxmemory_samples = REDIS_DEFAULT_MAXMEMORY_SAMPLES;
    // hash表的元素小于这个值时,使用ziplist 编码模式;如下几个相似
    server.hash_max_ziplist_entries = REDIS_HASH_MAX_ZIPLIST_ENTRIES;
    server.hash_max_ziplist_value = REDIS_HASH_MAX_ZIPLIST_VALUE;
    server.list_max_ziplist_entries = REDIS_LIST_MAX_ZIPLIST_ENTRIES;
    server.list_max_ziplist_value = REDIS_LIST_MAX_ZIPLIST_VALUE;
    server.set_max_intset_entries = REDIS_SET_MAX_INTSET_ENTRIES;
    server.zset_max_ziplist_entries = REDIS_ZSET_MAX_ZIPLIST_ENTRIES;
    server.zset_max_ziplist_value = REDIS_ZSET_MAX_ZIPLIST_VALUE;
    server.hll_sparse_max_bytes = REDIS_DEFAULT_HLL_SPARSE_MAX_BYTES;
    // 该标识打开时,表示正在关闭服务器
    server.shutdown_asap = 0;
    // 复制相关
    server.repl_ping_slave_period = REDIS_REPL_PING_SLAVE_PERIOD;
    server.repl_timeout = REDIS_REPL_TIMEOUT;
    server.repl_min_slaves_to_write = REDIS_DEFAULT_MIN_SLAVES_TO_WRITE;
    server.repl_min_slaves_max_lag = REDIS_DEFAULT_MIN_SLAVES_MAX_LAG;
    // cluster模式相关
    server.cluster_enabled = 0;
    server.cluster_node_timeout = REDIS_CLUSTER_DEFAULT_NODE_TIMEOUT;
    server.cluster_migration_barrier = REDIS_CLUSTER_DEFAULT_MIGRATION_BARRIER;
    server.cluster_configfile = zstrdup(REDIS_DEFAULT_CLUSTER_CONFIG_FILE);
    // lua脚本
    server.lua_caller = NULL;
    server.lua_time_limit = REDIS_LUA_TIME_LIMIT;
    server.lua_client = NULL;
    server.lua_timedout = 0;
    //
    server.migrate_cached_sockets = dictCreate(&migrateCacheDictType,NULL);
    server.loading_process_events_interval_bytes = (1024*1024*2);

    // 初始化 LRU 时间
    server.lruclock = getLRUClock();

    // 初始化并设置保存条件
    resetServerSaveParams();

    // rdb的默认保存策略
    appendServerSaveParams(60*60,1);  /* save after 1 hour and 1 change */
    appendServerSaveParams(300,100);  /* save after 5 minutes and 100 changes */
    appendServerSaveParams(60,10000); /* save after 1 minute and 10000 changes */

    /* Replication related */
    // 初始化和复制相关的状态
    server.masterauth = NULL;
    server.masterhost = NULL;
    server.masterport = 6379;
    server.master = NULL;
    server.cached_master = NULL;
    server.repl_master_initial_offset = -1;
    server.repl_state = REDIS_REPL_NONE;
    server.repl_syncio_timeout = REDIS_REPL_SYNCIO_TIMEOUT;
    server.repl_serve_stale_data = REDIS_DEFAULT_SLAVE_SERVE_STALE_DATA;
    server.repl_slave_ro = REDIS_DEFAULT_SLAVE_READ_ONLY;
    server.repl_down_since = 0; /* Never connected, repl is down since EVER. */
    server.repl_disable_tcp_nodelay = REDIS_DEFAULT_REPL_DISABLE_TCP_NODELAY;
    server.slave_priority = REDIS_DEFAULT_SLAVE_PRIORITY;
    server.master_repl_offset = 0;

    /* Replication partial resync backlog */
    // 初始化 PSYNC 命令所使用的 backlog
    server.repl_backlog = NULL;
    server.repl_backlog_size = REDIS_DEFAULT_REPL_BACKLOG_SIZE;
    server.repl_backlog_histlen = 0;
    server.repl_backlog_idx = 0;
    server.repl_backlog_off = 0;
    server.repl_backlog_time_limit = REDIS_DEFAULT_REPL_BACKLOG_TIME_LIMIT;
    server.repl_no_slaves_since = time(NULL);

    /* Client output buffer limits */
    // 设置客户端的输出缓冲区限制
    for (j = 0; j < REDIS_CLIENT_LIMIT_NUM_CLASSES; j++)
        server.client_obuf_limits[j] = clientBufferLimitsDefaults[j];

    /* Double constants initialization */
    // 初始化浮点常量
    R_Zero = 0.0;
    R_PosInf = 1.0/R_Zero;
    R_NegInf = -1.0/R_Zero;
    R_Nan = R_Zero/R_Zero;


    // 初始化命令表,好比get、set、hset等各自的处理函数,放进一个hash表,方便后续处理请求
    server.commands = dictCreate(&commandTableDictType,NULL);
    server.orig_commands = dictCreate(&commandTableDictType,NULL);
    populateCommandTable();
    server.delCommand = lookupCommandByCString("del");
    server.multiCommand = lookupCommandByCString("multi");
    server.lpushCommand = lookupCommandByCString("lpush");
    server.lpopCommand = lookupCommandByCString("lpop");
    server.rpopCommand = lookupCommandByCString("rpop");
    
    /* Slow log */
    // 初始化慢查询日志
    server.slowlog_log_slower_than = REDIS_SLOWLOG_LOG_SLOWER_THAN;
    server.slowlog_max_len = REDIS_SLOWLOG_MAX_LEN;

    /* Debugging */
    // 初始化调试项
    server.assert_failed = "<no assertion failed>";
    server.assert_file = "<no file>";
    server.assert_line = 0;
    server.bug_report_start = 0;
    server.watchdog_period = 0;
}

以上都加了注释,咱们能够先不看:复制、cluster、lua等相关的,先看其余的。

总结

过久没碰c了,有些遗忘,不过整体来讲,并不难,难的是内存泄露之类,但咱们只是debug学习使用,不用担忧这些问题。

指针那一块,须要一点点基础,你们能够花点时间学一下。

你们看看有啥问题或者建议,欢迎指出。

相关文章
相关标签/搜索