操纵字符串(string)对于大多数的程序,都是一项主要工作。但是 c 语言中没有内置 string 类型,所以 redis 封装了自己的 string 类型 sds。今天我们就看一下 redis 中的 string 是如何实现的。
其对应的代码在 src/sds.h 和 src/sds.c 中。
柔性数组
在分析 redis 的字符串实现前,我们先岔开话题聊一下柔性数组。
柔性数组或者叫做 Arrays of Length Zero 是对标准 c 的一个扩展。其允许我们使用如下的结构体定义:
struct S {
int a;
char data[]; // 也可以写为 char data[]; 注意并不是只能用于 char 数组
};
printf("%lu", sizeof(struct S)); // 输出 4 而不是 8
data 往往是需要动态分配的内存(长度不固定),在构造一个如上的结构体时,一般使用如下代码:
int datalen = get_datalen();
struct S *s = malloc(sizeof(*s) + datalen);
分配后内存的布局如下:
|-----s----|---data----|
|<-4bytes->|<-datalen->|
可以看到这个声明就好像 struct S 的 data 是一个 datalen 的字符数组,如果我们不使用柔性数组想达到这个效果,就不得不使用如下的定义:
#define MAX_LENGTH 1024 // max length of data
struct S {
int a;
char data[MAX_LENGTH];
};
这会导致内存的浪费。
柔性数组多用于替代类似下面的定义方式:
struct S {
int a;
char *data;
};
printf("%lu", sizeof(struct S)); // 8 or 16
其主要的优点有:
- 节省内存,没有额外的指针开销(从结构体的 size 可以看出)
- 避免通过指针的间接访问,提高访问效率(第二种使用指针的要多寻址一次)
缺点有:
- 只能声明在结构体的最后,所以一个结构体内只能使用一个
- 在 c++ 的类继承体系中会有问题(使用这种技术的结构体不能作为基类)
sds header
struct sdshdr {
// 数据占用的 buf 长度,不包括 \0
int len;
// buf 内剩余可用空间
int free;
// 柔性数组,存放真正的数据
char buf[];
};
typedef char *sds;
redis 中的 sds 就使用柔性数组来实现。这也是字符串常见的一种实现方式。
构造
sds sdsnewlen(const void *init, size_t initlen) {
struct sdshdr *sh;
// 根据 initlen 决定到底需要申请多少内存,注意 +1(用于\0)
if (init) {
sh = zmalloc(sizeof(struct sdshdr)+initlen+1)
} else {
// 不需要拷贝数据的话,直接将新申请的内存清零
sh = zcalloc(sizeof(struct sdshdr)+initlen+1)
}
if (sh == NULL) return NULL;
// 新构造的字符串没有空闲空间
// sh->len + sh->free == len of buf
sh->len = initlen;
sh->free = 0;
if (initlen && init) {
// 如果传入了 init data,进行拷贝
memcpy(sh->buf, init, initlen);
}
// 字符串以 \0 结尾
sh->buf[initlen] = '\0';
// 返回字符串数组地址
return (char*)sh->buf;
}
sdsnewlen 是最基础的构造函数,redis 还提供了类似
sds sdsdup(const sds s);
sds sdsnew(const char *init);
sds sdsempty();
等接口,都会转调用 sdsnewlen。
需要注意的是,构造好 sdshdr 结构体以后,我们并没有返回这个结构体,而是返回了 buf 的地址,后续所有的操作都是直接操作这个字符串数组,而不是 sdshdr。
析构
void sdsfree(sds s) {
if (s == NULL) return;
// s-sizeof(struct sdshdr) 计算出对应的 sdshdr 的地址
zfree(s-sizeof(struct sdshdr));
}
因为释放内存的时候,我们需要释放整个 sdshdr ,所以我们需要计算对应 sdshdr 的地址。s - sizeof(struct sdshdr) 在后面的接口实现中会非常常见。这里大家可以想一下,如果 buf 是 char *,这种计算方式还可以 work 吗?如果不能的话,要怎么求 sdshdr 的地址?
sdscat
字符串的拼接是最常见的任务之一。让我们看一下 redis 内的实现
size_t sdsavail(const sds s) {
struct sdshdr *sh = (void*)(s-sizeof(struct sdshdr));
return sh->free;
}
size_t sdslen(const sds s) {
struct sdshdr *sh = (void*)(s-sizeof(struct sdshdr));
return sh->len;
}
sds sdsMaekRoomFor(sds s, size_t addlen) {
struct sdshdr *sh, *newsh;
size_t free, len, newlen;
free = sdsavail(s);
// 剩余空闲空间足够,直接返回
if (free >= addlen) return s;
// 计算需要的内存大小
len = sdslen(s);
sh = (vodi*)(s-sizeof(struct sdshdr));
newlen = len + sh->addlen;
if (newlen < SDS_MAX_PREALLOC)
newlen *= 2;
else
newlen += SDS_MAX_PREALLOC;
// 申请新 hdr
newsh = zrealloc(sh, sizeof(struct sdshdr) + newlen+1);
if (newsh == NULL) return NULL;
newsh->free = newlen - len;
return (char*)newsh->buf;
}
sds sdscatlen(sds s, const void *t, size_t len) {
struct sdshdr *sh;
size_t curlen = sdslen(s);
// 确保有足够的内存
s = sdsMakeRoomFor(s, len);
if (s == NULL) return NULL;
sh = (void*)(s-sizeof(struct sdshdr));
memcpy(s+curllen, t, len);
// 更新 len、free
sh->len = curlen+len;
sh->free -= len;
return s;
}
sds sdscat(sds s, const char *t) {
return sdscatlen(s, t, strlen(t));
}
sds sdscatsds(sds s, const sds t) {
return sdscatlen(s, t, sdslen(t));
}
拼接的内容也比较直观,如果可用内存不够的话,使用 zrealloc 重新申请内存即可。之后只需要将添加的数据拷贝到 buf 即可。
总结
redis 的字符串相关的接口实现还是比较直观的简单的。比较值得学习的是使用了柔性数组来存储字符串。