redis字符串源码分析sds

前言

分析的为redis现在的最新版 6.2.3

源码链接:

sds.h: https://github.com/redis/redis/blob/unstable/src/sds.h

sds.c: https://github.com/redis/redis/blob/unstable/src/sds.c

sds结构体的定义

// sds的定义
typedef char *sds;

/* Note: sdshdr5 is never used, we just access the flags byte directly.
 * However is here to document the layout of type 5 SDS strings. */
// 不会被用到
struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; // 字符串长度,buf已经用过的长度
    uint8_t alloc; // 字符串的总容量
    unsigned char flags; // 第三位保存类型标志
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; /* used */
    uint16_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len; /* used */
    uint32_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; /* used */
    uint64_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};

我们看到sds的类型定义:

typedef char *sds;

所以这里为什么sds竟然等同于 char*?sds和传统的c语言字符串保持类型兼容,因此他们的定义是一样的,都是char*。这有些情况下,需要传入一个C语言字符串的地方,也确实可以传入一个sds。但是,sds和 char * 并不等同。sds是Binary Safe的,它可以存储任意二进制数据,不像C语言字符串那样以字符\0来标识字符串的结束,因此它必然有个长度字段。但是这个字段在哪?实际上还有一个header结构:

sds结构体从4.0开始就使用了5种header定义,节省内存的使用,但是不会用到sdshdr5。之所以有五种类型,就是为了节省内存:

  • sds5 对应1<<5 32
  • sds8 对应 1<<8 256
  • sds16 对应 1<<16 65536
  • sds32 对应 1<<32 2^32
  • sds64 对应 1<<16 2^64

一个sds字符串的完整结构,由在内存地址上前后相邻的两部分组成:

  • 一个header。通常包含字符串的长度(len), 最大容量(alloc)和flags。sdshdr5不同
  • 一个字符数组。这个字符数组的长度等于最大容量+1。真正有效的字符串数据,长度通常小于最大容量。
#define SDS_TYPE_5  0
#define SDS_TYPE_8  1
#define SDS_TYPE_16 2
#define SDS_TYPE_32 3
#define SDS_TYPE_64 4

sds内部结构解析

sds的数据结构,我们非常有必要非常仔细的去解析它

在这里插入图片描述

上图是一个内部的结构例子,画的很丑😣大致意思应该能明白 ✌️

这里的__attribute__ ((__packed__))是告诉编译分配紧凑的内存,而不是字节对齐的方式。因为内存分配的时候,是按字节对齐的,一般都是按一个指针大小为单位进行分配的。比如一个char也会分配一个指针大小的内存。

  • len就是字符串的长度
  • alloc字符串的总容量
  • flags字符串的类型标记 具体的类型有五种:SDS_TYPE_5, SDS_TYPE_8, SDS_TYPE_16, SDS_TYPE_32, SDS_TYPE_64。
  • buf[]就是柔性数组。在分配内存的时候会指向字符串的内容

sds结构体的获取

#define SDS_TYPE_MASK 7
#define SDS_TYPE_BITS 3
#define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T)));
#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))))
#define SDS_TYPE_5_LEN(f) ((f)>>SDS_TYPE_BITS)

要使用sds结构体,上面可以看到5种结构体的定义,在使用的时候是通过一个宏来获取的:

#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))))

##被称为连接符,它是一种预处理运算符,用来把两个语句符号(Token)组合成单个语句符号。比如SDS_HDR(10, s),根据宏定义展开是:

((struct sdshdr10 *)((s)-(sizeof(struct sdshdr10))))

具体使用那一种结构体,sds底层是通过flags属性与SDS_TYPE_MASK做与运算得出具体的类型(具体实现看下面的sdslen函数),然后根据类型获取具体的结构体

有了头地址,我们就能很轻松的通过计算偏移多少位找到所在的len和alloc了。

内存是紧凑分配的。所以我们取到字符串的内容的时候,通过指针后退一位sds[-1] 就可以得到字符串的类型。同时SDS_TYPE_MASK通过和flags进行&操作得到具体类型。所以有了这段代码:

static inline size_t sdslen(const sds s) {
    unsigned char flags = s[-1];
    switch(flags&SDS_TYPE_MASK) {
        case SDS_TYPE_5:
            return SDS_TYPE_5_LEN(flags);
        case SDS_TYPE_8:
            return SDS_HDR(8,s)->len;
        case SDS_TYPE_16:
            return SDS_HDR(16,s)->len;
        case SDS_TYPE_32:
            return SDS_HDR(32,s)->len;
        case SDS_TYPE_64:
            return SDS_HDR(64,s)->len;
    }
    return 0;
}

这里的SDS_TYPE_5LEN(flags)获取长度可以这样理解。因为前面定义sdshdr5的时候注释上面说了:/* 3 lsb of type, and 5 msb of string length */所以这里我们只要做一个左移三位的操作就能得到长度了。

