1. 前言
本文的内容是基于 redis-7.0.0
的源码,具体的源码可以参阅 redis 7.0.0。本文也不会对源文件 sds.h/sds.c
中所有的源码进行解释,只会讲解其中的主干部分,如果有兴趣可以参阅 sds.h 和 sds.c。
2. C 语言中的字符串
在 c 语言中字符串常量的形式如下:
"redis"
虽然字符串的内容有了,但是不确定的是字符串的长度。所以字符串常量在内存中的存储会在字符串末尾添加字符 '\0'
作为结尾。其内存结构示意图如下所示:
在大多数情况下,我们会通过 char *
类型的指针来操作字符串常量,定义该类型的指针 c
来间接访问字符串常量:
char *c = "redis";
其内存示意图如下:
C 语言中的字符串有如下几个缺陷:
- 计算字符串长度时,比如 strlen() 函数的时间复杂度为
O(n)
。 - 因为使用
'\0'
在内存中作为字符串的结尾,也就是说判断是否到达字符串末尾,是通过按照顺序扫描字符串中的每一个字符是否为'\0'
来判断的。如果字符串本身就有 '\0' 字符,那么这样就会使得字符串"提前终止",比如计算字符串长度的strlen()
函数或者打印字符串printf()
函数。这种规则也使得 C 语言中的字符串是二进制不安全的(关于二进制安全可以参阅维基百科词条 Binary-safe)。 - 对 C 语言中的字符串进行拼接,每次都需要对字符串进行内存再分配。
3. Redis 中的 string
3.1 定义
redis 中的 string,定义了五种类型的 string,分别为 sdshdr5
,sdshdr8
,sdshdr16
,sdshdr32
和 sdshdr64
,其中 sdshdr5
不被使用。源代码如下:
sds.h
/* 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.h
//定义 redis string 的类型,对应于 `sdshdr5`,`sdshdr8`,
//`sdshdr16`,`sdshdr32` 和 `sdshdr64。
#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
除 sdshdr5
之外,其余四种 sdshdr
类型的 string 都有 len
,alloc
,flags
和 buf
字段。
- len 代表
buf
中字符串的大小。 - alloc 代表为
buf
分配的内存大小。 - flags 记录当前 string 的
SDS_TYPE
。 - buf 代表真实存储的字符串(buf 指针就可以看作指向 C 语言中的字符串
char *
指针)
sdshdr 是通过设计不同 sdshdr 类型来表示不同大小的字符串,并使用 attribute ((packed))这个编程小技巧,来实现紧凑型内存布局,达到节省内存的目的。
其中类型为 sdshdr32 的字符串的内存布局如下:
- 计算 string 长度的时间复杂度为 O(1)
- 二进制安全,访问字符串不依赖于 buf 数组内容(不包括最后结尾添加的
'\0'
)中是否存在'\0'
,而是根据len
字段判断是否达到字符串末尾。 - 在一些字符串操作情况下,不必重新分配内存空间。
3.2 创建一个 redis string
创建 redis string 的源码如下:
定于类型别名 sds
:
typedef char *sds;
/* 使用 init 指针 和 initlen 创建一个新的 sds 字符串。
* 如果 init 被设置成 NULL,则字符串将会被初始化为零字节。
* 如果使用 SDS_NOINIT,则缓冲区则保持未初始化状态。
* 字符串始终以空字符结尾,即使您使用以下方式创建了一个sds字符串:
* mystring = sdsnewlen("abc",3);
* 你可以使用 printf() 函数打印该字符串,因为该字符串有一个隐式的 '\0'。
* 但是该字符串是而禁止安全的,可以在字符中间包含 '\0' 字符,因为长度存储在 len 字段中。 */
/* Create a new sds string with the content specified by the 'init' pointer
* and 'initlen'.
* If NULL is used for 'init' the string is initialized with zero bytes.
* If SDS_NOINIT is used, the buffer is left uninitialized;
*
* The string is always null-terminated (all the sds strings are, always) so
* even if you create an sds string with:
*
* mystring = sdsnewlen("abc",3);
*
* You can print the string with printf() as there is an implicit \0 at the
* end of the string. However the string is binary safe and can contain
* \0 characters in the middle, as the length is stored in the sds header. */
sds _sdsnewlen(const void *init, size_t initlen, int trymalloc) {
void *sh;
sds s;
// 根据 initlen 确定 sdshdr 的类型
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;
}
该函数执行逻辑如下:
- 根据 initlen 确定 sdshdr 类型。
- 根据 sdshdr 类型计算 hdrlen。这里需要注意的是 hdrlen 并不包括
buf
字段的大小,只包括len + alloc + flasg
的字段大小。C99 中引入了可变长数组,可以参阅 Array of variable length in a structure。 - 分配内存空间,大小为 initlen + hdrlen + 1。
- 根据 sdshdr 类型 确定 flags 字段的值。
- 根据 initlen 确定 len 字段的值。
- 根据 useable 的值确认 alloc 字段的值。
- buf 字段作为函数的返回值,返回类型为 sds。
这里需要注意的是 _sdsnewlen
返回的类型为 sds(char *),该指针值为 sdshdr 中的 buf 字段。这就意味着你可以和使用 C 语言中的字符串的方式来使用 redis string。那么我该如何通过 sds 来得到它相对应的 sdshdr 对象的首地址呢?答案是通过计算偏移量。
定义宏,计算对应的 sdrhds 对象的首地址。
//返回一个匿名的 sdshdr##T 指针,指向 sds 所对应的 sdshdr##T 对象。
#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))))
定义一个类型为 sdshdr##T *的指针,指向 sds 所对应的 sdshdr##T 对象。
#define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T)));
3.3 主要的方法
3.3.1 sdsIncrLen
void sdsIncrLen(sds s, ssize_t incr)
/* Increment the sds length and decrements the left free space at the
* end of the string according to 'incr'. Also set the null term
* in the new end of the string.
*
* This function is used in order to fix the string length after the
* user calls sdsMakeRoomFor(), writes something after the end of
* the current string, and finally needs to set the new length.
*
* Note: it is possible to use a negative increment in order to
* right-trim the string.
*
* Usage example:
*
* Using sdsIncrLen() and sdsMakeRoomFor() it is possible to mount the
* following schema, to cat bytes coming from the kernel to the end of an
* sds string without copying into an intermediate buffer:
*
* 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);
*/
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;
}
case SDS_TYPE_16: {
SDS_HDR_VAR(16,s);
assert((incr >= 0 && sh->alloc-sh->len >= incr) || (incr < 0 && sh->len >= (unsigned int)(-incr)));
len = (sh->len += incr);
break;
}
case SDS_TYPE_32: {
SDS_HDR_VAR(32,s);
assert((incr >= 0 && sh->alloc-sh->len >= (unsigned int)incr) || (incr < 0 && sh->len >= (unsigned int)(-incr)));
len = (sh->len += incr);
break;
}
case SDS_TYPE_64: {
SDS_HDR_VAR(64,s);
assert((incr >= 0 && sh->alloc-sh->len >= (uint64_t)incr) || (incr < 0 && sh->len >= (uint64_t)(-incr)));
len = (sh->len += incr);
break;
}
default: len = 0; /* Just to avoid compilation warnings. */
}
s[len] = '\0';
}
3.3.3 sds _sdsMakeRoomFor(sds s, size_t addlen, int greedy)
sds _sdsMakeRoomFor(sds s, size_t addlen, int greedy) 扩大字符串的空间,如果空闲空间的大小 >= addlen, 则不进行重新分配的操作,直接返回。
/* 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.
* If there's already sufficient free space, this function returns without any
* action, if there isn't sufficient free space, it'll allocate what's missing,
* and possibly more:
* When greedy is 1, enlarge more than needed, to avoid need for future reallocs
* on incremental growth.
* When greedy is 0, enlarge just enough so that there's free space for 'addlen'.
*
* 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, int greedy) {
void *sh, *newsh;
size_t avail = sdsavail(s);
size_t len, newlen, reqlen;
//获得之前的 sdshdr 类型
char type, oldtype = s[-1] & SDS_TYPE_MASK;
int hdrlen;
size_t usable;
/* 如果剩余空间 >= addlen 直接返回*/
if (avail >= addlen) return s;
len = sdslen(s);
sh = (char*)s-sdsHdrSize(oldtype);
/* 计算新的字符串长度 */
reqlen = newlen = (len+addlen);
assert(newlen > len); /* Catch size_t overflow */
/* 参数 greedy == 1,多分配空间 */
if (greedy == 1) {
if (newlen < SDS_MAX_PREALLOC)
newlen *= 2;
else
newlen += SDS_MAX_PREALLOC;
}
//根据新字符串长度来得到对应的新的 sdshdr type
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;
//获取头部字段长度 len + alloc + flags
hdrlen = sdsHdrSize(type);
assert(hdrlen + newlen + 1 > reqlen); /* Catch size_t overflow */
//如果新的 sdshdr type 和旧的一样
if (oldtype==type) {
newsh = s_realloc_usable(sh, hdrlen+newlen+1, &usable);
if (newsh == NULL) return NULL;
s = (char*)newsh+hdrlen;
} else {
//如果新的 sdshdr type 和旧的不一样
//需要拷贝字符串
/* 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);
//设置 alloc 字段的值
sdssetalloc(s, usable);
return s;
}
4.总结
- 计算 string 长度的时间复杂度为 O(1)
- 二进制安全,访问字符串不依赖于 buf 数组内容(不包括最后结尾添加的
'\0'
)中是否存在'\0'
,而是根据len
字段判断是否达到字符串末尾。 - 在一些字符串操作情况下,不必重新分配内存空间。
- sdshdr 是通过设计不同 sdshdr 类型来表示不同大小的字符串,并使用 attribute ((packed))这个编程小技巧,来实现紧凑型内存布局,达到节省内存的目的。