malloc 源码_Redis远古源码阅读

Redis源码阅读

版本tag:1.3.6

2010年的版本了。

如有错误,感谢指正!

如有建议和意见,欢迎提出!

从main开始

第一次看源码,所以先从最简单的版本看起,找了很久,没有1.0.0版本的,就找了个看似是最早的版本了。

跟着main函数一点点看吧,看见感兴趣的函数就查一下,尽量查看官方文档,不依靠csdn和百度!

int main(int argc, char **argv) {
    time_t start;
    //0. 初始化Server配置参数,可能会被2覆盖
    initServerConfig();
    if (argc == 2) {
        //1. 重置自动保存的参数,暂时先不管
        resetServerSaveParams();  
        //2. 如果启动的时候传入了配置文件,则依照配置文件初始化Server的配置
        loadServerConfig(argv[1]); 
    } else if (argc > 2) {
        fprintf(stderr,"Usage: ./redis-server [/path/to/redis.conf]n");
        exit(1);
    } else {
        redisLog(REDIS_WARNING,"Warning: no config file specified, using the default config. In order to specify a config file use 'redis-server /path/to/redis.conf'");
    }
    // 3. daemon化,变成一个后台进程,否则断开会话时,进程也会被终止
    if (server.daemonize) daemonize(); 
    // 4. 初始化服务
    initServer();
    redisLog(REDIS_NOTICE,"Server started, Redis version " REDIS_VERSION);
#ifdef __linux__
    linuxOvercommitMemoryWarning();
#endif
    start = time(NULL);
    if (server.appendonly) {
        if (loadAppendOnlyFile(server.appendfilename) == REDIS_OK)
            redisLog(REDIS_NOTICE,"DB loaded from append only file: %ld seconds",time(NULL)-start);
    } else {
        if (rdbLoad(server.dbfilename) == REDIS_OK)
            redisLog(REDIS_NOTICE,"DB loaded from disk: %ld seconds",time(NULL)-start);
    }
    redisLog(REDIS_NOTICE,"The server is now ready to accept connections on port %d", server.port);
    aeSetBeforeSleepProc(server.el,beforeSleep);
    // 进入事件循环
    aeMain(server.el);
    aeDeleteEventLoop(server.el);
    return 0;
}

daemonize

daemon守护进程,像很多服务都可以看见这个意思,比如mysqld、dockerd等,就是一个后台进程,比如我们ssh到一台linux服务器,然后启动./redis-server,然后断开ssh连接,此时,redis-server进程会收到一个SIGHUP(hang up挂断)的信号,也会关闭。

而我们实际想要的结果是让这个server成为一个后台进程。这就是daemonize函数的作用

如何daemon化呢,可以参考一下,Redis的Daemonize没有:

  • How to daemonize a process:代码层面如何实现以及一些关于Terminal、Process的背景知识
  • How to keep redis server running:Redis如何跑在后台

代码实现涉及到的API

  • setsid:简单来说就是创建一个新的session,调用进程成为了该session的leader(如果它不是一个leader),且这个session没有对应的控制终端(controling terminal)。
    • 当然你也可以申请一个open /dev/tty,所以防止出现这种情况,上面提到的资料中还推荐再fork一次,这样fork出来的子进程就不是session leader,也不能申请控制终端了。
      • 当然fork以后还可以再setid,所以break这个递归的前提就是不要瞎搞!
        • ……
  • dup2:dup2的意思就是dup后面跟2个参数,用来复用文件描述符,代码中打开了一个特殊的文件/dev/null,把STDINSTDOUTSTDERR都重定向到这个无底洞中。
static void daemonize(void) {
    int fd;
    FILE *fp;
    // 退出父进程,使得子进程变成孤儿进程,最后变成init的子进程
    if (fork() != 0) exit(0); /* parent exits */
    // 创建一个新的session,没有controling terminal与之对应
    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. */
    // 所有的输出都重定向至无底洞/dev/null中
    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);
    }
    /* Try to write the pid file */
    // 将pid写入到/var/run/redis.pid这个文件中
    fp = fopen(server.pidfile,"w");
    if (fp) {
        fprintf(fp,"%dn",getpid());
        fclose(fp);
    }
}