sds的一些基础函数

  • sdslen(const sds s) 获取sds字符串的长度
  • sdssetlen(sds s, size_t newlen) 设置sds字符串长度
  • sdsinclen(sds s, size_t inc) 增加sds字符串长度
  • sdsalloc(const sds s) 获取sds字符串容量
  • sdssetalloc(sds s, size_t newlen) 设置sds字符串容量
  • sdsavail(const sds s) 获取sds字符串空余空间
  • sdsHdrSize(char type) 根据header类型得到header大小
  • sdsReqType(size_t string_size) 根据字符串数据长度计算所需要的header类型

sdslen

获取sds字符串的长度

static inline size_t sdslen(const sds s) {
    unsigned char flags = s[-1];
    switch(flags&SDS_TYPE_MASK) {
        case SDS_TYPE_5:
            return SDS_TYPE_5_LEN(flags);
        case SDS_TYPE_8:
            return SDS_HDR(8,s)->len;
        case SDS_TYPE_16:
            return SDS_HDR(16,s)->len;
        case SDS_TYPE_32:
            return SDS_HDR(32,s)->len;
        case SDS_TYPE_64:
            return SDS_HDR(64,s)->len;
    }
    return 0;
}

和前面分析的一样,sdslen先用s[-1]向低地址方向偏移一字节,得到flags;然后与SDS_TYPE_MASK进行按位与,得到header类型;然后根据不同的header类型,调用SDS_HDR得到header起始指针,进而获得len字段。

sdsReqType

根据字符串数据长度计算所需要的header类型

static inline char sdsReqType(size_t string_size) {
    if (string_size < 1<<5)
        return SDS_TYPE_5;
    if (string_size < 1<<8)
        return SDS_TYPE_8;
    if (string_size < 1<<16)
        return SDS_TYPE_16;
#if (LONG_MAX == LLONG_MAX)
    if (string_size < 1ll<<32)
        return SDS_TYPE_32;
    return SDS_TYPE_64;
#else
    return SDS_TYPE_32;
#endif
}

获取了len字段后,通过sdsReqType的代码可以看出

  • 长度在0 ~ pow(2, 5) - 1之间,选用SDS_TYPE_5类型的header
  • 长度在pow(2, 5) - 1 ~ pow(2, 8) - 1,选用SDS_TYPE_8类型的header

sdsnewlen

sds的创建和销毁

sds _sdsnewlen(const void *init, size_t initlen, int trymalloc) {
    void *sh;
    sds s;
    char type = sdsReqType(initlen);
    /* Empty strings are usually created in order to append. Use type 8
     * since type 5 is not good at this. */
    if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
    int hdrlen = sdsHdrSize(type);
    unsigned char *fp; /* flags pointer. */
    size_t usable;

    assert(initlen + hdrlen + 1 > initlen); /* Catch size_t overflow */
    sh = trymalloc?
        s_trymalloc_usable(hdrlen+initlen+1, &usable) :
        s_malloc_usable(hdrlen+initlen+1, &usable);
    if (sh == NULL) return NULL;
    if (init==SDS_NOINIT)
        init = NULL;
    else if (!init)
        memset(sh, 0, hdrlen+initlen+1);
    s = (char*)sh+hdrlen;
    fp = ((unsigned char*)s)-1;
    usable = usable-hdrlen-1;
    if (usable > sdsTypeMaxSize(type))
        usable = sdsTypeMaxSize(type);
    switch(type) {
        case SDS_TYPE_5: {
            *fp = type | (initlen << SDS_TYPE_BITS);
            break;
        }
        case SDS_TYPE_8: {
            SDS_HDR_VAR(8,s);
            sh->len = initlen;
            sh->alloc = usable;
            *fp = type;
            break;
        }
        case SDS_TYPE_16: {
            SDS_HDR_VAR(16,s);
            sh->len = initlen;
            sh->alloc = usable;
            *fp = type;
            break;
        }
        case SDS_TYPE_32: {
            SDS_HDR_VAR(32,s);
            sh->len = initlen;
            sh->alloc = usable;
            *fp = type;
            break;
        }
        case SDS_TYPE_64: {
            SDS_HDR_VAR(64,s);
            sh->len = initlen;
            sh->alloc = usable;
            *fp = type;
            break;
        }
    }
    if (initlen && init)
        memcpy(s, init, initlen);
    s[initlen] = '\0';
    return s;
}

sds sdsnewlen(const void *init, size_t initlen) {
    return _sdsnewlen(init, initlen, 0);
}

sds sdstrynewlen(const void *init, size_t initlen) {
    return _sdsnewlen(init, initlen, 1);
}

/* Create an empty (zero length) sds string. Even in this case the string
 * always has an implicit null term. */
sds sdsempty(void) {
    return sdsnewlen("",0);
}

/* Create a new sds string starting from a null terminated C string. */
sds sdsnew(const char *init) {
    size_t initlen = (init == NULL) ? 0 : strlen(init);
    return sdsnewlen(init, initlen);
}

/* Duplicate an sds string. */
sds sdsdup(const sds s) {
    return sdsnewlen(s, sdslen(s));
}

/* Free an sds string. No operation is performed if 's' is NULL. */
void sdsfree(sds s) {
    if (s == NULL) return;
    s_free((char*)s-sdsHdrSize(s[-1]));
}

