redis源码分析(1)——初始化

最近一直在看redis的源码,准备把对源码的理解以及阅读心得记录下来,避免忘记并可以和大家分享、谈论。看代码的思路很简单,直接从main函数走起,先看看初始化过程。

redis中一个最重要的数据结构是redis_server,会创建一个这个结构的全局变量server,表示当前redis的配置及状态,初始化的大部分工作就是设置这个结构的属性。

可以把初始化工作主要划分为4个部分:

1)为server设置默认值

2)解析命令行参数

3)解析配置文件

4)初始化server

初始化完成后,就会启动事件循环,以接收、服务请求。下面分步骤解析main函数,不会关注sentinel逻辑。

(1)首先是main函数的起始部分,主要进行简单的初始化工作,包括设置collate,设置随机数种子,然后调用initServerConfig()为全局变量server设置默认值。

struct timeval tv;

    /* We need to initialize our libraries, and the server configuration. */
#ifdef INIT_SETPROCTITLE_REPLACEMENT
    spt_init(argc, argv);
#endif
    // <MM>
    // 使用环境变量初始化 字符串比较
    // </MM>
    setlocale(LC_COLLATE,"");
    zmalloc_enable_thread_safeness();
    zmalloc_set_oom_handler(redisOutOfMemoryHandler);
    // <MM>
    // 设置random的种子,之后会生成随机的run id,所有加入进程id因素
    // </MM>
    srand(time(NULL)^getpid());
    gettimeofday(&tv,NULL);
    dictSetHashFunctionSeed(tv.tv_sec^tv.tv_usec^getpid());
    server.sentinel_mode = checkForSentinelMode(argc,argv);
    // <MM>
    // 为redisServer设置初始默认值
    // </MM>
    initServerConfig();

    /* 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();
    }

在initServerConfig函数中,大部分是对server的属性设置默认值,还有一部分是调用populateCommandTable函数对redis的命令表初始化。全局变量redisCommandTable是redisCommand类型的数组,保存redis支持的所有命令。server.commands是一个dict,保存命令名到redisCommand的映射。populateCommandTable函数会遍历全局redisCommandTable表,把每条命令插入到server.commands中,根据每个命令的属性设置其flags。

redisCommand结构如下:

struct redisCommand {
    // 命令名称,在server.commands命令表中,以命令名位key
    char *name;
    // 命令处理函数
    redisCommandProc *proc;
    // command的操作数,>0时表示确切的操作数,<0则表示至少有arity个操作数
    int arity;
    // 标记位的字符表示形式,主要用于命令表的初始化
    char *sflags; /* Flags as string representation, one char per flag. */
    // 属性标记位,bitwise,指定command的类型
    int flags;    /* The actual flags, obtained from the ‘sflags‘ field. */

    // 下面的4个属性都是用于从客户端的一个请求解析出key,比如mset k1, v1, k2, v2 ...
    /* Use a function to determine keys arguments in a command line. */
    redisGetKeysProc *getkeys_proc;
    /* What keys should be loaded in background when calling this command? */
    int firstkey; /* The first argument that‘s a key (0 = no keys) */
    int lastkey;  /* The last argument that‘s a key */
    int keystep;  /* The step between first and last key */
    // 统计信息
    long long microseconds, calls;
};

redisCommand有属性proc,表示命令处理函数。在处理客户端请求时,获取到命令名,从server.commands字典中获取到redisCommand,然后回调其proc函数。

(2)命令行解析部分,主要是解析出命令行配置选项,以及通过命令行传入的配置项。然后会解析配置文件,主要是通过两个函数:

loadServerConfig:完成的功能很简单,就是将文件内容读到字符串中。并将通过命令行传入的配置项追加到该字符串后。

loadServerConfigFromString:从字符串中解析出配置项,并设置server的相关属性。

此步完成后,server中的简单属性(整数、字符串)基本都设置完成。