initServer

此处的initServer不一样了,主要是初始化一些运行时需要的容器(list)、事件循环eventLoop。

而之前的initServerConfigloadServerConfig,是设置默认参数值和载入参数值。

初始化结构体redisServer里的一些成员变量,挑几个感兴趣的

  • list *objfreelist:A list of freed objects to avoid malloc(),需要被回收的,可以拿来复用的obj,避免重复的malloc和free。
  • aeEventLoop *elaeEventLoop是事件循环的结构体,在initServer里创建,然后通过aeMain(server.el);开始事件循环。事件循环相关的代码都以ae开头,与操作系统也有关联,不同的操作系统使用的网络模型也不同,官方文档中提到的是epoll,但是我使用Windows+vscode看代码时,ctrl+左键点击相关函数,发现使用的是select。因为在ae.c这个文件中有如下宏定义
#ifdef HAVE_EPOLL
#include "ae_epoll.c"
#else
    #ifdef HAVE_KQUEUE
    #include "ae_kqueue.c"
    #else
    #include "ae_select.c"
    #endif
#endif
static void initServer() {
    int j;
​
    signal(SIGHUP, SIG_IGN);
    signal(SIGPIPE, SIG_IGN);
    setupSigSegvAction();
​
    server.devnull = fopen("/dev/null","w");
    if (server.devnull == NULL) {
        redisLog(REDIS_WARNING, "Can't open /dev/null: %s", server.neterr);
        exit(1);
    }
    // 初始化结构体redisServer的成员变量
    server.clients = listCreate(); // 连接的客户端列表
    server.slaves = listCreate();  // slave列表
    server.monitors = listCreate();// TODO:
    server.objfreelist = listCreate(); // 需要被回收的,可以拿来复用的obj,避免重复的malloc
    createSharedObjects();
    server.el = aeCreateEventLoop(); // 创建事件循环
    server.db = zmalloc(sizeof(redisDb)*server.dbnum);
    server.sharingpool = dictCreate(&setDictType,NULL);
    server.fd = anetTcpServer(server.neterr, server.port, server.bindaddr);
    if (server.fd == -1) {
        redisLog(REDIS_WARNING, "Opening TCP port: %s", server.neterr);
        exit(1);
    }
    for (j = 0; j < server.dbnum; j++) {
        server.db[j].dict = dictCreate(&dbDictType,NULL);
        server.db[j].expires = dictCreate(&keyptrDictType,NULL);
        server.db[j].blockingkeys = dictCreate(&keylistDictType,NULL);
        if (server.vm_enabled)
            server.db[j].io_keys = dictCreate(&keylistDictType,NULL);
        server.db[j].id = j;
    }
    server.cronloops = 0;
    server.bgsavechildpid = -1;
    server.bgrewritechildpid = -1;
    server.bgrewritebuf = sdsempty();
    server.lastsave = time(NULL);
    server.dirty = 0;
    server.stat_numcommands = 0;
    server.stat_numconnections = 0;
    server.stat_starttime = time(NULL);
    server.unixtime = time(NULL);
    aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL);
    if (aeCreateFileEvent(server.el, server.fd, AE_READABLE,
        acceptHandler, NULL) == AE_ERR) oom("creating file event");
​
    if (server.appendonly) {
        server.appendfd = open(server.appendfilename,O_WRONLY|O_APPEND|O_CREAT,0644);
        if (server.appendfd == -1) {
            redisLog(REDIS_WARNING, "Can't open the append-only file: %s",
                strerror(errno));
            exit(1);
        }
    }
​
    if (server.vm_enabled) vmInit();
}

zmalloc

redis源码封装了内存的动态分配函数,提供了一系列z开头的内存管理函数

