redis 源码阅读笔记

十一放假在家,没有更新博客,现在忙里偷闲,记录一下这几天源码的总结,redis的源码还是比较容易理解的,至少目前是,它有自己的lis实现和字符串的管理办法,也有自己的内存管理策略。现在就从这三部分一一总结一下。

内存管理:

内存管理源码只有zmalloc.h 和zmalloc,c这两个文件。主要函数是通过zmalloc来申请和zfree来释放。zmalloc的时候,他会额外的申请PREFIX_SIZE个空间。这是个long long的大小,为的是存储申请内存的大小,其实和malloc的现实也是一样的。malloc也有头部对吧。然后它将除去头部大小的实际内存返回给用户。

而calloc 将内存初始化为1而不是0,这个倒是挺奇怪的。zrealloc函数是首选判断之前的内存是不是NULL的,假如是NULL的直接调用zmalloc就好了,否则的话,在得到之前的真实内存起始地址,然后冲洗realloc,这个的现实其实和系统的realloc差不多。然后这个文件中有一些统计的信息,是用一个宏来实现的,我们自己做的项目其实也是用宏来实现的,宏的好处就是减少函数调用开销,有一个技巧,为了防止宏受到别的语句的影响,宏使用了do while方式,while的条件是0,也就是只执行一次。

然后是计算一下系统的信息,比如RSS。redis一个优点就是跨平台性,所以定义了不同系统得到系统信息的方式,比如rss,有的系统通过stat文件得到,有的系统通过stask结构体得到,有的系统没有,那么就直接使用统计信息。而对于得到系统总的内存大小,更是针对不同的系统进行了计算。内存管理这部分的一个额外收获是原来平台对malloc的真实实现有不同的版本,有的是用的是jemalloc有的使用的tcmalloc。


list的实现是通过双向链表来实现的。这个本身就是一段优美的代码。里面定义了复制和释放的函数指针,用来方便深度复制和拷贝吧.还纪录了list的长度大小插入部分 分别定义 了从头部插入,从尾部插入,然后是从中间插入。从任意插入,从任意节点插入又分为从节点的前面插入和后面插入,是通过after这个参数区分的。另一个需要提到的是list还设置了迭代器的概念,基于next指针实现的。

第三部分就是最有意思的字符串部分了,这部分封装了char*这个基本的数据结构,和内存管理一样,设置了一个头部信息,用来存放字符的长度,以及分配的空间大小。而根据不同的字符大小,有不同的头部大小,这样可以更加有效的使用内存。而且为了内存更加轻便,使用了pack关键字,就是强制1字节对齐,而不是默认的对齐方式,这样可以更加节省内存。这部分具体如下:

这部分学到了一个比较好玩的语法结构,就是attribute关键字的使用还有一个宏的使用方法:

struct __attribute__ ((__packed__)) sdshdr5 {                                           
    unsigned char flags; /* 3 lsb of type, and 5 msb of string length */                 
    char buf[];                                                                          
};

还有一个##连接的使用

#define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T)));
使用## 作为拼接的语法。

sds这部分是自己封装了char*,为了更好的进行字符的改变,扩展,修改等。它借鉴了vector的内存管理方式。使用sdsMakeRoomFor进行字符串的扩张。扩展方式是每次加入还有做够的空闲区域的话就直接返回。否则检查需要扩展的大小是否超过一个系统默认值,假如没有吵过,那么就2倍扩张,否则的话,就扩展系统默认值大小。这样既能够有效的扩展大小,又不至于扩展的特别严重。代码如下:

sds sdsMakeRoomFor(sds s, size_t addlen) {
    void *sh, *newsh;
    size_t avail = sdsavail(s);
    size_t len, newlen;
    char type, oldtype = s[-1] & SDS_TYPE_MASK;
    int hdrlen;

    /* Return ASAP if there is enough space left. */
    if (avail >= addlen) return s;

    len = sdslen(s);
    sh = (char*)s-sdsHdrSize(oldtype);
    newlen = (len+addlen);
    if (newlen < SDS_MAX_PREALLOC)
        newlen *= 2;
    else
        newlen += SDS_MAX_PREALLOC;

    type = sdsReqType(newlen);

    /* Don't use type 5: the user is appending to the string and type 5 is
     * not able to remember empty space, so sdsMakeRoomFor() must be called
     * at every appending operation. */
    if (type == SDS_TYPE_5) type = SDS_TYPE_8;

    hdrlen = sdsHdrSize(type);
    if (oldtype==type) {
        newsh = s_realloc(sh, hdrlen+newlen+1);
        if (newsh == NULL) return NULL;
        s = (char*)newsh+hdrlen;
    } else {
        /* Since the header size changes, need to move the string forward,
         * and can't use realloc */
        newsh = s_malloc(hdrlen+newlen+1);
        if (newsh == NULL) return NULL;
        memcpy((char*)newsh+hdrlen, s, len+1);
        s_free(sh);
        s = (char*)newsh+hdrlen;
        s[-1] = type;
        sdssetlen(s, len);
    }
    sdssetalloc(s, newline);
    return s;
}

