最近开始阅读redis的源码,记录一下,也是给自己一个坚持读下去的动力。
sds
sds(Simple Dynamic String,简单动态字符串),是redis底层使用的字符串表示,取代了C语言中默认的char *类型。与C语言的默认字符串相比,sds既可以高效地实现追加和长度计算,同时也是二进制安全的,并会在字符串末尾自动添加'\0'。
sds的实现
typedef char *sds;
struct sdshdr {
unsigned int len; //已使用的长度
unsigned int free; //剩余的长度
char buf[]; //实际保存字符串数据的地方
};
其中,sizeof(struct sdshdr)= 2*sizeof(unsigned int),因为 char buf[ ] 等价于 char buf[0], 它仅对编译器有效,并不实际占用存储。
那么,一个sds字符串申请的实际内存为:sizeof(sdshdr)+len+free+1,其中的“1”实际上是sds自动添加的字符'\0'。
基本方法
1、sdslen( )
这个方法用来获取sds字符串长度。
/* 获取sds字符串长度
*
* s存的是buf首个char数据的地址
*
* sizeof(struct sdshdr) 8个字节,free+len
*
* +--------------------------------+
* | struct sdshdr |
* +----------+----------+----------+
* | len | free | buf |
* +----------+----------+----------+
* | |
* | sds s
* |
* s-(sizeof(struct sdshdr))
*
*/
static inline size_t sdslen(const sds s) {
struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
return sh->len;
}
参数s存储的是buf[ ]的首个char数据的地址,而通过sds字符串s来获取对应的结构体sdshdr的地址的获取方法是:
struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
即将指针向前移动到 sdshdr 的起点,从而得出一个指向 sdshdr 结构的指针。
2、sdsavail( )
这个方法用来获取sds字符串可用长度。
/* 获取sds可用长度 */
static inline size_t sdsavail(const sds s) {
struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
return sh->free;
}
sds创建函数
1、sdsnewlen( )
这个方法可以创建指定长度的sds,并接受C字符串为初始化内容。
sds sdsnewlen(const void *init, size_t initlen) {
// size_t在32位架构上是4字节,在64位架构上是8字节
// 无符号数,表示0-MAXINT的范围
struct sdshdr *sh;
if (init) {
sh = zmalloc(sizeof(struct sdshdr)+initlen+1);
} else {
sh = zcalloc(sizeof(struct sdshdr)+initlen+1);
}
if (sh == NULL) return NULL;
sh->len = initlen;
sh->free = 0;
if (initlen && init)
memcpy(sh->buf, init, initlen);
sh->buf[initlen] = '\0';
return (char*)sh->buf;
}
当执行下面这行代码:
mystring = sdsnewlen("abc",3);
会在堆内存上面开辟一段连续的空间,具体的空间大小是11个字节的空间,其中的八个字节用于存放sdshdr结构体,三个字节用于存放字符串“abc”,最后的一个字节用于存放一个隐式的字符‘\0’。
另外,你可以使用printf()打印字符串,因为字符串末尾有一个隐式\0;但是字符串是二进制安全的,并且中间可以包含\0字符,因为长度存储在sds头中。
2、 sdsempty( )
这个方法用于创建一个空的sds字符串。
sds sdsempty(void) {
return sdsnewlen("",0);
}
即使在这种情况下,字符串也始终具有隐式空项 '\0'。
3、sdsnew( )
这个方法可以根据给定的C字符串创建sds字符串。
sds sdsnew(const char *init) {
size_t initlen = (init == NULL) ? 0 : strlen(init);
return sdsnewlen(init, initlen);
}
sds操作函数
1、sdsdup( )
这个方法可以复制sds字符串。
sds sdsdup(const sds s) {
return sdsnewlen(s, sdslen(s));
}
2、sdsfree( )
这个方法用于释放sds字符串,如果字符串 's' 为NULL,则不执行任何操作。
void sdsfree(sds s) {
if (s == NULL) return;
zfree(s-sizeof(struct sdshdr));
}
3、sdsupdatelen( )
这个方法用来修改字符串长度。
void sdsupdatelen(sds s) {
struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr)));
int reallen = strlen(s);
sh->free += (sh->len-reallen);
sh->len = reallen;
}
如下例所示:
s = sdsnew("foobar");
s[2] = '\0';
sdsupdatelen(s);
printf("%d\n", sdslen(s));
代码的输出将是“2”,但是如果我们注释掉对sdsupdatelen的调用,则输出将为“6”,因为字符串被修改但逻辑leng仍然是6个字节,sds字符串是二进制安全的,可以包含任何字符(字节),包括字符 ‘\0’ 。
4、sdsclear( )
这个方法用于修改sds字符串以使其为空(零长度)。
void sdsclear(sds s) {
struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr)));
sh->free += sh->len;
sh->len = 0;
sh->buf[0] = '\0';
}
但是,这个方法不会使sds字符串丢弃所有现有缓冲区,而是将其设置为可用空间,以便下一个追加操作直到先前可用的字节数前,都不需要分配。
5、sdsMakeRoomFor( )
这个方法用于对 sds 字符串 s 所对应 sdshdr 结构体的 buf 进行扩展。
sds sdsMakeRoomFor(sds s, size_t addlen) {
struct sdshdr *sh, *newsh;
size_t free = sdsavail(s);
size_t len, newlen;
if (free >= addlen) return s;
len = sdslen(s);
sh = (void*) (s-(sizeof(struct sdshdr)));
newlen = (len+addlen);
if (newlen < SDS_MAX_PREALLOC)
newlen *= 2;
else
newlen += SDS_MAX_PREALLOC;
// 在小于SDS_MAX_PREALLOC(即1M)时,会预分配出多一倍的空间,
// 在大于该阈值时,每次只预分配多SDS_MAX_PREALLOC内存。
newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1);
if (newsh == NULL) return NULL;
newsh->free = newlen - len;
return newsh->buf;
}
这个方法的目的在于放大sds字符串末尾的可用空间,即确保在调用此函数之后可以在字符串结束后覆盖 addlen 字节,再加上 '\0' 一个字节。
但是,这个方法不会更改sdslen()返回的sds字符串的 *length,而只会更改我们拥有的空闲缓冲区空间。
注意,代码中的 SDS_MAX_PREALLOC 是定义在 sds.h 中的:
#define SDS_MAX_PREALLOC (1024*1024)
其大小是1M,并根据这个值,在sds字符串扩容时进行空间预分配:
扩容过程中,如果需求长度小于1MB,则增加双倍长度,如果大约1MB,则需求长度+1MB,通过此策略,减少内存重分配次数。
if (newlen < SDS_MAX_PREALLOC)
newlen *= 2;
else
newlen += SDS_MAX_PREALLOC;
6、sdsAllocSize( )
这个方法返回指定的sds字符串分配的总大小。
size_t sdsAllocSize(sds s) {
struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr)));
return sizeof(*sh)+sh->len+sh->free+1;
}
返回值的大小,包括:
- 1)指针前的sds头。
- 2)字符串。
- 3)最后的空闲缓冲区(如果有的话)。
- 4)隐式空项(‘\0’)。
7、sdsIncrLen( )
这个方法用于对给定 sds 字符串的 buf 的右端进行扩展或缩小。
void sdsIncrLen(sds s, int incr) {
struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr)));
if (incr >= 0) //增加字符串长度
assert(sh->free >= (unsigned int)incr);
// assert(): 其作用是如果它的条件返回错误,则终止程序执行
else //减少字符串长度
assert(sh->len >= (unsigned int)(-incr));
sh->len += incr;
sh->free -= incr;
s[sh->len] = '\0';
}
它根据 incr 的大小增加 sds 字符串的长度并减少字符串末尾的左边空闲空间,同时,在字符串的新结尾设置 '\0'。
8、sdsgrowzero( )
这个方法可以使 sds 字符串增长到指定的长度,并将不属于 sds 字符串原始长度的字节将设置为零。
sds sdsgrowzero(sds s, size_t len) {
struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
size_t totlen, curlen = sh->len;
if (len <= curlen) return s;
s = sdsMakeRoomFor(s,len-curlen);
if (s == NULL) return NULL;
/* Make sure added region doesn't contain garbage */
sh = (void*)(s-(sizeof(struct sdshdr)));
memset(s+curlen,0,(len-curlen+1)); /* also set trailing \0 byte */
totlen = sh->len+sh->free;
sh->len = len;
sh->free = totlen-sh->len;
return s;
}