32

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

 4 years ago
source link: http://www.cnblogs.com/grey-wolf/p/12682760.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

文章导航

Redis源码系列的初衷,是帮助我们更好地理解Redis,更懂Redis,而怎么才能懂,光看是不够的,建议跟着下面的这一篇,把环境搭建起来,后续可以自己阅读源码,或者跟着我这边一起阅读。由于我用c也是好几年以前了,些许错误在所难免,希望读者能不吝指出。

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

一些补充知识

项目结构及入口

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

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

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

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

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

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

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

其他include相关知识,可以参考:

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

我对头文件的理解

一般来说,我们会在.c文件中,去编写我们的业务逻辑方法,其中,一些方法,可能是只在本文件内部用到的,类似于java class的private方法;一些方法呢,可能是需要在外部的其他源码文件中,也需要用到的,这些方法,要怎么才能让外部可以使用呢?

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

说回头文件,比如有个源码文件 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。

7NfAfyV.png!web

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学习使用,不用担心这些问题。

指针那一块,需要一点点基础,大家可以花点时间学一下。

大家看看有啥问题或者建议,欢迎指出。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK