前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Simple Dynamic Strings(SDS)源码解析和使用说明一

Simple Dynamic Strings(SDS)源码解析和使用说明一

作者头像
方亮
发布2019-01-16 15:57:33
5680
发布2019-01-16 15:57:33
举报
文章被收录于专栏:方亮

        SDS是Redis源码中一个独立的字符串管理库。它是由Redis作者Antirez设计和维护的。一开始,SDS只是Antirez为日常开发而实现的一套字符串库,它被使用在Redis、Disque和Hiredis等作者维护的项目中。但是作者觉得这块功能还是比较独立的,应该让其成为一个独立的库去被使用。于是就开发了第二版的SDS。本文我们要讨论的SDS就会是基于这个版本的。(转载请指明出于breaksoftware的csdn博客)

结构

       一般来说,如果我们要设计一个字符串,可能会使用到结构。其中至少要保存一个指向字符串内容的指针,比如

代码语言:javascript
复制
struct string_demo {
    char* data_ptr;
};

        如果我们考虑到strlen计算会随着字符串长度增长而耗费更多时间时,可能还需要给结构体增加一个字符串长度字段

代码语言:javascript
复制
struct string_demo {
    size_t data_len;
    char* data_ptr;
};

        如果还要考虑到字符串对象的使用安全性,我们可能还需要给该结构增加一个引用计数

代码语言:javascript
复制
struct string_demo {
    int refrence;
    size_t data_len;
    char* data_ptr;
};

        对于这样的字符串结构体对象,我们在使用时往往需要使用指针方式才能引用到相应的成员变量。比如我们要使用C语言中strlen计算字符串长度,则需要如下操作

代码语言:javascript
复制
string_demo_ptr->data_len = strlen(string_demo_ptr->data_ptr);

        或者我们就需要开发出一套针对我们设计的字符串结构的计算方法

代码语言:javascript
复制
size_t calc_string_demo_len(string_demo* ptr) {
    ……
}

        但是无论哪种方式,这种设计都导致C语言中字符串操作函数不能直接操作我们的字符串结构体对象。但SDS库则通过巧妙的设计让我们可以直接使用这些函数操作SDS字符串。我们看下它的定义

代码语言:javascript
复制
typedef char *sds;

        SDS字符串sds只是char*的别名,它并没有使用结构体去表示这个对象。这样我们就可以从语法层面保证调用C语言中字符串方法不会报错。然而通过这种写法,我们应该可以想到,sds所指向的内存空间保存就是字符串的内容,且和C语言中字符串内容的格式存在兼容性(没说一致性,因为SDS字符可以存储null,后面我们会做说明)。这样才可以让诸如strlen之类的方法正确执行。

        但是,如果SDS字符串结构仅仅如此,那就没有必要通过一篇博文去解释了。SDS字符其实也存在我们上述猜想中的结构,只是它没有让保存字符串内容的指针和结构体混为一体,但是在内存分布上是连续的。这个怎么理解呢?我认为Antirez在设计SDS时,是希望实现一套兼容C语言字符串只读性操作方法(不修改字符串内容)的结构。这样的话就要保证结构是一个char*型的指针,且内容指向字符串内容。但是为了可以更加高效的获取字符串长度以及辅助其他操作,则需要保存这个字符串额外的信息。这些信息总不能在内存中使用(字符串指针地址,额外信息)这样的map结构存储,因为通过指针地址查找额外信息的过程会降低整体效率,且这种割裂式的分布也不利于对象的整体管理。那么就让它的额外信息分布在其内容附近。于是就有分布在内容前还是内容后这两种选择。我们先来探讨分布在内容后,这样的设计会导致每个SDS字符串必须以NULL结尾,这会限制住SDS的承载能力。而且每次要取额外信息都要计算字符串结尾位置,这种计算会随着字符串变长而消耗更多时间。所以这种方案不可取。那么只剩下放在内容前这种方案了,SDS的确也是这么设计的。

