Redis数据结构-简易动态字符串sds

Redis底层数据结构分析——sds(simple dynamic string)

Redis字符串

sds(Simple Dynamic String)实现了一个简易的std::string,具备动态内存的特性,与std::string作为一个类不同,sds看起来就是char*
redis/src/sds.h约300行,.c文件约1300行+300行单测。

先在第一部分将涉及到的语言部分放上来

1. C 语法相关

1.1 __attribute__ 关键字

声明函数,结构类型的属性值,给编译器看的。常见的如:
- `__attribute__((noreturn))` 不返回
- `__attribute__((packed))`取消内存对齐
- `__attribute__((__molloc__))`molloc返回指针不能指向已存在object
- `__attribute__((__nothrow__))`不抛异常
- `__attribute__ ((__leaf__))`不会再调用其他函数
    struct node_{
        char flag;
        int id;
        char buf[];
    };
    struct __attribute__((__packed__)) node{
        char flag;
        int id;
        char buf[];
    };
    sizeof(node_)=8,sizeof(node)=5

1.2 C语言柔性数组

结构体中包含两个及以上属性,且定义最后一个元素为定义大小(或0)的数组时,该数组不计入结构体大小的计算,对struct申请内存是注意内存大小加上数组大小。realloc时,新旧的内存是连续的。

1.3 宏定义中的##

内容连接作用:`int##x=intx`

1.4 内存相关函数

  1. malloc C语言中最常用的堆内存申请函数,gcc>3.3的定义为: void *malloc (size_t __size) __THROW __attribute_malloc__ __wur __THROW=define __THROW __attribute__ ((__nothrow__ __LEAF))
    禁用throw;不会调用其他函数;不会返回错误码

  2. size_t malloc_usable_size (void *__ptr) __THROW; 返回指针__ptr指向内容申请的内存大小

  3. void *realloc (void *__ptr, size_t __size) __THROW __attribute_warn_unused_result__; 注意这里没有使用__attribute__((__malloc__))表明其返回值可以是原指针

  4. redis的sdsalloc.h文件中使用的内存相关函数,会使用原子变量记录memory_used,可能后面又内存动态管理吧,内存管理设计内容太丰富了
    #define atomicIncr(var,count) atomic_fetch_add_explicit(&var,(count),memory_order_relaxed)
    #define atomicDecr(var,count) atomic_fetch_sub_explicit(&var,(count),memory_order_relaxed)

2. sds定义

2.1 第一印象

初次打开redis源码文件 redis/src/sds.h时, sds的定义是typedef char *sds;,怎么不说是字符串对象吗?怎么是个char类型的指针,哪道是在字符串中定义有隐含的协议,例如:xxyyzz,总长度表示长度,yy表示已经使用的长度,zz是实际内容? 带着这种疑问,查看了操作sds的API,发现这其中的秘密,sds与其背后的结构体定义。这里我觉得需要关注的是两者之间是如何转化的。 因为如果是我,也许直接让sds为其与之定义的结构体了
补:看完源码之后,多个操作会改变sds对应的sdshr结构,但是这种header的改变对调用方来说是无感知的,直接使用结构体则不然。

2.2 sds背后的结构体

先看源码中的定义,这里__attribute__关键字的作用见 1.1, 柔性数组见 1.2.

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; /* used */
    uint8_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    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[];
};
  • len: buf已使用大小
  • alloc: buf当前总的内存大小
  • flags:标记uintxx_t,可能为下列值
#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

这里为什么不直接定义为uintxx_t对应的大小?
这种二进制(00,01,10,11,100)的定义方式,通过一个掩码与操作,可以达到与enum相同的目的, flag&b111, 结果被限制为上述几种宏的值

  • buf:实际sds存储数据的buf, 也就是说sds值为buf的值,数组的起始地址。
    现在知道sds指向buf,那么如何获取结构体的其他属性呢?-> 指针,由于内存对齐且unsiged char占用一个字节, 在内存连续的情况下,那么buf的前一个字节内存存储的就是flags的内容,通过flags能够获得整个结构体大小n,那么buf的前n个字节就是结构体的起始地址,通过强转得到指向该结构体的指针,从而获取结构体属性.看下源码是怎么实现的
2.2.1 以strlen函数看一下char* 和 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;
}
  • 第一眼看到s[-1],通常自己是不会这么写的。s[-1]等价于 s-1, 得益于s指向的内容单个元素大小恰为1byte,等于unsigend char大小,s-1指向flags,所以 s[-1]为flags的内容.
  • 获取到flags标识后通过SDS_HDR(sds_header)宏获得结构体起始地址
    SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))))
    s指针向后移动header大小到结构体起始地址,然后强转为sdshdT类型指针,就此实现 sds->sdshdT的转换.之后就可以通过指针去访问len,alloc,flags等属性。那么这一步为什么不继续使用指针后移然后进行强转了呢,例如sdshd8,flags向后移动一位指向alloc,将这片内存强转为uint8,例如:*(uint8_t*)(s-2)?
2.2.2 转换相关的简易api
  • sdsavail(alloc - len),sdssetlen(重设sds的len属性),sdsinclen(增加sds的len属性),sdsalloc(获取alloc属性),sdssetalloc(设置alloc), 这些api都是将sds转化为结构体后对其属性的读写操作。

2.3 sds对象相关操作

  1. 创建对象
  • 根据输入initLen判断使用的sdshrT
  • 申请 initLen+1+header大小的内存
  • 根据类型填充sdshr其他属性值