(3)接下来是初始化部分。设置为守护进程,创建pid文件,设置进程title以及打印启动日志。其中最重要的是initServer函数初始化基础数据结构。在初始化之后,需要从磁盘加载数据,可以是rdb或者aof。

    // <MM>
    // 守护进程
    // </MM>
    if (server.daemonize) daemonize();
    initServer();
    // <MM>
    // 创建pid文件
    // </MM>
    if (server.daemonize) createPidFile();
    // <MM>
    // 设置进程名字
    // </MM>
    redisSetProcTitle(argv[0]);
    // <MM>
    // 打印启动日志
    // </MM>
    redisAsciiArt();

    if (!server.sentinel_mode) {
        /* Things not needed when running in Sentinel mode. */
        redisLog(REDIS_WARNING,"Server started, Redis version " REDIS_VERSION);
    #ifdef __linux__
        linuxOvercommitMemoryWarning();
    #endif
        // <MM>
        // 从磁盘加载数据,rdb或aof
        // </MM>
        loadDataFromDisk();
        if (server.ipfd_count > 0)
            redisLog(REDIS_NOTICE,"The server is now ready to accept connections on port %d", server.port);
        if (server.sofd > 0)
            redisLog(REDIS_NOTICE,"The server is now ready to accept connections at %s", server.unixsocket);
    } else {
        sentinelIsRunning();
    }

    /* Warning the user about suspicious maxmemory setting. */
    if (server.maxmemory > 0 && server.maxmemory < 1024*1024) {
        redisLog(REDIS_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);
    }

initServerConfig中会对server的整型、字符串类型的属性设置,initServer主要对复合数据结构list、dict等初始化,并创建事件循环,初始化监听socket等。下面看下initServer函数:
首先,注册信号处理函数。

    int j;

    signal(SIGHUP, SIG_IGN);
    signal(SIGPIPE, SIG_IGN);
    setupSignalHandlers();

如果设置开启syslog,则初始化。

    if (server.syslog_enabled) {
        openlog(server.syslog_ident, LOG_PID | LOG_NDELAY | LOG_NOWAIT,
            server.syslog_facility);
    }

初始化客户端相关的数据结构。redis会将所有连接的客户端,slave以及monitor的客户端组织成链表,此处主要是创建这些链表。

    server.current_client = NULL;
    server.clients = listCreate();
    server.clients_to_close = listCreate();
    server.slaves = listCreate();
    server.monitors = listCreate();
    server.slaveseldb = -1; /* Force to emit the first SELECT command. */
    server.unblocked_clients = listCreate();
    server.ready_keys = listCreate();

redis中会对经常使用的对象,创建常量池,以避免频繁创建、回收的开销。这些对象包括经常使用的响应内容以及小于10000的整数,还有表示bulk个数的响应头以及bulk长度的响应头,这两个响应头只包括长度小于32的。

    // <MM>
    // 创建常量池,避免频繁创建
    // </MM>
    createSharedObjects();

调整打开描述符限制,需要调用getrlimit和setrlimit。

    // <MM>
    // 根据maxclients配置,调整打开文件描述符限制
    // </MM>
    adjustOpenFilesLimit();

初始化事件循环,具体过程在事件循环一节中讲述。然后根据配置创建db数组。

    // <MM>
    // 初始化event loop,如是epoll实现,会调用epoll_create创建epoll的fd
    // </MM>
    server.el = aeCreateEventLoop(server.maxclients+REDIS_EVENTLOOP_FDSET_INCR);
    server.db = zmalloc(sizeof(redisDb)*server.dbnum);

初始化监听socket,就是调用socket、bind、listen等,并将socket设置为非阻塞。如果配置了unix domain socket,也会进行相应的初始化。

    /* Open the TCP listening socket for the user commands. */
    if (server.port != 0 &&
        listenToPort(server.port,server.ipfd,&server.ipfd_count) == REDIS_ERR)
        exit(1);

    /* Open the listening Unix domain socket. */
    if (server.unixsocket != NULL) {
        unlink(server.unixsocket); /* don‘t care if this fails */
        server.sofd = anetUnixServer(server.neterr,server.unixsocket,
            server.unixsocketperm, server.tcp_backlog);
        if (server.sofd == ANET_ERR) {
            redisLog(REDIS_WARNING, "Opening socket: %s", server.neterr);
            exit(1);
        }
        anetNonBlock(NULL,server.sofd);
    }

    /* Abort if there are no listening sockets at all. */
    if (server.ipfd_count == 0 && server.sofd < 0) {
        redisLog(REDIS_WARNING, "Configured to not listen anywhere, exiting.");
        exit(1);
    }

接下来的代码,会初始化server.db数组中的每个db,主要创建相关的dict。然后关于pubsub、rdb、aof、replication相关数据结构的初始化。代码就不贴了。