zmalloc:每次分配堆空间时会额外在头部分配一个size_t(win64下是8个字节)的空间,以记录该指针对应的堆空间大小;同时,更新全局变量used_memory,记录下本次分配的空间
void *zmalloc(size_t size) {
 
 // #define PREFIX_SIZE sizeof(size_t)
 // 额外分配一个prefix,用于记录buff的大小
 void *ptr = malloc(size+PREFIX_SIZE);
​
 if (!ptr) zmalloc_oom(size);
#ifdef HAVE_MALLOC_SIZE // MacOS下才有
 increment_used_memory(redis_malloc_size(ptr));
 return ptr;
#else
 // 在prefix中写入buff的字节数(不包括prefix)
 *((size_t*)ptr) = size;
 // 更新已分配的字节数(包括prefix)
 increment_used_memory(size+PREFIX_SIZE);
 
 // 最后返回的是实际buff的起始指针
 return (char*)ptr+PREFIX_SIZE;
#endif
}

SDS

Hacking Strings

sds:sds stands for Simple Dynamic Strings,但是实际类型是char *

typedef char *sds;
​
struct sdshdr {
    long len;
    long free;
    char buf[]; // 注意!这里是char[],和C++里好像不太一样哦!
};

起初我比较奇怪,为什么用[],然后搜了一下这是一个C里的一个写法,叫做flexible array member

SO上关于这个写法的讨论:What are the real benefits of flexible array member?

struct h1 {
    size_t len;
    char* data;
};
​
struct h2 {
    size_t len;
    char data[];
};
size_t payloadLen = 1024;
​
// init h1
h1 *instance1 = (h1 *)malloc(sizeof(h1));
instance1->data = (char *)malloc(payloadLen);
instance1->len = payloadLen;
// free 
free(instance1->data);
free(instance1);
    
// init h2
h2 *instance2 = (h2 *)malloc(sizeof(h1)+payloadLen);
instance2->len = payloadLen;
// free
free(instance2);
​

相比之下,h2的操作更加简洁。

d0b4c1aac8ef3400c4bb708a7a92acc2.png

有了上面的基础,再看下面这个sdsnewlen的代码,就稍微看得懂一些了。

sds sdsnewlen(const void *init, size_t initlen) {
    struct sdshdr *sh;
    // 申请内存
    sh = zmalloc(sizeof(struct sdshdr)+initlen+1);
#ifdef SDS_ABORT_ON_OOM
    if (sh == NULL) sdsOomAbort();
#else
    if (sh == NULL) return NULL;
#endif
    // 记录长度
    sh->len = initlen;
    sh->free = 0;
    if (initlen) {
        // 拷贝data
        if (init) memcpy(sh->buf, init, initlen);
        else memset(sh->buf,0,initlen);
    }
    // buff的末尾置'0'
    sh->buf[initlen] = '0';
    // 返回buff的指针,也就是实际字符串的起始位置
    return (char*)sh->buf;
}   

官方给出的对于sds的说明文档:Hacking Strings

这样的结构在获取字符串长度时,反推就能获取len,时间复杂度是O(1)

sdsnewlen("redis", 5);
/*
-----------
|5|0|redis|
-----------
^   ^
sh  sh->buf
*/
​
size_t sdslen(const sds s) {
    //根据buff的位置,反推sdshdr的指针,从而获取到len,就不需要strlen了!
    struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr)));
    return sh->len;
}

总结一下:

  • zmalloc主要是为了记录当前程序分配的所有堆空间之和,所以对malloc进行了一次封装,添加上了一个prefix。
  • sdshdr为了记录字符串buff的长度和空闲空间大小,所以加了Len和Free两个Long类型的头。

感觉有点像TCP/IP协议栈,就是一个对象,不同层次,封装了好多个头,不同层解决不同的问题。

44dc0806fddf74284bf6a396c315ea6e.png

参考资料

  • Data Type Ranges
  • Dynamic Strings in C
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值