这时候要注意,可能要重现设置一下头部的大小。

因为是在char*的基础上封装的,所以很多信息都需要手动的去设置,在你改变了一个字符串的大小的时候,需要去重新设置字符的长度大小。

我觉得里面有几个函数写的很不错,拿出来分享一下:

一个是 格式化写入的函数,里面对于堆栈上数组的使用比较有意思:

/* Like sdscatprintf() but gets va_list instead of being variadic. */
sds sdscatvprintf(sds s, const char *fmt, va_list ap) {
    va_list cpy;
    char staticbuf[1024], *buf = staticbuf, *t;
    size_t buflen = strlen(fmt)*2;

    /* We try to start using a static buffer for speed.
     * If not possible we revert to heap allocation. */
    if (buflen > sizeof(staticbuf)) {
        buf = s_malloc(buflen);
        if (buf == NULL) return NULL;
    } else {
        buflen = sizeof(staticbuf);
    }

    /* Try with buffers two times bigger every time we fail to
     * fit the string in the current buffer size. */
    while(1) {
        buf[buflen-2] = '\0';
        va_copy(cpy,ap);
        vsnprintf(buf, buflen, fmt, cpy);
        va_end(cpy);
        if (buf[buflen-2] != '\0') {
            if (buf != staticbuf) s_free(buf);
            buflen *= 2;
            buf = s_malloc(buflen);
            if (buf == NULL) return NULL;
            continue;
        }
        break;
    }

    /* Finally concat the obtained string to the SDS string and return it. */
    t = sdscat(s, buf);
    if (buf != staticbuf) s_free(buf);
    return t;
}

还有一个函数
sds sdscatfmt(sds s, char const *fmt, ...) 

感觉写的有点问题,就是没有控制一些特殊情况,就是边界问题,不知道你们是怎么想的。
实现里的trim函数写的非常简练,标记学习下:

sds sdstrim(sds s, const char *cset) {
    char *start, *end, *sp, *ep;
    size_t len;

    sp = start = s;
    ep = end = s+sdslen(s)-1;
    while(sp <= end && strchr(cset, *sp)) sp++;
    while(ep > sp && strchr(cset, *ep)) ep--;
    len = (sp > ep) ? 0 : ((ep-sp)+1);
    if (s != sp) memmove(s, sp, len);
    s[len] = '\0';
    sdssetlen(s,len);
    return s;
}

split函数写的有点技巧,动态的增加splot大小:

sds *sdssplitlen(const char *s, int len, const char *sep, int seplen, int *count) {
    int elements = 0, slots = 5, start = 0, j;
    sds *tokens;

    if (seplen < 1 || len < 0) return NULL;

    tokens = s_malloc(sizeof(sds)*slots);
    if (tokens == NULL) return NULL;

    if (len == 0) {
        *count = 0;
        return tokens;
    }
    for (j = 0; j < (len-(seplen-1)); j++) {
        /* make sure there is room for the next element and the final one */
        if (slots < elements+2) {
            sds *newtokens;

            slots *= 2;
            newtokens = s_realloc(tokens,sizeof(sds)*slots);
            if (newtokens == NULL) goto cleanup;
            tokens = newtokens;
        }
        /* search the separator */
        if ((seplen == 1 && *(s+j) == sep[0]) || (memcmp(s+j,sep,seplen) == 0)) {
            tokens[elements] = sdsnewlen(s+start,j-start);
            if (tokens[elements] == NULL) goto cleanup;
            elements++;
            start = j+seplen;
            j = j+seplen-1; /* skip the separator */
        }
    }
    /* Add the final element. We are sure there is room in the tokens array. */
    tokens[elements] = sdsnewlen(s+start,len-start);
    if (tokens[elements] == NULL) goto cleanup;
    elements++;
    *count = elements;
    return tokens;

cleanup:
    {
        int i;
        for (i = 0; i < elements; i++) sdsfree(tokens[i]);
        s_free(tokens);
        *count = 0;
        return NULL;
    }
}

这个模块自己写了一个测试的函数,有两点需要学习的:

1. 测试的使用使用 宏来做,自动进行测试,可以显示哪些测试用例通过了,哪些没有通过,我之前使用assert做的。

2.写完一个模块,一定要给这个模块写一些测试用例,阿里和百度都挺重视这个问题的。


你可能感兴趣的:(redis 源码阅读笔记)