然后添加定时器事件serverCron,在启动事件循环1ms后执行。redis中的事件循环就只有这一个,每0.1s(大约)执行一次,主要处理一些异步任务,比如清除expired keys,更新统计,check进行rdb、aof子进程的状态,然后会调用clientCron和databaseCron等。

    // <MM>
    // 注册serverCrom时间事件,启动event loop 1ms后执行
    // </MM>
    /* Create the serverCron() time event, that‘s our main way to process
     * background operations. */
    if(aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {
        redisPanic("Can‘t create the serverCron time event.");
        exit(1);
    }

将所有监听socket添加到事件循环。可以看到会有acceptTcpHandler处理客户端连接的建立。

    // <MM>
    // 为所有的监听socket添加file event,事件处理器是acceptTcpHandler用于处理连接的创建
    // </MM>
    /* Create an event handler for accepting new connections in TCP and Unix
     * domain sockets. */
    for (j = 0; j < server.ipfd_count; j++) {
        if (aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE,
            acceptTcpHandler,NULL) == AE_ERR)
            {
                redisPanic(
                    "Unrecoverable error creating server.ipfd file event.");
            }
    }
    if (server.sofd > 0 && aeCreateFileEvent(server.el,server.sofd,AE_READABLE,
        acceptUnixHandler,NULL) == AE_ERR) redisPanic("Unrecoverable error creating server.sofd file event.");

如果开启aof,则创建aof文件。

    /* Open the AOF file if needed. */
    if (server.aof_state == REDIS_AOF_ON) {
        server.aof_fd = open(server.aof_filename,
                               O_WRONLY|O_APPEND|O_CREAT,0644);
        if (server.aof_fd == -1) {
            redisLog(REDIS_WARNING, "Can‘t open the append-only file: %s",
                strerror(errno));
            exit(1);
        }
    }

最后部分,校验并设置maxmemory配置,初始化lua脚本、slow log以及latency monitor,最后的bioInit初始化异步io线程。redis是单线程,对于重IO操作,比如aof的fsync的调用会由异步线程调用,避免阻塞主线程的事件循环。以上就是initServer函数。

    /* 32 bit instances are limited to 4GB of address space, so if there is
     * no explicit limit in the user provided configuration we set a limit
     * at 3 GB using maxmemory with ‘noeviction‘ policy‘. This avoids
     * useless crashes of the Redis instance for out of memory. */
    if (server.arch_bits == 32 && server.maxmemory == 0) {
        redisLog(REDIS_WARNING,"Warning: 32 bit instance detected but no memory limit set. Setting 3 GB maxmemory limit with ‘noeviction‘ policy now.");
        server.maxmemory = 3072LL*(1024*1024); /* 3 GB */
        server.maxmemory_policy = REDIS_MAXMEMORY_NO_EVICTION;
    }

    replicationScriptCacheInit();
    scriptingInit();
    slowlogInit();
    latencyMonitorInit();
    // <MM>
    // 初始化异步阻塞IO处理线程
    // </MM>
    bioInit();

(4)main函数的最后,是启动事件循环。在事件循环的每次迭代,sleep之前会调用beforeSleep函数,进行一些异步处理。此处首先设置beforeSleep函数,然后启动aeMain事件循环。当从事件循环退出后,清理事件循环,然后退出。

    aeSetBeforeSleepProc(server.el,beforeSleep);
    // <MM>
    // 开启event loop
    // </MM>
    aeMain(server.el);
    aeDeleteEventLoop(server.el);
    return 0;

以上便是redis的初始化、启动过程。总体上,比较简单,相比nginx的配置文件解析,模块初始化,进程初始化要简单不少,读起来不会太费劲。下一篇介绍事件循环。

时间: 2024-10-24 00:04:09

redis源码分析(1)——初始化的相关文章

redis源码分析4---结构体---跳跃表

redis源码分析4---结构体---跳跃表 跳跃表是一种有序的数据结构,他通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的: 跳跃表支持平均O(logN),最坏O(N)复杂度的节点查找,还可以通过顺序性操作来批量处理节点.性能上和平衡树媲美,因为事先简单,常用来代替平衡树. 在redis中,只在两个地方使用了跳跃表,一个是实现有序集合键,另一个是在集群节点中用作内部数据结构. 1 跳跃表节点 1.1 层 层的数量越多,访问其他节点的速度越快: 1.2 前进指针 遍历举例

redis源码分析3---结构体---字典

redis源码分析3---结构体---字典 字典,简单来说就是一种用于保存键值对的抽象数据结构: 注意,字典中每个键都是独一无二的:在redis中,内部的redis的数据库就是使用字典作为底层实现的: 1 字典的实现 在redis中,字典是使用哈希表作为底层实现的,一个hash表里面可以有多个hash表节点,而每个hash表节点就保存了字典中的一个键值对: hash表定义 table属性是一个数组,数组中的每个元素都是一个指向dictEntry结构的指针,每个dictEntry结构保存着一个键值

