简单动态字符串(Simple Dynamic Strings, SDS)是Redis的基本数据结构之一,用于存储字符串和整型数据。SDS兼容C语言标准字符串处理函数,且在此基础上保证了二进制安全。本章将详细讲解SDS的实现,为读者理解Redis的原理和各种命令的实现打下基础。
数据结构
在学习SDS源码前,我们先思考一个问题:如何实现一个扩容方便且二进制安全的字符串呢?
注意:什么是二进制安全?通俗地讲,C语言中,用“\0”表示字符串的结束,如果字符串中本身就有“\0”字符,字符串就会被截断,即非二进制安全;若通过某种机制,保证读写字符串时不损害其内容,则是二进制安全。
SDS既然是字符串,那么首先需要一个字符串指针;为了方便上层的接口调用,该结构还需要记录一些统计信息,如当前数据长度和剩余容量等,例如:
struct sds {
int len; // buf中已占用字节数
int free; // buf中剩余可用字节数
char buf[]; // 数据空间
};
SDS结构示意如图2-1所示,在64位系统下,字段len和字段free各占4个字节,紧接着存放字符串。
Redis 3.2之前的SDS也是这样设计的。这样设计有以下几个优点。
- 有单独的统计变量len和free(称为头部)。可以很方便地得到字符串长度。
- 内容存放在柔性数组buf中,SDS对上层暴露的指针不是指向结构体SDS的指针,而是直接指向柔性数组buf的指针。上层可像读取C字符串一样读取SDS的内容,兼容C语言处理字符串的各种函数。
- 由于有长度统计变量len的存在,读写字符串时不依赖“\0”终止符,保证了二进制安全。
注意:上例中的buf[]是一个柔性数组。柔性数组成员(flexible array member),也叫伸缩性数组成员,只能被放在结构体的末尾。包含柔性数组成员的结构体,通过malloc函数为柔性数组动态分配内存。
之所以用柔性数组存放字符串,是因为柔性数组的地址和结构体是连续的,这样查找内存更快(因为不需要额外通过指针找到字符串的位置);可以很方便地通过柔性数组的首地址偏移得到结构体首地址,进而能很方便地获取其余变量。
到这里我们实现了一个最基本的动态字符串,但是该结构是否有改进的空间呢?我们从一个简单的问题开始思考:不同长度的字符串是否有必要占用相同大小的头部?一个int占4字节,在实际应用中,存放于Redis中的字符串往往没有这么长,每个字符串都用4字节存储未免太浪费空间了。我们考虑三种情况:短字符串,len和free的长度为1字节就够了;长字符串,用2字节或4字节;更长的字符串,用8字节。
这样确实更省内存,但依然存在以下问题。
- 问题1:如何区分这3种情况?
- 问题2:对于短字符串来说,头部还是太长了。以长度为1字节的字符串为例,len和free本身就占了2个字节,能不能进一步压缩呢?
对于问题1,我们考虑增加一个字段flags来标识类型,用最小的1字节来存储,且把flags加在柔性数组buf之前,这样虽然多了1字节,但通过偏移柔性数组的指针即能快速定位flags,区分类型,也可以接受;对于问题2,由于len已经是最小的1字节了,再压缩只能考虑用位来存储长度了。结合两个问题,5种类型(长度1字节、2字节、4字节、8字节、小于1字节)的SDS至少要用3位来存储类型(23=8),1个字节8位,剩余的5位存储长度,可以满足长度小于32的短字符串。在Redis 7.2中,我们用如下结构来存储长度小于32的短字符串:
struct __attribute__ ((__packed__)) sdshdr5 {
// 是一个无符号字符变量,用于存储标志位。
// 其中低3位表示类型,高5位表示字符串长度
unsigned char flags;
// 一个字符数组,用于存储字符串的实际内容。
char buf[];
};
sdshdr5结构中,flags占1个字节,其低3位(bit)表示type,高5位(bit)表示长度,能表示的长度区间为0~31(-1), flags后面就是字符串的内容。
而长度大于31的字符串,1个字节依然存不下。我们按之前的思路,将len和free单独存放。sdshdr8、sdshdr16、sdshdr32和sdshdr64的结构相同,sdshdr16结构如下:
其中“表头”共占用了S[2(len)+2(alloc)+1(flags)]个字节。flags的内容与sdshdr5类似,依然采用3位存储类型,但剩余5位不存储长度。在Redis的源代码中,对类型的宏定义如下:
#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
在Redis 7中,sdshdr8、sdshdr16、sdshdr32和sdshdr64的数据结构如下:
// 结构体使用了__attribute__ ((packed))属性,表示按照原样打包存储,不进行自动的内存对齐
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* 用于存储数据的长度,用1个字节存储 */
uint8_t alloc; /* 用于存储数据的分配长度,不包括头部和终止符,用1个字节存储 */
unsigned char flags; /* 前3位用于表示类型,后5位未使用 */
char buf[]; // 用于存储数据的实际内容
};
struct __attribute__ ((__packed__)) sdsherr16 {
uint16_t len; /* 用于存储字符串的长度,用2个字节存储 */
uint16_t alloc; /* 用于存储字符串的分配长度,不包括头部和终止符,用2个字节存储 */
unsigned char flags; /* 字符串类型的3个最低有效位,5个未使用位 */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
uint32_t len; /* 用于存储字符串的长度,用4个字节存储 */
uint32_t alloc; /* 用于存储字符串的分配长度,不包括头部和终止符,用4个字节存储 */
unsigned char flags; /* 字符串类型的3个最低有效位,5个未使用位 */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
uint64_t len; /* 用于存储字符串的长度,用8个字节存储 */
uint64_t alloc; /* 用于存储字符串的分配长度,不包括头部和终止符,用8个字节存储 */
unsigned char flags; /* 字符串类型的3个最低有效位,5个未使用位 */
char buf[];
};
可以看到,这4种结构的成员变量类似,唯一的区别是len和alloc的类型不同。注意:结构最后的buf依然是柔性数组,通过对数组指针作“减一”操作,能方便地定位到flags。
源码中的__attribute__((__packed__))需要重点关注。一般情况下,结构体会按其所有变量大小的最小公倍数做字节对齐,而用packed修饰后,结构体则变为按1字节对齐。以sdshdr32为例,修饰前按4字节对齐大小为12(4×3)字节;修饰后按1字节对齐,注意buf是个char类型的柔性数组,地址连续,始终在flags之后。packed修饰前后示意如下图所示:
这样做有以下两个好处。
- 节省内存,例如sdshdr32可节省3个字节(12-9)。
- SDS返回给上层的,不是结构体首地址,而是指向内容的buf指针。因为此时按1字节对齐,故SDS创建成功后,无论是sdshdr8、sdshdr16还是sdshdr32,都能通过(char*)sh+hdrlen得到buf指针地址(其中hdrlen是结构体长度,通过sizeof计算得到)。修饰后,无论是sdshdr8、sdshdr16还是sdshdr32,都能通过buf[-1]找到flags,因为此时按1字节对齐。若没有packed的修饰,还需要对不同结构进行处理,实现更复杂。
基本操作
数据结构的基本操作不外乎增、删、改、查,SDS也不例外。由于Redis 3.2后的SDS涉及多种类型,修改字符串内容带来的长度变化可能会影响SDS的类型而引发扩容。本节着重介绍创建、释放、拼接字符串的相关API,帮助大家更好地理解SDS结构。
创建字符串
Redis通过sdsnewlen函数创建SDS。在函数中会根据字符串长度选择合适的类型,初始化完相应的统计值后,返回指向字符串内容的指针,根据字符串长度选择不同的类型:
/*
* 根据指定的'init'指针和'initlen'创建一个新的sds字符串。
* 如果'init'为NULL,则字符串初始化为零字节。
* 如果使用了SDS_NOINIT,则缓冲区保持未初始化状态;
*
* 所有的sds字符串都是尾部定界符,因此即使您使用以下方式创建sds字符串:
*
* mystring = sdsnewlen("abc",3);
*
* 您仍然可以使用printf()函数来打印字符串,因为字符串是隐式地以'\0'结尾的。
* 但是,该字符串是二进制安全的,可以包含中间的'\0'字符,因为长度存储在sds头中。
*/
sds _sdsnewlen(const void *init, size_t initlen, int trymalloc) {
void *sh; // sds字符串头指针
sds s; // sds字符串指针
char type = sdsReqType(initlen); // 根据字符串长度确定sds字符串类型
/* 通常,空字符串用于追加。由于类型5在这种情况下表现不佳,因此如果类型为5,则使用类型8。 */
if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
int hdrlen = sdsHdrSize(type); // 头的长度
unsigned char *fp; /* flags指针。 */
size_t usable; // 可用的字节数
assert(initlen + hdrlen + 1 > initlen); /* 捕获size_t溢出 */
sh = trymalloc?
s_trymalloc_usable(hdrlen+initlen+1, &usable) :
s_malloc_usable(hdrlen+initlen+1, &usable); // 分配sds字符串头和有效载荷的内存
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指针
usable = usable-hdrlen-1;
if (usable > sdsTypeMaxSize(type))
usable = sdsTypeMaxSize(type); // 根据类型调整可用字节数
switch(type) {
case SDS_TYPE_5: {
*fp = type | (initlen << SDS_TYPE_BITS); // 根据类型和长度设置flags字段
break;
}
case SDS_TYPE_8: {
SDS_HDR_VAR(8,s);
sh->len = initlen; // 设置字符串长度
sh->alloc = usable; // 设置字符串可分配的字节数
*fp = type; // 设置flags字段
break;
}
case SDS_TYPE_16: {
SDS_HDR_VAR(16,s);
sh->len = initlen; // 设置字符串长度
sh->alloc = usable; // 设置字符串可分配的字节数
*fp = type; // 设置flags字段
break;
}
case SDS_TYPE_32: {
SDS_HDR_VAR(32,s);
sh->len = initlen; // 设置字符串长度
sh->alloc = usable; // 设置字符串可分配的字节数
*fp = type; // 设置flags字段
break;
}
case SDS_TYPE_64: {
SDS_HDR_VAR(64,s);
sh->len = initlen; // 设置字符串长度
sh->alloc = usable; // 设置字符串可分配的字节数
*fp = type; // 设置flags字段
break;
}
}
if (initlen && init)
memcpy(s, init, initlen); // 将数据复制到有效载荷
s[initlen] = '\0'; // 在尾部添加字符串定界符
return s; // 返回创建的sds字符串指针
}
sds sdsnewlen(const void *init, size_t initlen) {
return _sdsnewlen(init, initlen, 0);
}
注意:Redis 3.2后的SDS结构由1种增至5种,且对于sdshdr5类型,在创建空字符串时会强制转换为sdshdr8。原因可能是创建空字符串后,其内容可能会频繁更新而引发扩容,故创建时直接创建为sdshdr8。
创建SDS的大致流程:首先计算好不同类型的头部和初始长度,然后动态分配内存。需要注意以下3点。
- 创建空字符串时,SDS_TYPE_5被强制转换为SDS_TYPE_8。
- 长度计算时有“+1”操作,是为了算上结束符“\0”。
- 返回值是指向sds结构buf字段的指针。
返回值sds的类型定义如下:
typedef char *sds;
从源码中我们可以看到,其实s就是一个字符数组的指针,即结构中的buf。这样设计的好处在于直接对上层提供了字符串内容指针,兼容了部分C函数,且通过偏移能迅速定位到SDS结构体的各处成员变量。
SDS数据长度
SDS提供了直接获取数据长度的方法——sdslen,该方法通过对SDS的偏移,先获取SDS的类型,然后再偏移定位到SDS结构的首部,然后获取len:
// 定义宏SDS_HDR,用于获取指定类型sdshdr结构体的头指针
// 参数T为指定的sdshdr结构体类型后缀,s为结构体起始地址
// 返回值为指向指定类型sdshdr结构体的头指针
#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))))
// 定义宏SDS_TYPE_5_LEN,用于获取字段f对应的类型5 sdshdr结构体长度
// 参数f为给定的字段值
// 返回值为字段f对应的类型5 sdshdr结构体长度
#define SDS_TYPE_5_LEN(f) ((f)>>SDS_TYPE_BITS)
static inline size_t sdslen(const sds s) {
unsigned char flags = s[-1];
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; // 返回8字节类型SDS的数据长度
case SDS_TYPE_16:
return SDS_HDR(16,s)->len; // 返回16字节类型SDS的数据长度
case SDS_TYPE_32:
return SDS_HDR(32,s)->len; // 返回32字节类型SDS的数据长度
case SDS_TYPE_64:
return SDS_HDR(64,s)->len; // 返回64字节类型SDS的数据长度
}
return 0; // 如果SDS类型不可识别,则返回0
}
SDS提供了直接设置数据长度的方法——sdssetlen,该方法通过对SDS的偏移,先获取SDS的类型,然后再偏移定位到SDS结构的首部,然后设置len:
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
*fp = SDS_TYPE_5 | (newlen << SDS_TYPE_BITS); // 更新标志字符
}
break;
case SDS_TYPE_8:
SDS_HDR(8,s)->len = newlen; // 更新字节长度为 8 的字符串的数据长度
break;
case SDS_TYPE_16:
SDS_HDR(16,s)->len = newlen; // 更新字节长度为 16 的字符串的数据长度
break;
case SDS_TYPE_32:
SDS_HDR(32,s)->len = newlen; // 更新字节长度为 32 的字符串的数据长度
break;
case SDS_TYPE_64:
SDS_HDR(64,s)->len = newlen; // 更新字节长度为 64 的字符串的数据长度
break;
}
}
SDS提供了直接增加数据长度的方法——sdsinclen,该方法通过对SDS的偏移,先获取SDS的类型,然后再偏移定位到SDS结构的首部,然后设置len:
static inline void sdsinclen(sds s, size_t inc) {
unsigned char flags = s[-1]; // 获取字符串的标志位
switch(flags&SDS_TYPE_MASK) { // 根据标志位判断字符串的类型
case SDS_TYPE_5:
{
unsigned char *fp = ((unsigned char*)s)-1; // 字符串指针的偏移量
unsigned char newlen = SDS_TYPE_5_LEN(flags)+inc; // 计算新的长度
*fp = SDS_TYPE_5 | (newlen << SDS_TYPE_BITS); // 更新字符串指针的偏移量
}
break;
case SDS_TYPE_8:
SDS_HDR(8,s)->len += inc; // 更新8字节类型字符串的长度字段
break;
case SDS_TYPE_16:
SDS_HDR(16,s)->len += inc; // 更新16字节类型字符串的长度字段
break;
case SDS_TYPE_32:
SDS_HDR(32,s)->len += inc; // 更新32字节类型字符串的长度字段
break;
case SDS_TYPE_64:
SDS_HDR(64,s)->len += inc; // 更新64字节类型字符串的长度字段
break;
}
}
SDS分配内存大小
SDS提供了直接获取数据长度的方法——sdsalloc,该方法通过对SDS的偏移,先获取SDS的类型,然后再偏移定位到SDS结构的首部,然后获取alloc:
/* sdsalloc()函数用于计算sds对象的分配内存大小,其值等于sds对象的可用内存大小和sds对象的长度之和 */
static inline size_t sdsalloc(const sds s) {
unsigned char flags = s[-1]; /* 获取s对象的标志字符 */
switch(flags&SDS_TYPE_MASK) { /* 根据标志字符和SDS_TYPE_MASK的按位与结果进行判断 */
case SDS_TYPE_5:
return SDS_TYPE_5_LEN(flags); /* 调用SDS_TYPE_5_LEN函数返回标志字符的长度 */
case SDS_TYPE_8:
return SDS_HDR(8,s)->alloc; /* 调用SDS_HDR函数返回SDS_HDR(8,s)结构体的alloc成员 */
case SDS_TYPE_16:
return SDS_HDR(16,s)->alloc; /* 调用SDS_HDR函数返回SDS_HDR(16,s)结构体的alloc成员 */
case SDS_TYPE_32:
return SDS_HDR(32,s)->alloc; /* 调用SDS_HDR函数返回SDS_HDR(32,s)结构体的alloc成员 */
case SDS_TYPE_64:
return SDS_HDR(64,s)->alloc; /* 调用SDS_HDR函数返回SDS_HDR(64,s)结构体的alloc成员 */
}
return 0; /* 如果标志字符不匹配任何类型,则返回0 */
}
SDS提供了直接设置数据长度的方法——sdsalloc,该方法通过对SDS的偏移,先获取SDS的类型,然后再偏移定位到SDS结构的首部,然后设置alloc:
static inline void sdssetalloc(sds s, size_t newlen) {
unsigned char flags = s[-1];
switch(flags&SDS_TYPE_MASK) {
case SDS_TYPE_5:
/* 无需处理,此类型没有总分配信息。 */
break;
case SDS_TYPE_8:
SDS_HDR(8,s)->alloc = newlen; // 更新指定sds类型的总分配长度为newlen。
break;
case SDS_TYPE_16:
SDS_HDR(16,s)->alloc = newlen; // 更新指定sds类型的总分配长度为newlen。
break;
case SDS_TYPE_32:
SDS_HDR(32,s)->alloc = newlen; // 更新指定sds类型的总分配长度为newlen。
break;
case SDS_TYPE_64:
SDS_HDR(64,s)->alloc = newlen; // 更新指定sds类型的总分配长度为newlen。
break;
}
}
释放SDS
SDS提供了直接释放内存的方法——sdsfree,该方法通过对s的偏移,可定位到SDS结构体的首部,然后调用s_free释放内存:
/*
* 释放sds字符串,如果's'为NULL则不执行任何操作。
*/
void sdsfree(sds s) {
if (s == NULL) return;
s_free((char*)s-sdsHdrSize(s[-1]);
}
为了优化性能(减少申请内存的开销), SDS提供了不直接释放内存,而是通过重置统计值达到清空目的的方法——sdsclear。该方法仅将SDS的len归零,此处已存在的buf并没有真正被清除,新的数据可以覆盖写,而不用重新申请内存。
/* 将一个 sds 字符串内部修改为空(长度为零)。
* 但是,现有的缓冲区不会被丢弃,而是被标记为可用空间,
* 这样下次追加操作就无需分配之前可用字节数目的内存了。 */
void sdsclear(sds s) {
sdssetlen(s, 0);
s[0] = '\0';
}
拼接SDS数据
拼接字符串操作本身不复杂,可用sdscatsds来实现,代码如下:
/* 将指定的sds字符串't'追加到已存在的sds字符串's'。
*
* 在调用后,修改后的sds字符串不再有效,所有引用必须被替换为调用返回的新指针。 */
sds sdscatsds(sds s, const sds t) {
return sdscatlen(s, t, sdslen(t));
}
sdscatsds是暴露给上层的方法,其最终调用的是sdscatlen。由于其中可能涉及SDS的扩容,sdscatlen中调用sdsMakeRoomFor对带拼接的字符串s容量做检查,若无须扩容则直接返回s;若需要扩容,则返回扩容好的新字符串s。函数中的len、curlen等长度值是不含结束符的,而拼接时用memcpy将两个字符串拼接在一起,指定了相关长度,故该过程保证了二进制安全。最后需要加上结束符。
/**
* 将指定的二进制安全字符串指针't'中的'len'字节追加到指定的sds字符串's'末尾。
* 在调用后,传递的sds字符串不再有效,所有引用必须被替换为调用返回的新指针。
*
* @param s 要追加的SDS字符串
* @param t 要追加的字符串
* @param len 追加字符串的长度
* @return 追加后的新的SDS字符串,若内存分配失败则返回NULL
*/
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;
}
下图描述了sdsMakeRoomFor的实现过程。
Redis的sds中有如下扩容策略。
1)若sds中剩余空闲长度avail大于新增内容的长度addlen,直接在柔性数组buf末尾追加即可,无须扩容。
2)若sds中剩余空闲长度avail小于或等于新增内容的长度addlen,则分情况讨论:新增后总长度len+addlen<1MB的,按新长度的2倍扩容;新增后总长度len+addlen>1MB的,按新长度加上1MB扩容。
3)最后根据新长度重新选取存储类型,并分配空间。此处若无须更改类型,通过realloc扩大柔性数组即可;否则需要重新开辟内存,并将原字符串的buf内容移动到新位置。
/* 为sds字符串末端的空闲空间扩大足够大的长度,以确保调用此函数的用户能够覆盖字符串末尾之后至多addlen个字节,外加一个字节的空位。
* 如果已经存在足够的空闲空间,则此函数无需任何操作;如果没有足够的空闲空间,则会分配所需的空间,甚至更多:
* 当greedy为1时,扩大所需空间以上的大小,以避免未来重新分配操作的增量增长。
* 当greedy为0时,只扩大恰好足够的长度,以确保有'addlen'个字节的空闲空间。
*
* 注意:这不会改变sds字符串的*长度*,如sdslen()函数返回的长度,但只会改变我们所具有的空闲缓冲区空间。 */
sds _sdsMakeRoomFor(sds s, size_t addlen, int greedy) {
void *sh, *newsh;
size_t avail = sdsavail(s);
size_t len, newlen, reqlen;
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);
reqlen = newlen = (len + addlen);
assert(newlen > len); /* 捕获size_t溢出 */
if (greedy == 1) {
if (newlen < SDS_MAX_PREALLOC)
newlen *= 2;
else
newlen += SDS_MAX_PREALLOC;
}
type = sdsReqType(newlen); // 根据扩容后的长度计算类型
/* 不使用类型5:用户正在追加到字符串,而类型5无法记住空闲空间,因此必须在每次追加操作时调用sdsMakeRoomFor()。 */
if (type == SDS_TYPE_5) type = SDS_TYPE_8;
hdrlen = sdsHdrSize(type); // 获取SDS头部分的大小
assert(hdrlen + newlen + 1 > reqlen); /* 捕获size_t溢出 */
if (oldtype == type) {
// 如果SDS类型没有发生改变,通过s_realloc_usable扩大数据数组即可,注意,这里buf的指针s被更新了
newsh = s_realloc_usable(sh, hdrlen + newlen + 1, &usable);
if (newsh == NULL) return NULL;
s = (char *)newsh + hdrlen; // 返回SDS的buf指针
} else {
/* 由于头部大小更改,需要将字符串向前移动,不能使用realloc */
newsh = s_malloc_usable(hdrlen + newlen + 1, &usable); // 按新长度重新分配内存
if (newsh == NULL) return NULL;
memcpy((char *)newsh + hdrlen, s, len + 1); // 将原buf的数据拷贝到新的SDS的buf位置上
s_free(sh); // 释放原SDS的指针
s = (char *)newsh + hdrlen; // 偏移SDS结构的起始地址,得到buf的地址
s[-1] = type; // 为SDS结构flags设置值
sdssetlen(s, len); // 为SDS结构的len设置数据的长度
}
usable = usable - hdrlen - 1; // 计算可用的长度
// 如果可用长度大于当前SDS结构的最大长度,则将可用长度设置为当前SDS结构的最大长度
if (usable > sdsTypeMaxSize(type))
usable = sdsTypeMaxSize(type);
sdssetalloc(s, usable); // 为SDS结构alloc设置值
return s;
}
重新分配内存
sdsResize函数用于重新分配sds字符串的内存大小,如果请求的大小小于当前使用的长度,数据将被截断。
/*
* sdsResize函数用于重新分配sds字符串的内存大小。
* 如果请求的大小小于当前使用的长度,数据将被截断。
* 当would_regrow参数设置为1时,会防止使用SDS_TYPE_5类型的内存,这对于可能再次改变的sds字符串是很有利的。
* sdsAlloc的大小将设置为请求的大小,而不受实际分配大小的限制,这样做的目的是为了避免在调用者检测到有额外空间时重复调用该函数。
*/
sds sdsResize(sds s, size_t size, int would_regrow) {
void *sh, *newsh; // 分配的新内存指针
char type, oldtype = s[-1] & SDS_TYPE_MASK; // 当前sds字符串的类型
int hdrlen, oldhdrlen = sdsHdrSize(oldtype); // 当前sds字符串的头部大小
size_t len = sdslen(s); // 当前sds字符串的长度
sh = (char*)s-oldhdrlen;
/* 如果当前内存大小和请求大小相同,则直接返回。 */
if (sdsalloc(s) == size) return s;
/* 如果请求大小小于当前长度,则设置请求大小为当前长度。*/
if (size < len) len = size;
/* 检查为了适应这个字符串,需要的最小SDS头部类型。*/
type = sdsReqType(size);
if (would_regrow) {
/* 对于期望字符串增长的情况,不要使用type 5,因为它不适合再次改变的字符串。*/
if (type == SDS_TYPE_5) type = SDS_TYPE_8;
}
hdrlen = sdsHdrSize(type);
/* 如果类型相同,或者可以使用低开销容纳大小(大于SDS_TYPE_8),则只使用realloc函数重新分配内存,
* 让分配器在需要时进行拷贝。否则,如果更改巨大,我们手动重新分配字符串的类型。*/
int use_realloc = (oldtype==type || (type < oldtype && type > SDS_TYPE_8));
size_t newlen = use_realloc ? oldhdrlen+size+1 : hdrlen+size+1;
int alloc_already_optimal = 0;
#if defined(USE_JEMALLOC)
/* 当使用Jemalloc时,je_nallocx返回新的长度newlen的预期分配大小,
* 如果分配大小没有改变,我们的目标是避免调用realloc函数,因为即使分配大小保持不变,也会产生开销。*/
alloc_already_optimal = (je_nallocx(newlen, 0) == zmalloc_size(sh));
#endif
/* 如果既使用realloc函数又没有最优分配内存,则使用realloc函数重新分配内存。*/
if (use_realloc && !alloc_already_optimal) {
newsh = s_realloc(sh, newlen);
if (newsh == NULL) return NULL;
s = (char*)newsh+oldhdrlen;
}
/* 如果没有最优分配内存,则使用malloc函数重新分配内存。*/
else if (!alloc_already_optimal) {
newsh = s_malloc(newlen);
if (newsh == NULL) return NULL;
memcpy((char*)newsh+hdrlen, s, len);
s_free(sh);
s = (char*)newsh+hdrlen;
s[-1] = type;
}
s[len] = 0; // 设置字符串的结束符为0
sdssetlen(s, len); // 设置字符串的长度
sdssetalloc(s, size); // 设置字符串的分配内存大小
return s; // 返回重新分配内存后的字符串
}
sdsRemoveFreeSpace
/* 重新分配sds字符串的内存空间,使其没有空闲空间。包含的字符串保持不变,
* 但是接下来的拼接操作将需要重新分配内存空间。
*
* 调用后,传入的sds字符串不再有效,所有引用必须被替换为调用返回的新指针。 */
sds sdsRemoveFreeSpace(sds s, int would_regrow) {
return sdsResize(s, sdslen(s), would_regrow);
}
sdsMakeRoomFor
/* 为sds字符串末尾的空闲空间增加更多的空间,
* 这对于重复追加到sds中以避免重复重新分配空间很有用。 */
sds sdsMakeRoomFor(sds s, size_t addlen) {
return _sdsMakeRoomFor(s, addlen, 1);
}
/* 与sdsMakeRoomFor()不同,这只增长到所需的大小。 */
sds sdsMakeRoomForNonGreedy(sds s, size_t addlen) {
return _sdsMakeRoomFor(s, addlen, 0);
}
sdsAllocSize
/* 返回指定sds字符串的分配大小,包括:
* 1) 指针之前的sds头部。
* 2) 字符串本身。
* 3) 末尾的自由缓冲区(如果有)。
* 4) 潜在的空字符项。
*/
size_t sdsAllocSize(sds s) {
size_t alloc = sdsalloc(s);
return sdsHdrSize(s[-1])+alloc+1;
}
sdsAllocPtr
/* 返回实际的SDS分配的指针(通常情况下,SDS字符串是通过字符串缓冲区的起始地址引用的)。 */
void *sdsAllocPtr(sds s) {
return (void*) (s-sdsHdrSize(s[-1]));
}
sdsIncrLen
/* 增加sds的长度并根据增量`incr`减少字符串末尾的空闲空间。
* 同时,设置字符串新结尾的null终止符。
*
* 该函数用于在用户调用sdsMakeRoomFor()后,在字符串末尾写入内容,
* 并最终需要设置新长度时,修正字符串的长度。
*
* 注意:可以使用负的增量来右修剪字符串。
*
* 使用示例:
*
* 通过sdsIncrLen()和sdsMakeRoomFor()可以实现以下方案,将从内核读取的字节追加到sds字符串末尾而无需中间拷贝:
*
* oldlen = sdslen(s);
* s = sdsMakeRoomFor(s, BUFFER_SIZE);
* nread = read(fd, s+oldlen, BUFFER_SIZE);
* ... 检查nread <= 0 并做出处理 ...
* 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; /* 只为了消除编译错误。 */
}
s[len] = '\0';
}
sdsgrowzero
/* 将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;
/* 确保添加的区域不包含垃圾数据 */
memset(s+curlen,0,(len-curlen+1)); /* 同时设置尾部的 \0 字节 */
sdssetlen(s, len);
return s;
}
sdscatlen
/* 将指定的二进制安全字符串指针't'中的'len'字节追加到指定的sds字符串's'末尾。 */
/* 在调用后,传递的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;
}
sdscat
/* 将指定的以null终止的C字符串追加到sds字符串's'中。
*
* 追加完成后,传入的sds字符串将不再有效,所有引用必须被替换为调用返回的新指针。 */
sds sdscat(sds s, const char *t) {
return sdscatlen(s, t, strlen(t));
}
sdscpylen
/* 将指向长度为 'len' 字节的指定二进制安全字符串 't' 的指针复制到 sds 字符串 's' 中,并对其进行破坏性修改。 */
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;
}
sdscpy
/* 类似于 sdscpylen(),但是 't' 必须是一个以 null 终止的字符串,以便通过 strlen() 获取字符串的长度。 */
sds sdscpy(sds s, const char *t) {
return sdscpylen(s, t, strlen(t));
}
sdsfromlonglong
/* 从一个 long long 值创建一个 sds 字符串。它比以下方式要快得多:
*
* sdscatprintf(sdsempty(),"%lld\n", value);
*/
sds sdsfromlonglong(long long value) {
char buf[LONG_STR_SIZE];
int len = ll2string(buf,sizeof(buf),value);
return sdsnewlen(buf,len);
}
sdscatvprintf
/* 与 sdscatprintf() 类似,但是获取 va_list 而不是可变参数。 */
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;
int bufstrlen;
/* 我们尝试使用静态缓冲区以提高速度。
* 如果无法使用,则回退到堆分配。 */
if (buflen > sizeof(staticbuf)) {
buf = s_malloc(buflen);
if (buf == NULL) return NULL;
} else {
buflen = sizeof(staticbuf);
}
/* 在无法将字符串放入当前缓冲区大小后,分配足够的空间,
* 以容纳缓冲区中的字符串和 \0。 */
while(1) {
va_copy(cpy,ap);
bufstrlen = vsnprintf(buf, buflen, fmt, cpy);
va_end(cpy);
if (bufstrlen < 0) {
if (buf != staticbuf) s_free(buf);
return NULL;
}
if (((size_t)bufstrlen) >= buflen) {
if (buf != staticbuf) s_free(buf);
buflen = ((size_t)bufstrlen) + 1;
buf = s_malloc(buflen);
if (buf == NULL) return NULL;
continue;
}
break;
}
/* 最后将获得的字符串合并到 SDS 字符串中并返回。 */
t = sdscatlen(s, buf, bufstrlen);
if (buf != staticbuf) s_free(buf);
return t;
}
sdscatprintf
/* 将格式化后的字符串与sds字符串's'拼接
*
* 该函数执行后,修改后的sds字符串将不再有效,所有引用必须用调用函数返回的新指针替换。
*
* 示例:
*
* s = sdsnew("Sum is: ");
* s = sdscatprintf(s,"%d+%d = %d",a,b,a+b).
*
* 经常需要根据printf-like格式创建一个字符串。 当需要这样做的时候,只需使用sdsempty()作为目标字符串:
*
* s = sdscatprintf(sdsempty(), "... your format ...", args);
*/
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;
}
sdscatfmt
/*
* 这个函数类似于sdscatprintf,但是由于不依赖于libc中实现在sprintf家族函数,因此速度更快。
* 此外,由于直接将数据拼接到sds字符串中,因此性能也有所提高。
*
* 但是该函数只支持不兼容的printf类似的格式说明符子集:
*
* %s - C 字符串
* %S - SDS 字符串
* %i - 有符号整数
* %I - 64 位有符号整数(long long, int64_t)
* %u - 无符号整数
* %U - 64 位无符号整数(unsigned long long, uint64_t)
* %% - 直接输出 "%" 字符。
*/
sds sdscatfmt(sds s, char const *fmt, ...) {
size_t initlen = sdslen(s);
const char *f = fmt;
long i;
va_list ap;
/* 为了避免连续的重新分配,让我们从一个初始长度至少为格式字符串本身的两倍的缓冲区开始。这不是一个最好的启发式方法,但在实践中似乎有效。 */
s = sdsMakeRoomFor(s, strlen(fmt)*2);
va_start(ap,fmt);
f = fmt; /* 下一个要处理的格式说明符字节。 */
i = initlen; /* 下一个要写入目标字符串的字节位置。 */
while(*f) {
char next, *str;
size_t l;
long long num;
unsigned long long unum;
/* 确保始终至少有1个字符的空间。 */
if (sdsavail(s)==0) {
s = sdsMakeRoomFor(s,1);
}
switch(*f) {
case '%':
next = *(f+1);
if (next == '\0') break;
f++;
switch(next) {
case 's':
case 'S':
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':
if (next == 'i')
num = va_arg(ap,int);
else
num = va_arg(ap,long long);
{
char buf[LONG_STR_SIZE];
l = ll2string(buf,sizeof(buf),num);
if (sdsavail(s) < l) {
s = sdsMakeRoomFor(s,l);
}
memcpy(s+i,buf,l);
sdsinclen(s,l);
i += l;
}
break;
case 'u':
case 'U':
if (next == 'u')
unum = va_arg(ap,unsigned int);
else
unum = va_arg(ap,unsigned long long);
{
char buf[LONG_STR_SIZE];
l = ull2string(buf,sizeof(buf),unum);
if (sdsavail(s) < l) {
s = sdsMakeRoomFor(s,l);
}
memcpy(s+i,buf,l);
sdsinclen(s,l);
i += l;
}
break;
default: /* 处理 %% 和其他 %<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;
}
sdsrange
/* 将字符串转换成较小(或与指定索引范围相等)的字符串,仅包含由 'start' 和 'end' 索引指定的子字符串。 start 和 end 可以是负数,负数-1表示字符串的最后一个字符,负数-2表示字符串的倒数第二个字符,以此类推。 范围是包括的,因此 start 和 end 字符将是结果字符串的组成部分。 字符串在原地修改。 注意:此函数可能具有误导性,并且可能具有意外的行为,特别是在需要新字符串长度为0时。 当 start==end 时,结果字符串将包含一个字符。 请考虑使用 sdssubstr。 例如: s = sdsnew("Hello World"); sdsrange(s,1,-1); => "ello World" */
void sdsrange(sds s, ssize_t start, ssize_t end) {
size_t newlen, len = sdslen(s);
if (len == 0) return;
if (start < 0)
start = len + start;
if (end < 0)
end = len + end;
newlen = (start > end) ? 0 : (end-start)+1;
sdssubstr(s, start, newlen);
}
sdscmp
/* 使用memcmp()比较两个sds字符串s1和s2。
* 返回值:
* 若s1 > s2,则返回正数。
* 若s1 < s2,则返回负数。
* 若s1 和 s2 是完全相同的二进制字符串,则返回0。
*
* 如果两个字符串共享完全相同的前缀,但其中一个字符串有多余的字符,
* 则较长的字符串将被认为是大于较短的字符串。 */
int sdscmp(const sds s1, const sds s2) {
size_t l1, l2, minlen;
int cmp;
l1 = sdslen(s1);
l2 = sdslen(s2);
minlen = (l1 < l2) ? l1 : l2;
cmp = memcmp(s1,s2,minlen);
if (cmp == 0) return l1>l2? 1: (l1<l2? -1: 0);
return cmp;
}
sdssplitlen
/*
* 分割字符串 's' 使用分隔符 'sep'。
* 返回一个 sds 字符串数组。通过引用设置 *count 来返回分割的元素数量。
*
* 在出现内存不足、空字符串、空分隔符的情况下,返回 NULL。
*
* 注意,'sep' 可以使用多字符分隔符来分割字符串。例如
* sdssplit("foo_-_bar","_-_"); 将返回两个元素 "foo" 和 "bar"。
*
* 此版本的函数是二进制安全的,但需要长度参数。sdssplit() 是相同的功能用于零终止的字符串。
*/
sds *sdssplitlen(const char *s, ssize_t len, const char *sep, int seplen, int *count) {
int elements = 0, slots = 5;
long start = 0, j;
sds *tokens;
if (seplen < 1 || len <= 0) {
*count = 0;
return NULL;
}
tokens = s_malloc(sizeof(sds)*slots);
if (tokens == NULL) return NULL;
for (j = 0; j < (len-(seplen-1)); j++) {
/* 确保为下一个元素和最后一个元素留出空间 */
if (slots < elements+2) {
sds *newtokens;
slots *= 2;
newtokens = s_realloc(tokens,sizeof(sds)*slots);
if (newtokens == NULL) goto cleanup;
tokens = newtokens;
}
/* 搜索分隔符 */
if ((seplen == 1 && *(s+j) == sep[0]) || (memcmp(s+j,sep,seplen) == 0)) {
tokens[elements] = sdsnewlen(s+start,j-start);
if (tokens[elements] == NULL) goto cleanup;
elements++;
start = j+seplen;
j = j+seplen-1; /* 跳过分隔符 */
}
}
/* 添加最终元素。我们确定tokens数组中一定有足够空间 */
tokens[elements] = sdsnewlen(s+start,len-start);
if (tokens[elements] == NULL) goto cleanup;
elements++;
*count = elements;
return tokens;
cleanup:
{
int i;
for (i = 0; i < elements; i++) sdsfree(tokens[i]);
s_free(tokens);
*count = 0;
return NULL;
}
}