sdsnewlen创建一个长度为initlen的sds字符串,并使用init指向的字符数组(任意二进制数据)来初始化数据。如果init为NULL,那么使用全0来初始化数据。

要注意的是:如果创建一个长度为0的空字符串,那么不使用SDS_TYPE_5类型的header,而是使用SDS_TYPE_8类型的header。这是因为创建的空字符串一般接下来的才做很可能是追加数据,但是SDS_TYPE_5类型不适合追加数据。

这里的内存是一次性分配的,sh = trymalloc ? s_trymalloc_usable(hdrlen+initlen+1, &usable) : s_malloc_usable(hdrlen+initlen+1, &usable); 这里的参数trymalloc是表示调用s_trymalloc_usable还是s_malloc_usable

关于sdsfree,注意的是:内存要整体释放,所以要先计算出header起始指针,把它传给s_free函数。这个指针也正是在sdsnewlen中调用s_malloc返回的那个地址。

sdscat

连接追加操作

/* Append the specified binary-safe string pointed by 't' of 'len' bytes to the
 * end of the specified sds string 's'.
 *
 * After the call, the passed sds string is no longer valid and all the
 * references must be substituted with the new pointer returned by the call. */
sds sdscatlen(sds s, const void *t, size_t len) {
    size_t curlen = sdslen(s);

    s = sdsMakeRoomFor(s,len);
    if (s == NULL) return NULL;
    memcpy(s+curlen, t, len);
    sdssetlen(s, curlen+len);
    s[curlen+len] = '\0';
    return s;
}

/* Append the specified null terminated C string to the sds string 's'.
 *
 * After the call, the passed sds string is no longer valid and all the
 * references must be substituted with the new pointer returned by the call. */
sds sdscat(sds s, const char *t) {
    return sdscatlen(s, t, strlen(t));
}

/* Append the specified sds 't' to the existing sds 's'.
 *
 * After the call, the modified sds string is no longer valid and all the
 * references must be substituted with the new pointer returned by the call. */
sds sdscatsds(sds s, const sds t) {
    return sdscatlen(s, t, sdslen(t));
}

/* Enlarge the free space at the end of the sds string so that the caller
 * is sure that after calling this function can overwrite up to addlen
 * bytes after the end of the string, plus one more byte for nul term.
 *
 * Note: this does not change the *length* of the sds string as returned
 * by sdslen(), but only the free buffer space we have. */
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;
    size_t usable;

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

    len = sdslen(s);
    sh = (char*)s-sdsHdrSize(oldtype);
    newlen = (len+addlen);
    assert(newlen > len);   /* Catch size_t overflow */
    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);
    assert(hdrlen + newlen + 1 > len);  /* Catch size_t overflow */
    if (oldtype==type) {
        newsh = s_realloc_usable(sh, hdrlen+newlen+1, &usable);
        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_usable(hdrlen+newlen+1, &usable);
        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);
    }
    usable = usable-hdrlen-1;
    if (usable > sdsTypeMaxSize(type))
        usable = sdsTypeMaxSize(type);
    sdssetalloc(s, usable);
    return s;
}

sdscatlen将t指向长度为len的任意二进制数据追加到sds字符串s后面。

sdscatlen在实现中,先调用sdsMakeRoomFor来保证字符串s有足够的空间来追加长度为len的数据。

sdsMakeRoomFor可能会分配新的内存,也可能不会取决于t的这个len

我们详细来看看这个sdsMakeRoomFor函数

  • 首先当原本的空间足够用,那就什么也不用做if (avail >= addlen) return s;

  • 如果需要分配空间,它会比实际请求的要多分配一些 2倍,以防备接下来的继续追加。它在字符串已经比较长的情况下要至少分配SDS_MAX_PREALLOC个字节,这个常量在sds.h中定义为 1024 * 1024 = 1M;

    具体是这个代码:

    // 定义的SDS_MAX_PREALLOC
    #define SDS_MAX_PREALLOC (1024*1024)
    
    // 分配内存代码
    len = sdslen(s);
    sh = (char*)s-sdsHdrSize(oldtype);
    newlen = (len+addlen);
    assert(newlen > len);   /* Catch size_t overflow */
    if (newlen < SDS_MAX_PREALLOC)
      newlen *= 2;
    else
      newlen += SDS_MAX_PREALLOC;
    
  • 分配后的空间需要跟换header类型(原来的allo类型太小,表达不了后面的容量)具体代码如下:

    if (oldtype==type) {
      newsh = s_realloc_usable(sh, hdrlen+newlen+1, &usable);
      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_usable(hdrlen+newlen+1, &usable);
      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);
    }
    

由于本人还比较菜,实在不理解两个函数。一个是s_realloc_usable一个是s_trymalloc_usable不能做详尽分析😣

后面的sds还有很多函数sdscpy,sdstrim,sdsjoin我觉着只要搞懂sds内部结构还有内存分配这一块,应该都能迎刃而解,这就不在赘述sds啦~

写的有点乱,见谅啊

参考资料:

https://www.hoohack.me/2017/11/13/read-redis-src-sds

https://zhuanlan.zhihu.com/p/88635294

http://zhangtielei.com/posts/blog-redis-sds.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值