一、sds.h
redis实现动态字符串的方法是定义一个结构体分别指明字符串长度、分配长度、类型和字符数组,其中类型用来表示不同长度的结构,如下:
/* 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[];
};
由于使用的是C语言编写,所以不能像C++中声明一个类作为所有结构的基类,这里redis采用了计算指针偏移查找变量首地址的方式来实现,这样对于不同的函数来说只要操作一个字符串地址就可以获取它的结构地址,进而获取得到所有的结构信息。通过以上方式就实现了类似继承类中获取同一函数的方法。
#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))))
这里的全局函数有很多,这里以sdslen为栗子,头文件中其余函数也类似:
typedef char *sds;
/* ... ... */
static inline size_t sdslen(const sds s) {
// 对于每一个结构体来说flag都是一个字节,按定义的顺序取其前一个地址的就是flags了
unsigned char flags = s[-1];
// 根据flag标志来判断类型
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;
}
static inline size_t sdsavail(const sds s) { /* ... */ }
static inline void sdssetlen(sds s, size_t newlen) { /* ... */ }
static inline void sdsinclen(sds s, size_t inc) { /* ... */ }
static inline size_t sdsalloc(const sds s) { /* ... */ }
// 其中alloc = avail + len
static inline void sdssetalloc(sds s, size_t newlen) { /* ... */ }
二、sds.c
在动态字符串的.c文件中首先要看到是字符串空间分配,需要注意的是redis本身有一套内存分配的封装函数,其可以返回分配的可用空间——s_trymalloc_usable和s_malloc_usable(关于redis内存分配的实现在内存篇章中再做分析)。
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 */
// 申请内存,内存长度为字符串长度+结构体大小+字符串终止符'\0'(即大小为1)
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;
// 计算可用的字符串长度,即alloc长度
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;
}
// 类似的代码就不再贴出了
/* ... ... */
}
if (initlen && init)
memcpy(s, init, initlen);
s[initlen] = '\0';
return s;
}
值得注意的是当直接将字符串某一位置置为'\0'时,其长度还是和原先一致,因此需要有一个update都过程,同时清空的操作也不释放内存而是将第一个字符置'\0',原理很简单,如下:
/* s = sdsnew("foobar");
* s[2] = '\0';
* sdsupdatelen(s);
* printf("%d\n", sdslen(s));
*
* The output will be "2", but if we comment out the call to sdsupdatelen()
* the output will be "6" as the string was modified but the logical length
* remains 6 bytes. */
void sdsupdatelen(sds s) {
size_t reallen = strlen(s);
sdssetlen(s, reallen);
}
void sdsclear(sds s) {
sdssetlen(s, 0);
s[0] = '\0';
}
初始化空间之后当遇到字符空间长度不够时扩展字符的可用长度,需要区分加上所需长度后是否会改变字符类型的情况,同时重新设定字符串长度和分配空间的长度。一般地,和vector的实现一样,分配空间首先是为长度翻倍。注意不能使用之前的结构体去存储新的结构,因为二者的成员变量长度不一致:
/* Since the header size changes, need to move the string forward,
* and can't use realloc */
另外需要好好关注void sdsIncrLen(sds s, ssize_t incr)的使用场景,它改变所需字符串的长度,即提前计算'\0'的位置,不用使用第三者局部变量存储字符串。
sds _sdsMakeRoomFor(sds s, size_t addlen, int greedy) {
void *sh, *newsh;
size_t avail = sdsavail(s);
size_t len, newlen;
// 获取type
char type, oldtype = s[-1] & SDS_TYPE_MASK;
int hdrlen;
size_t usable;
/* 可用的空间足够长则直接就返回 */
if (avail >= addlen) return s;
len = sdslen(s);
sh = (char*)s-sdsHdrSize(oldtype);
newlen = (len+addlen);
assert(newlen > len); /* Catch size_t overflow */
// 当应用"贪婪"时,则为空间翻倍或者添加SDS_MAX_PREALLOC
if (greedy == 1) {
if (newlen < SDS_MAX_PREALLOC)
newlen *= 2;
else
newlen += SDS_MAX_PREALLOC;
}
// 根据新的长度来获取类型,下面的代码分为了两种情况讨论
// 1.类型不变时直接realloc
// 2.类型变换时直接malloc再拷贝字符串并释放原先的结构体
type = sdsReqType(newlen);
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;
}
// 类似的实现有以下几个函数
// 去除多余的空间,若avail已经是零,那么直接返回
sds sdsRemoveFreeSpace(sds s) { /* ... */ }
// 重新改变字符串的长度,当原本长度长于size时直接截断
sds sdsResize(sds s, size_t size) { /* ... */ }
除了常规的cat、cpy函数之外,也有数值转字符串的函数,以long long类型转字符串为栗子:
int sdsll2str(char *s, long long value) {
char *p, aux;
unsigned long long v;
size_t l;
// 首先是判断数字的±性,当为负数时还需要注意是否是LLONG_MIN,因为负数的绝对值是超过ll类型的
// 最大值的,既|LLONG_MIN|=|LLONG_MAX+1|,这会导致溢出,所以需要使用范围更大的ull来接
if (value < 0) {
/* Since v is unsigned, if value==LLONG_MIN, -LLONG_MIN will overflow. */
if (value != LLONG_MIN) {
v = -value;
} else {
v = ((unsigned long long)LLONG_MAX) + 1;
}
} else {
v = value;
}
// 这里是直接得余数加入,但获取到的字符串是反的
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. */
// 将字符串取反,采用的是swap首尾两两交换的方法
p--;
while(s < p) {
aux = *s;
*s = *p;
*p = aux;
s++;
p--;
}
return l;
}
sds还实现了类似printf等打印函数的功能,为vsnprintf封装以更好适应sds,注意va_list是一个指向参数首地址的指针:
typedef struct {
char *a0; /* pointer to first homed integer argument */
int offset; /* byte offset of next parameter */
} va_list;
/* s = sdsnew("Sum is: ");
* s = sdscatprintf(s,"%d+%d = %d",a,b,a+b).
*/
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); // 将ap复制给cpy
// 通过vsnprintf把cpy对应的fmt中的字符串拷贝给buf
vsnprintf(buf, buflen, fmt, cpy);
va_end(cpy); // 清空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 sdscatprintf(sds s, const char *fmt, ...) {
va_list ap;
char *t;
va_start(ap, fmt);
t = sdscatvprintf(s,fmt,ap);
va_end(ap);
return t;
}
/* %s - C String
* %S - SDS string
* %i - signed int
* %I - 64 bit signed integer (long long, int64_t)
* %u - unsigned int
* %U - 64 bit unsigned integer (unsigned long long, uint64_t)
* %% - Verbatim "%" character.
*/
sds sdscatfmt(sds s, char const *fmt, ...) {
size_t initlen = sdslen(s);
const char *f = fmt;
long i;
va_list ap;
// 预先分配空间,减少空间分配的次数
s = sdsMakeRoomFor(s, initlen + strlen(fmt)*2);
va_start(ap,fmt);
f = fmt; /* Next format specifier byte to process. */
i = initlen; /* Position of the next byte to write to dest str. */
while(*f) { // 指向下一个字符,注意不是字符串
char next, *str;
size_t l;
long long num;
unsigned long long unum;
/* Make sure there is always space for at least 1 char. */
if (sdsavail(s)==0) {
s = sdsMakeRoomFor(s,1);
}
switch(*f) {
case '%':
next = *(f+1);
f++;
switch(next) { // 这个next标识的是%后一位
case 's':
case 'S':
// va_arg用于获取可变参数...的每一个参数。如函数fun(char *fmt, ...)的一次调用
// fun(fmt, arg1, arg2, arg3)。在使用va_start()进行ap的初始化后,调用一次
// va_arg(ap,type)就获得了参数arg1,在调用一次就获得arg2,……从而,得到每一个
// 参数的值
str = va_arg(ap,char*);
l = (next == 's') ? strlen(str) : sdslen(str);
if (sdsavail(s) < l) {
s = sdsMakeRoomFor(s,l);
}
memcpy(s+i,str,l);
sdsinclen(s,l);
i += l;
break;
case 'i':
case 'I':
/* ... 尝试自己实现 ... */
break;
case 'u':
case 'U':
/* ... 尝试自己实现 ... */
break;
default: /* Handle %% and generally %<unknown>. */
s[i++] = next;
sdsinclen(s,1);
break;
}
break;
default:
s[i++] = *f;
sdsinclen(s,1);
break;
}
f++;
}
va_end(ap);
s[i] = '\0';
return s;
}
还有许多工具函数,可以尝试自己实现:
/* s = sdsnew("AA...AA.a.aa.aHelloWorld :::");
* s = sdstrim(s,"Aa. :");
* printf("%s\n", s);
* Output will be just "HelloWorld".
*
* using char *strchr(const char *str, int c)
* str -- 要被检索的字符串
* c -- 在str中要搜索的字符
*/
sds sdstrim(sds s, const char *cset) { /* ... */ }
/* s = sdsnew("Hello World");
* sdsrange(s,1,-1); => "ello World"
*/
void sdsrange(sds s, ssize_t start, ssize_t end) { /* ... */ }
/* positive if s1 > s2.
* negative if s1 < s2.
* 0 if s1 and s2 are exactly the same binary string.
*/
int sdscmp(const sds s1, const sds s2) { /* ... */ }
/* sdssplit("foo_-_bar","_-_"); will return two
* elements "foo" and "bar".
*
* 这个函数实现比较复杂,但核心很简单,就是预分配一个数组用于存储,当数组长度不够时
* 再进行分配。比较为直接找第一个字符,当第一个字符符合时直接用memcpy比较后续数据是
* 否一致。同时内存失败的情况下需要清空已分配的内存
*/
sds *sdssplitlen(const char *s, ssize_t len, const char *sep, int seplen,
int *count) { /* ... */ }
/* 将所有p中包含的转义字符(不可打印字符)都用'//'表示出来
*/
sds sdscatrepr(sds s, const char *p, size_t len) { /* ... */ }
/* 分割输入的字符串line,可以处理引号、转义字符(包括16进制),比较复杂可以尝试实现
*/
sds *sdssplitargs(const char *line, int *argc) { /* ... */ }
/* For instance: sdsmapchars(mystring, "ho", "01", 2)
* will have the effect of turning the string "hello" into "0ell1".
*/
sds sdsmapchars(sds s, const char *from, const char *to, size_t setlen) { /* ... */ }