sds _sdsnewlen(const void *init, size_t initlen, int trymalloc) {
    void *sh;
    sds s;
    // 根据初始长度,判断使用哪种类型的sdshd类型与sds对应
    char type = sdsReqType(initlen);
    /* Empty strings are usually created in order to append. Use type 8
     * since type 5 is not good at this. */
    // 空字符串类型,初始化为最短的uint8_t相关的sdshr
    if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
    // 获取该类型header的大小
    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);
    // 可以看到,此时申请内存大小为输入长度+header大小+'\0', usable代表实际分配的内存大小
    // 例如hdrlen+initlen+1=15, useable可能为24
    if (sh == NULL) return NULL;
    if (init==SDS_NOINIT)
        init = NULL;
    else if (!init)
        memset(sh, 0, hdrlen+initlen+1);
    s = (char*)sh+hdrlen;   // 使s指向buf
    fp = ((unsigned char*)s)-1; // 指向flags
    usable = usable-hdrlen-1;   // buf还可以使用的内存大小
    if (usable > sdsTypeMaxSize(type))
        usable = sdsTypeMaxSize(type);
    // 根据类型,初始化 len,flags,alloc属性
    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);
    // 自己管理'\0'
    s[initlen] = '\0';
    return s;
}
  1. _sdsMakeRoomFor sds内存扩展
    当sds变长时,可能没有足够的内存,这时需要扩展内存
  • 这种扩展可以保证sds对象是内存连续的
  • 如果可用空间不小于addlen时不进行扩展
  • 计算新长度,newlen,预分配模式下,若是newlen<1M,则扩展长度为newlen的两倍;否则为newlen+1M
  • 获取sdshrT的类型,若是类型没有发生变化,在原起始地址处realloc;若是类型发生变化,sds头需要改变,则需要重新申请内存,拷贝原内容到新地址
sds _sdsMakeRoomFor(sds s, size_t addlen, int greedy) {
    void *sh, *newsh;
    size_t avail = sdsavail(s);
    size_t len, newlen, reqlen;
    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);
    reqlen = newlen = (len+addlen);
    assert(newlen > len);   /* Catch size_t overflow */
    if (greedy == 1) {
        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 > reqlen);  /* 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;
}
  1. sdsRemoveFreeSpace
    同理,也需要对内存进行释放,例如sbs结构向上增加后buf大小没有得到利用,len/alloc较小
  • 根据已经使用的空间len计算类型sdshrT
  • 若类型相同,或者是类型能够包下sdshrT,则header不需要变化,直接进行realloc,较小空间
  • 若类型变小(SDS_TYPE_8=sdshr8类型),header也需要缩小,直接重新申请内存,copy
sds sdsRemoveFreeSpace(sds s) {
    void *sh, *newsh;
    char type, oldtype = s[-1] & SDS_TYPE_MASK;
    int hdrlen, oldhdrlen = sdsHdrSize(oldtype);
    size_t len = sdslen(s);
    size_t avail = sdsavail(s);
    sh = (char*)s-oldhdrlen;

    /* Return ASAP if there is no space left. */
    if (avail == 0) return s;

    /* Check what would be the minimum SDS header that is just good enough to
     * fit this string. */
    type = sdsReqType(len);
    hdrlen = sdsHdrSize(type);

    /* If the type is the same, or at least a large enough type is still
     * required, we just realloc(), letting the allocator to do the copy
     * only if really needed. Otherwise if the change is huge, we manually
     * reallocate the string to use the different header type. */
    if (oldtype==type || type > SDS_TYPE_8) {
        newsh = s_realloc(sh, oldhdrlen+len+1);
        if (newsh == NULL) return NULL;
        s = (char*)newsh+oldhdrlen;
    } else {
        newsh = s_malloc(hdrlen+len+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, len);
    return s;
}
  1. 内存安全的API示例
    在进行内容拷贝,连接等操作时会检查可用内存是否充足,否则调用 _sdsMakeRoomFor进行内存的扩展,例如sdscpylen
sds sdscpylen(sds s, const char *t, size_t len) {
    if (sdsalloc(s) < len) {
        // 当前可用内存小于len,则扩充 len-sdslen(s),默认是使用预分配算法的
        s = sdsMakeRoomFor(s,len-sdslen(s));
        if (s == NULL) return NULL;
    }
    memcpy(s, t, len);
    s[len] = '\0';
    sdssetlen(s, len);
    return s;
}

2.4 sds结构体相比于char*的优势

  • 首先sds结构体相当于在char*上加了一个header,多使用了部分内存(sizeof(header)+1)
  • O(1) 时间获取字符串长度
  • +1的字节是存储’\0’,也就是说对于内容中包含’\0’的字符串与普通字符串同等对待
  • 由于记录了alloc和len的大小,在涉及到内存操作时api多了一步判断,增强了安全性
  • 得益于alloc的伸缩方案,减少了频繁的内存申请和销毁

3.总结

  1. 理解sdshrT的定义,通过新增len,alloc属性,方便获取sds长度,进行更安全的内存操作和动态伸缩
  2. 采用柔性数组存储实际的字符串内容,自己管理null-terminated;在内存伸缩时,header变长必须重新malloc分配内存,否则realloc
  3. sds通过禁用内存对齐从而 sds-1获取flags, sds-header_size获取sdshrT, 实现sds到sdshrT对象转换;api入参和返回值仅是sds,实现统一
  4. 内存预分配时,1M是一个阈值,低于1M变为预设长度2倍,超出1M,每次新增1M
  5. 若非显示的调用内存缩小api,一般api操作引起的buf实际长度的较小,并不会立即释放多余的空间,仅是修改len长度。
    在涉及内存边长的API中会显示调用_sdsMakeRoomFor,但是但从sds定义的文件中没有看到显示调用sdsRemoveFreeSpace或Resize,
    那么什么时候显示调用内存缩小呢?
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值