代码语言:javascript
复制
+--------+-------------------------------+-----------+
| Header | Binary safe C alike string... | Null term |
+--------+-------------------------------+-----------+
         |
         `-> Pointer returned to the user.

        有一点我们之前没有注意,SDS字符串最后要以NULL字符结尾。这样的设计可以预防用户设置的字符串内容溢出。         我们看下Header部分是什么样的结构

代码语言:javascript
复制
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[];
};

        除了sdshdr5之外,其他结构都是相似的。我们先看看sdshdr,它只有flags和buf成员,其中flag空间被充分利用,其第三位保存了SDS字符串的类型:

代码语言:javascript
复制
#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

        高5位则保存了字符串的长度,那么可见sdshdr5对应的SDS_TYPE_5类型字符串只能保存原串长度小于等于2^5=32个。

        我们再看下结构相对统一的其他头结构。由于不同类型的SDS字符串是为了保存不同长度的内容,所以它们主要区别是成员的类型不同。第一个成员变量len记录的是为buf分配的内存空间已使用的长度;第二个成员变量alloc记录的是为buf分配的内存空间的总长度,当然这长度不包括SDS字符串头和结尾NULL。第三个字符flags只是在第三位保存了SDS字符串类型,而剩下的高五位则没有使用。

        由于SDS字符串结构的设计,在我们需要访问头中成员变量时,需要通过sds指针向前回溯一个头结构体的长度,然后通过这个地址去访问。至于回溯多长,则要视该SDS字符串的类型而定,而这个信息就保存在sds指针前一个unsigned char长度的空间中——即flags。以获取SDS字符串内容的长度为例:

代码语言:javascript
复制
#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))))
#define SDS_TYPE_5_LEN(f) ((f)>>SDS_TYPE_BITS)

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_HDR宏在代码中出现非常多,因为通过它我们可以找到并转换SDS字符串头结构地址。

创建SDS字符串

        我们可以通过下面四个方法进行SDS字符串的创建

代码语言:javascript
复制
sds sdsnewlen(const void *init, size_t initlen);
sds sdsnew(const char *init);
sds sdsempty(void);
sds sdsdup(const sds s);

        sdsnew方法要求传入一个NULL结尾的字符串内存地址,其底层调用的是sdsnewlen方法:

代码语言:javascript
复制
sds sdsnew(const char *init) {
    size_t initlen = (init == NULL) ? 0 : strlen(init);
    return sdsnewlen(init, initlen);
}

        sdsempty方法创建了一个空的SDS字符串,其底层也是调用了sdsnewlen:

代码语言:javascript
复制
sds sdsempty(void) {
    return sdsnewlen("",0);
}

        sdsdup方法用于复制一个SDS字符串对象,其底层还是使用sdsnewlen去实现的:

代码语言:javascript
复制
sds sdsdup(const sds s) {
    return sdsnewlen(s, sdslen(s));
}

        现在我们重点关注下sdsnewlen方法的实现。

        首先,我们需要通过传入的长度确定创建什么类型的SDS字符串

代码语言:javascript
复制
sds sdsnewlen(const void *init, size_t initlen) {
    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;

        我们看下选择类型的原则:

代码语言:javascript
复制
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 (string_size < 1ll<<32)
        return SDS_TYPE_32;
    return SDS_TYPE_64;
}

        如果要求创建一个空串,作者认为一般创建的空串,在未来都是用于填充数据的。所以此时创建一个承载内容长度小于32的类型是不合适的,于是采用SDS_TYPE_8类型。那么只有在字符串内容在1到32之间的情况下才会创建SDS_TYPE_5类型的字符串。

        接下来要计算相应类型的头长度,并且根据头长度、字符串长度等申请一段空间

代码语言:javascript
复制
    int hdrlen = sdsHdrSize(type);
    unsigned char *fp; /* flags pointer. */

    sh = s_malloc(hdrlen+initlen+1);

        计算长度时最后加上是因为SDS字符串的整体结构要求以NULL结尾。

        如果要创建的是空串,则要将申请的内存都置空

代码语言:javascript
复制
    if (!init)
        memset(sh, 0, hdrlen+initlen+1);
    if (sh == NULL) return NULL;

        下一步,我们需要获取sds地址和SDS字符串头地址

代码语言:javascript
复制
    s = (char*)sh+hdrlen;
    fp = ((unsigned char*)s)-1;

        然后根据类型,我们需要向SDS字符串头结构中填写相应值,我们只看下SDS_TYPE_5和SDS_TYPE_8的设置

代码语言:javascript
复制
    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 = initlen;
            *fp = type;
            break;
        }

        可以见得,在最初创建SDS字符串时,alloc大小和len大小是一样的。它们产生差别是在之后介绍的字符串连接时。

        最后我们根据需要创建的字符串是否有内容而将相关数据复制到内存中,并让内存最后一位为NULL

代码语言:javascript
复制
    }
    if (initlen && init)
        memcpy(s, init, initlen);
    s[initlen] = '\0';
    return s;
}

        我们看下这些方法的使用样例

代码语言:javascript
复制
sds mystring = sdsnew("Hello World!");
printf("%s\n", mystring);
sdsfree(mystring);

output> Hello World!
代码语言:javascript
复制
char buf[3];
sds mystring;

buf[0] = 'A';
buf[1] = 'B';
buf[2] = 'C';
mystring = sdsnewlen(buf,3);
printf("%s of len %d\n", mystring, (int) sdslen(mystring));

output> ABC of len 3
代码语言:javascript
复制
sds mystring = sdsempty();
printf("%d\n", (int) sdslen(mystring));

output> 0
代码语言:javascript
复制
sds s1, s2;

s1 = sdsnew("Hello");
s2 = sdsdup(s1);
printf("%s %s\n", s1, s2);

output> Hello Hello

获取SDS字符串长度

        可以通过下面的方法获取SDS字符串的长度

代码语言:javascript
复制
size_t sdslen(const sds s);

        在之前,我们已经看了该函数的实现了。它只是返回SDS字符串头中的len字段值。这样设计有两个好处:

  • 相较于C语言中strlen,sdslen计算SDS字符串的长度的时间是固定的。我们知道strlen通过遍历内存,一直找到NULL才能计算出字符串长度。而SDS字符串长度在被改变时已经被计算好了,它被保存在字符串头结构中,这样每次获取时只要通过固定的地址偏移便可以拿到。
  • 它是二进制安全的。因为不依赖于NULL计算长度,所以NULL字符不再那么特殊了。SDS字符串中可以包含若干个NULL。

        但是有个东西需要注意下。虽然我们可以使用C语言中的只读性方法访问SDS字符串,但是由于SDS字符串内容中可以包含NULL,而C语言中字符串以NULL结尾,则会在混用时遇到一些的现象,如:

代码语言:javascript
复制
sds s = sdsnewlen("A\0\0B",4);
printf("%d\n", (int) sdslen(s));

output> 4

释放字符串

        由于SDS字符串的所有空间都是在堆上分配的,所以在不使用时我们需要释放它。

代码语言:javascript
复制
void sdsfree(sds s);

        我们看下其实现

代码语言:javascript
复制
void sdsfree(sds s) {
    if (s == NULL) return;
    s_free((char*)s-sdsHdrSize(s[-1]));
}

        该函数一开始时判断了传入的是不是NULL,所以我们在调用sdsfree前就不需要判断入参是否为空了。然后通过一系列位移计算出SDS字符串头的起始地址,它就是之前在sdsnewlen中通过malloc在堆上分配的空间地址,于是我们要使用free方法释放它。

        在之前我们介绍过,创建的空SDS字符串其实也是占用了一定的堆上空间,所以对空SDS字符串也要使用sdsfree去释放,否则会造成内存泄漏。最后我们看下使用的方法:

代码语言:javascript
复制
if (string) sdsfree(string); /* Not needed. */
sdsfree(string); /* Same effect but simpler. */
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2016年12月01日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 结构
  • 创建SDS字符串
  • 获取SDS字符串长度
  • 释放字符串
相关产品与服务
云数据库 Redis
腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档