在任何语言和系统中,字符串都几乎是使用频率最高的数据结构,而标准的c字符串有一些问题,主要包括:
- 字符串必须以’\0’结尾,导致图片等二进制不安全的数据无法以字符串形式存储;
- 字符串统计长度的复杂度为O(n),调用strlen时会对整个字符串从头到尾遍历,如果不小心在外层加了一层循环,会导致复杂度变更高,这点上没有java或者python方便;
- 容易发生溢出的问题,字符串是以’\0’为结尾的,一旦操作不慎,会在拼接字符串时,溢出到别的内存区域;
- 拼接字符串比较费时,3中说的一般是字符数组,字符串变量本身不能直接拼接,每次执行cat时,需要计算出两个字符串的长度,然后开辟一个全新的大小合适的空间,在那里写入新的字符串,而malloc这个系统调用实在是太慢了,很多高级语言为了避免这个方法调用,都使用了缓存,而标准的c似乎没有,至少字符串操作时不行;
以上四点是目前我能想到的c字符串的问题,或许还有其他,慢慢补充。
sds基本结构
sds是simple dynamic string的缩写;鉴于字符串在sds中基本就是最重要的,基本上哪里都得用到它,redis对字符串做了很多改良性的工作。
粗略的看,sds的基本结构是这样的:
struct sdshdr {
int len; // buf中已使用字节的数量
int alloc; // buf中总共分配的大小(除了末尾的'\0')
char buf[]; // 字节数组,保存字符串;
}
以上是sds的基本结构,类似于面向对象语言,sds将字符串的长度作为属性值保存在了一个结构体中,同时开了一个比字符串本身要长的空间做缓存,用alloc来标记这个分配的空间的大小(虽然sds有len属性标记字符串长度,但是依然会在每个字符串最后加一个’\0’,必要情况下,可以直接使用c的接口;这个末尾的空字符没有记在alloc中,因此实际的buff部分长度是alloc+1,这点要注意)。
以上只是对sds的简化,实际上redis源码中,sds有五种数据结构(参见sds.h文件),如下:
typedef char *sds; // 好处很明显,可以直接与c字符串既有接口通用;
/* 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; /* 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[];
};
sds根据初始创建对象时字符串的长度,创建了五种结构体,其中第一种长度最短,按照作者的意思,这个是没有使用的,而其它四种结构完全一样,区别只是长度区间不同而已。
这五种类型中都有一个flags字段,是一个单字节字段,区分sds类型的参数就在这里边,最低三位表示sds类型,而类型的值包括:
#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
#define SDS_TYPE_MASK 7 // 因为是最低三位存储类型,111;
#define SDS_TYPE_BITS 3 // 为了sds5计算长度时右移使用;
这里还有一点要注意,定义结构体时,使用了__attribute__((packed)),正常的c语言中,分配内存时会执行字节对齐,这就可能导致字段之间会不连续;而使用这个字段,会强制各个字段连续存储而不对齐,比如在所有结构体中,buf与flags永远是相邻两个字节,而从len到alloc到flags也是相邻的,redis在计算对象的实际起始地址时,使用了这个特征,这个要注意。
sdshdr5
这种结构下的字符串长度最短,而且没有alloc和len字段,即没有冗余空间。在其它类型中,flags的高5位是不使用的,但这个会使用高五位记录buf字段的大小;
其它四种
剩余四种整体结构一致,区别只是len和alloc的大小区间不一样,如sds8只用一个字节,而16使用了2个字节,其它类似;
sds接口
获取sds字符串长度:
#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T)))) // 获取其它四种类型实例的起始指针,##T是个特殊的标记符,编译时会将T替换成相关的类型数字值;
#define SDS_TYPE_5_LEN(f) ((f)>>SDS_TYPE_BITS) // 获取sds5类型的buf长度
static inline size_t sdslen(const sds s) {
unsigned char flags = s[-1]; // c中支持负索引,此时代表指针减1,而不是常规的指针加1,指针减1正好偏移到flags字段;
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;
}
由于两种不同结构体的存在,在获取长度时,就需要判断类型;
首先要注意的是,虽然结构体叫做sdshrd*,但是在对外的接口中,实际的参数类型都是sds,而它本质上是一个指向buf数组的char*指针,我们使用sds时必须使用这个类型而不能是sdshdr这个结构体家族,特别是释放内存的时候,不然会出错的;
其次,flags字段和buf字段是紧挨着的,而且都是单字节,因此根据buf的位置可以立马就能找到flags字段;
第三,具体的结构体类型存储在flags中的低三位,因此通过flags做位操可以获取到类型,即上面的&操作;
第四,其它四种都是要先通过SDS_HDR获取到类型对象的起始指针,然后直接读取len字段;而sds5就不同了,该类型直接使用flags的高5位存储字符串,所以它需要做的是对flags右移取值;
获取sds中未使用部分的容量大小:
static inline size_t sdsavail(const sds s) {
unsigned char flags = s[-1];
switch(flags&SDS_TYPE_MASK) {
case SDS_TYPE_5: {
return 0;
}
case SDS_TYPE_8: {
SDS_HDR_VAR(8,s);
return sh->alloc - sh->len;
}
... // 其余同sds8。
}
return 0;
}
基本同sdslen,唯一的区别是sds5没有未使用部分,它的已使用大小永远都是buf的字符串大小,因此永远返回0;
给sds设置新的字符串长度:
static inline void sdssetlen(sds s, size_t newlen) {
unsigned char flags = s[-1];
switch(flags&SDS_TYPE_MASK) {
case SDS_TYPE_5:
{
unsigned char *fp = ((unsigned char*)s)-1;
*fp = SDS_TYPE_5 | (newlen << SDS_TYPE_BITS);
}
break;
case SDS_TYPE_8:
SDS_HDR(8,s)->len = newlen;
break;
... //同sds8
}
}
获取sds头部大小
static inline int sdsHdrSize(char type) {
switch(type&SDS_TYPE_MASK) {
case SDS_TYPE_5:
return sizeof(struct sdshdr5);
case SDS_TYPE_8:
return sizeof(struct sdshdr8);
... // 其它雷同8
}
return 0;
}
这个方法用来获取sds头部(不包括buf字段,在c中,像buf这样不声明大小的数组,被认为是一个占位符,本身是不计入头部大小的,需要有其它手段协助才能知道)大小;
根据需要的buff大小,返回需要的sds类型:
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
}
以上这些接口都是static的,基本是内部的sds中其它接口被调用时使用。
创建一个新的sds对象
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;
int hdrlen = sdsHdrSize(type);
unsigned char *fp; /* flags pointer. */
sh = s_malloc(hdrlen+initlen+1);
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; // 指向flags字段
switch(type) {
case SDS_TYPE_5: {
*fp = type | (initlen << SDS_TYPE_BITS);
break;
}
case SDS_TYPE_8: {
SDS_HDR_VAR(8,s); // 宏定义展开,操作后,sh将被转为相应sds类型的对象,并指向该对象的起始位置;
sh->len = initlen;
sh->alloc = initlen;
*fp = type;
break;
}
... // 其它三种同sds8
}
if (initlen && init)
memcpy(s, init, initlen);
s[initlen] = '\0';
return s;
}
这个接口用来创建一个新的sds对象。
首先,根据传入的size选择使用哪种结构体,sds5虽然不被推荐使用,不过这里并没有完全废弃,当传入的initlen足够小但还不是0时,依然使用了sds5;这里还需要注意,在分配大小时,尾部多分配了一个用来存储’\0’的字节;
第二步,设置flags的值,只有sds5需要同时设置长度和类型,其它都是单独分开设置的;
第三步,如果init不是空指针,就把该字段的值复制到新开的buf区域中,这里不用担心有溢出发生,因为initlen已经保证了复制操作不可能溢出;另外就是末尾设置了’\0’;
比较奇怪的地方是不知都为什么init是一个void而不是char,好像是为了多态?因为在后面dsdup中页调用了这个函数,但传入的是sds而不是char*;
根据给定的字符串创建sds对象
/* 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);
}
本质上就是调用sdsnewlen函数,不过它只接收字符串作为参数。
复制sds对象
sds sdsdup(const sds s) {
return sdsnewlen(s, sdslen(s));
}
通过传入的sds对象,构造一个各参数内容一样的对象出来;
释放sds对象
void sdsfree(sds s) {
if (s == NULL) return;
s_free((char*)s-sdsHdrSize(s[-1])); // 计算出真正的起始位置;
}
这里是sds释放时应该被调用的方法,因为sds只是指向buf的指针,整个对象的起始位置不在这里,所以千万不能调用free直接操作,搞不好会爆炸的;
扩展sds buf大小
sds sdsMakeRoomFor(sds s, size_t addlen) {
void *sh, *newsh;
size_t avail = sdsavail(s); // 计算当前buf中还有多少自由空间,如果足够的话就不重新分配;
size_t len, newlen;
char type, oldtype = s[-1] & SDS_TYPE_MASK; // 需要保存当前sds类型,因为扩容可能会引起类型变化;
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) //#define SDS_MAX_PREALLOC (1024*1024)。即1M.
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) { // 扩容前后类型一样,直接开辟一个新空间把所有值复制过去就行,除了alloc字段;
newsh = s_realloc(sh, hdrlen+newlen+1); // realloc会自动将sh中的每个字节按序copy到新开辟的空间中,并释放sh指向的空间;
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, newlen);
return s;
}
当要给一个sds进行扩容时使用,这里的addlen不是值扩容后buf分配的大小,而是指冗余的大小;
首先计算当前剩余的空间,如果比addlen大,就不分配直接返回;
然后根据上一步计算出的新的长度确定需要给buf分配的真实大小,sds在分配时不是说要有addlen的容量就一定给addlen的容量,它只是保证自由空间大小不会比整个值小而已,上一步就是这样的,而这一步中,sds会在需求的空间大小上,再进一步扩大整个空间,因为这种需求往往来自于append,而append如果出现连续操作的情况,则可能会导致不停的系统调用,因此sds在分配时实际上给的空间要比需要的大,根据新大小的需求不同分配的大小不同:
- 分配后的新的空间不超过1M,就给它翻倍的空间;
- 分配后的空间超过了1M,就只给它多1M的额外空间,不然内存怕是扛不住的;
第三步,根据重新计算后需要的大小来判断新分配的sds头类型,因为这种扩容操作极有可能造成类型升级,所以需要看一下是否整个头结构都要变化,因为sds5没有冗余,所以至少要使用sds8;
第四步,这里开始将原始值复制到新的目标空间,如果扩容前后类型一样,那么只要将原来的各个字节从头到尾copy到新空间即可;如果类型不同,除了copy内容外,还要设置flags字段和len字段;
最后一步,扩容后,不管哪种情况,alloc字段都需要重新设置;
清除sds结尾的冗余空间
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) { // 为什么type > sds8 时也可以呢?
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;
}
这个方法没有什么特殊的地方,唯一没有想明白的是为什么缩容前后类型不同但都是sds8以上的内容时,可以直接复制呢?
获取整个sds对象的大小
size_t sdsAllocSize(sds s) {
size_t alloc = sdsalloc(s);
return sdsHdrSize(s[-1])+alloc+1;
}
这个值包含了头部大小、buf有效字符部分、冗余部分、最后的’\0’部分。
将sds len增加到指定size
void sdsIncrLen(sds s, ssize_t incr) {
unsigned char flags = s[-1];
size_t len;
switch(flags&SDS_TYPE_MASK) {
case SDS_TYPE_5: {
unsigned char *fp = ((unsigned char*)s)-1;
unsigned char oldlen = SDS_TYPE_5_LEN(flags);
assert((incr > 0 && oldlen+incr < 32) || (incr < 0 && oldlen >= (unsigned int)(-incr)));
*fp = SDS_TYPE_5 | ((oldlen+incr) << SDS_TYPE_BITS);
len = oldlen+incr;
break;
}
case SDS_TYPE_8: {
SDS_HDR_VAR(8,s);
assert((incr >= 0 && sh->alloc-sh->len >= incr) || (incr < 0 && sh->len >= (unsigned int)(-incr)));
len = (sh->len += incr);
break;
}
... // 其它类似于sds8
default: len = 0; /* Just to avoid compilation warnings. */
}
s[len] = '\0';
}
这个方法应该是不能单独被使用的,按照作者的意思,当调用上面的sdsMakeRoomFor后,可能会使用这个方法,作者举例如下:
* oldlen = sdslen(s);
* s = sdsMakeRoomFor(s, BUFFER_SIZE);
* nread = read(fd, s+oldlen, BUFFER_SIZE);
* ... check for nread <= 0 and handle it ...
* sdsIncrLen(s, nread);
可以将sds作为输出的直接位置而不用使用中间缓冲区,此时在将内容写入完毕后,可以调用sdsIncrLen函数,将len设置成指定长度。
它的逻辑也比较简单,有两点需要注意:
1)incr参数可正可负,负表示要缩短len的大小,但正数时不能溢出,因为不会扩容,而负数时不能将长度缩短到小于0;代码中通过assert进行判断;
2 case最后使用了default,作者的意思是要防止编译警告,不过在之前的其它地方却都没有这么干过,不知道这里为什么就需要单独警告,改天编译起来看看效果;
填充sds到指定长度
sds sdsgrowzero(sds s, size_t len) {
size_t curlen = sdslen(s);
if (len <= curlen) return s;
s = sdsMakeRoomFor(s,len-curlen);
if (s == NULL) return NULL;
/* Make sure added region doesn't contain garbage */
memset(s+curlen,0,(len-curlen+1)); /* also set trailing \0 byte */
sdssetlen(s, len);
return s;
}
这个函数没什么特色,就是将sds用0填充到指定长度,要是原始len本来就比目标len长,无操作;
向sds中追加二进制数据
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;
}
没什么特色,只是由于是二进制数据,结尾不知道在哪里,所以需要专门传入长度参数。
向sds中追加字符串数据
sds sdscat(sds s, const char *t) {
return sdscatlen(s, t, strlen(t));
}
更没什么特色,直接就是调用上面的方法,只是这次有\0结尾知道长度而已。
用char *更新sds数据
sds sdscpylen(sds s, const char *t, size_t len) {
if (sdsalloc(s) < len) {
s = sdsMakeRoomFor(s,len-sdslen(s));
if (s == NULL) return NULL;
}
memcpy(s, t, len);
s[len] = '\0';
sdssetlen(s, len);
return s;
}
sds sdscpy(sds s, const char *t) {
return sdscpylen(s, t, strlen(t));
}
没啥,只是上面的sdscpylen函数比较奇怪,看作者的注释和代码中做的,是用*t的内容完全替换既有的sds中的内容,但是当空间不足时,作者居然使用了sdsMakeRoomFor函数而不是完全新建一个新的,不太能理解;
long long int转换成字符串
#define SDS_LLSTR_SIZE 21
int sdsll2str(char *s, long long value) {
char *p, aux;
unsigned long long v;
size_t l;
/* Generate the string representation, this method produces
* a reversed string. */
v = (value < 0) ? -value : value; // 因为v是unsigned类型,所以不会溢出;
p = s;
do {
*p++ = '0'+(v%10);
v /= 10;
} while(v);
if (value < 0) *p++ = '-';
/* Compute length and add null term. */
l = p-s;
*p = '\0';
/* Reverse the string. */
p--;
while(s < p) {
aux = *s;
*s = *p;
*p = aux;
s++;
p--;
}
return l;
}
这里的char *s参数是指向一片有效的字符串空间,且长度足够长,至少不短于SDS_LLSTR_SIZE;另外为了防止在将value转成正数时发生溢出,v使用unsigned类型;返回的类型是转换后字符串的长度;
unsigned long long转换字符串
int sdsull2str(char *s, unsigned long long v)
这个类似上面,它连溢出都不需要考虑;
用long long 创建sds对象
sds sdsfromlonglong(long long value) {
char buf[SDS_LLSTR_SIZE];
int len = sdsll2str(buf,value);
return sdsnewlen(buf,len);
}
直接调用上面的sdsll2str方法。
还有其它的一些接口,基本都类似与上面,不再列出。