redis源码分析之事务Transaction(下)

接着上一篇,这篇文章分析一下redis事务操作中multi,exec,discard三个核心命令. 原文地址:http://www.jianshu.com/p/e22615586595 看本篇文章前需要先对上面文章有所了解: redis源码分析之事务Transaction(上) 一.redis事务核心命令简介 redis事务操作核心命令: //用于开启事务 {"multi",multiCommand,1,"sF",0,NULL,0,0,0,0,0}, //用来执行事

redis 源码分析(一) 内存管理

一,redis内存管理介绍 redis是一个基于内存的key-value的数据库,其内存管理是非常重要的,为了屏蔽不同平台之间的差异,以及统计内存占用量等,redis对内存分配函数进行了一层封装,程序中统一使用zmalloc,zfree一系列函数,其对应的源码在src/zmalloc.h和src/zmalloc.c两个文件中,源码点这里. 二,redis内存管理源码分析 redis封装是为了屏蔽底层平台的差异,同时方便自己实现相关的函数,我们可以通过src/zmalloc.h 文件中的相关宏定义

redis源码分析之内存布局

redis源码分析之内存布局 1. 介绍 众所周知,redis是一个开源.短小.高效的key-value存储系统,相对于memcached,redis能够支持更加丰富的数据结构,包括: 字符串(string) 哈希表(map) 列表(list) 集合(set) 有序集(zset) 主流的key-value存储系统,都是在系统内部维护一个hash表,因为对hash表的操作时间复杂度为O(1).如果数据增加以后,导致冲突严重,时间复杂度增加,则可以对hash表进行rehash,以此来保证操作的常量时

Redis源码分析(四)-- sds字符串

今天分析的是Redis源码中的字符串操作类的代码实现.有了上几次的分析经验,渐渐觉得我得换一种分析的方法,如果每个API都进行代码分析,有些功能性的重复,导致分析效率的偏低.所以下面我觉得对于代码的分析偏重的是一种功能整体的思维实现来讲解,其中我也会挑出一个比较有特点的方法进行拆分了解,这也可以让我们见识一下里面的一些神奇的代码.好,回归正题,说到字符串,这不管放到哪个编程语言中,都是使用频率极高的操作类.什么new String, concat, strcopy,substr, splitSt

redis源码分析之事务Transaction(上)

这周学习了一下redis事务功能的实现原理,本来是想用一篇文章进行总结的,写完以后发现这块内容比较多,而且多个命令之间又互相依赖,放在一篇文章里一方面篇幅会比较大,另一方面文章组织结构会比较乱,不容易阅读.因此把事务这个模块整理成上下两篇文章进行总结. 原文地址:http://www.jianshu.com/p/acb97d620ad7 这篇文章我们重点分析一下redis事务命令中的两个辅助命令:watch跟unwatch. 一.redis事务辅助命令简介 依然从server.c文件的命令表中找

MyBatis源码分析-MyBatis初始化流程

MyBatis 是支持定制化 SQL.存储过程以及高级映射的优秀的持久层框架.MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集.MyBatis 可以对配置和原生Map使用简单的 XML 或注解,将接口和 Java 的 POJOs(Plain Old Java Objects,普通的 Java对象)映射成数据库中的记录.如何新建MyBatis源码工程请点击MyBatis源码分析-IDEA新建MyBatis源码工程. MyBatis初始化的过程也就是创建Configura

Redis源码分析(一)--Redis结构解析

从今天起,本人将会展开对Redis源码的学习,Redis的代码规模比较小,非常适合学习,是一份非常不错的学习资料,数了一下大概100个文件左右的样子,用的是C语言写的.希望最终能把他啃完吧,C语言好久不用,快忘光了.分析源码的第一步,先别急着想着从哪开始看起,先浏览一下源码结构,可以模块式的渐入,不过比较坑爹的是,Redis的源码全部放在在里面的src目录里,一下90多个文件统统在里面了,所以我选择了拆分,按功能拆分,有些文件你看名字就知道那是干什么的.我拆分好后的而结果如下: 11个包,这样每

redis源码分析(1)--makefile和目录结构分析

一.redis源码编译 redis可以直接在官网下载(本文使用版本 3.0.7):https://redis.io/download 安装: $ tar xzf redis-3.0.7.tar.gz $ cd redis-3.0.7 $ make make执行以后主要编译产物在src/redis-server src/redis-cli 如果想把redis-server直接install到可执行目录/usr/local/bin,还需要执行: $ make install Run Redis wi