Redis系统当中,针对字符串进行的更加完善的封装,建立了一个动态字符串,并构建了大量的实用api。相关的实现代码为sds.h及sds.c,以下为我的源码阅读笔记。内容较多,逐步更新
typedef char *sds;
struct __attribute__ ((__packed__)) sdshdr5 {
usigned char flags;
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len;
uint8_t alloc;
unsigned char flags;
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
uint16_t len;
uint16_t alloc;
unsigned char flags;
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
uint32_t len;
uint32_t alloc;
unsigned char flags;
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
uint64_t len;
uint64_t alloc;
unsigned char flags;
char buf[];
};
#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
#define SDS_TYPE_MASK 7
#define SDS_TYPE_BITS 3
#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))))
#define SDS_TYPE_5_LEN(f) ((f)>>SDS_TYPE_BITS)
以上是动态字符串的结构体声明及define声明的函数。动态字符串一共有5种类型,分别为不同长度的字符串所实用。我在这里称之为动态字符串的头部。
sdshdr5:长度为小于32的字符串
sdshdr8:长度为小于256的字符串
sdshdr16:长度为小于2^16的字符串
sdshdr32:长度为小于2^32的字符串。这里有一点注意,若是机器的LONG_MAXbu不等于LLONG_MAX,则返回sdshdr64类型。
sdshdr64:其他所有长度都实用此类。
sdshdr5这个类型不同于其他类型,它缺少len成员与alloc成员,它的判断与处理都比较特别。但是官方在代码有过一段注释,如下:
/* Note: sdshdr5 is never used, we just access the flags byte directly.
* However is here to document the layout of type 5 SDS strings. */
注释中说明,这个类型从未被使用,所以我们在这里姑且不考虑它,而实际上,它的处理操作本质上与其他类型并没有什么区别。方便起见,我们以通用的类型进行研究。
结构体当中,有四个类,分别为len、alloc、flags与buf。
len:字符串的长度。
alloc:字符串内存总大小,注意,alloc不同于len。len是实际字符串的长度,而alloc,则是实际分配的内存大小(不包含sds头与结尾的'\0'的大小)。为了减少字符串内容增加时反复的重新申请内存,redis当中会申请更多内存以备使用。当字符串大小小于1MB的时候,申请两倍大小的内存使用,当字符串大小大于等于1MB的时候,多申请1MB的内存以备使用。详细的设定可以看之后的sdsMakeRoomFor函数的解析。
flags:作为区分不同类型的标记使用,暂时只使用了低3位来标记,高5位暂未使用,也供以后增加新功能时使用。上面代码中define声明的SDS_TYPE_*类型为相应的标记内容,用于区分不同的字符串类型。比如flags等于SDS_TYPE_8时,则可以从字符串开始字节在向前17字节,或者字符串头部开始获取8字节数据获取当前字符串的实际长度。等于SDS_TYPE_16时,从字符串开始字节向前33字节,或者字符串头部开始获取16自己获取当前字符串的实际长度。flags与头部信息的配合使用,将在之后的函数解析里面大量出现。
buf:实际存储字符串内容的数组,同传统数组一样,结尾需要'\0'字符。
在sdshdr的声明当中,我们可以看到 __attribute__ ((__packed__)) 关键字,这将使这个结构体在内存中不再遵守字符串对齐规则,而是以内存紧凑的方式排列。所以可以从字符串位置开始向前一个字节便可以获取flags信息了,比如buf[-1]。具体__attribute__ ((__packed__))与字符串对齐的内容请查看另一篇博客。
SDS_HDR_VAR函数则通过结构体类型与字符串开始字节,获取到动态字符串头部的开始位置,并赋值给sh指针。SDS_HDR函数则通过类型与字符串开始字节,返回动态字符串头部的指针。使用方式为可在之后的代码当中看到,具体define声明中的双'#'号的使用方式与意义,请看草另一篇博客。
sds比起传统的字符串,有着自己优势跟便利之处。
1、内存预分配:sds通过预先分配了更多的内存量,来避免频繁的申请、分配内存,无端的浪费了性能
2、惰性释放:sds作为普通字符串使用之时,可以通过在不同字节打上'\0'字符,来代表字符串的截断及减少长度,而不是需要清空多余的字节并释放它们,那些内存再之后的操作当中可以当做空闲内存继续使用。
3、二进制安全:作为非字符串使用存储数据之时,通过善用头部的len属性,可以存储一些包含'\0'字符的数据。当然,一定要善用len属性,api当中,如长度更新的函数,同样通过'\0'字符来判断结尾!
接下来开始介绍sds相关的api函数,第一批是声明、定义在sds.h文件内的静态函数,这些函数都是针对动态字符串头部的属性的获取与修改,简单易懂。
//获取动态字符串长度
static inline size_t sdslen(const sds s) {
unsigned char flags = s[-1];//获取头部信息中的flags属性,因内存紧密相连,可以直接通过这种方式获取
switch(flags&SDS_TASK_MASK) {//获取类型,SDS_TASK_MASK为7,所以flags&SDS_TASK_MASK等于flags
case SDS_TYPE_5:
return SDS_TYPE_5_LEN(flags);//SDS_TYPE_5类型的长度获取稍微不同,它的长度被定义在flags的高5位当中,具体可查看之后的sdsnewlen函数,或者下面的sdssetlen函数
case SDS_TYPE_8:
return SDS_HDR(8,s)->len;//SDS_HDR函数通过类型与字符串开始字节获取头部,以此获取字符串的长度
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) {
unsigned char flags = s[-1];//获取flags
switch(flags&SDS_TYPE_MASK) {
case SDS_TYPE_5://SDS_TYPE_5直接返回0,
return 0;
case SDS_TYPE_8: {
SDS_HDR_VAR(8,s);//通过SDS_HDR_VAR函数,将头部指针放置在sh变量
return sh->alloc - sh->len;//总内存大小 - 字符串长度,获取可用内存大小
}
case SDS_TYPE_16: {
SDS_HDR_VAR(16,s);
return sh->alloc - sh->len;
}
case SDS_TYPE_32: {
SDS_HDR_VAR(32,s);
return sh->alloc - sh->len;
}
case SDS_TYPE_64: {
SDS_HDR_VAR(64,s);
return sh->alloc - sh-len;
}
}
return 0;
}
//重置字符串长度
static inline void sdssetlen(sds s, size_t newlen) {
unsigned char flags = s[-1];//获取flags
switch(flags&SDS_TASK_MASK) {
//SDS_TYPE_5的长度设置较为特殊,长度信息写在flags的高5位
case SDS_TYPE_5:
{
unsigned char *fp = ((unsigned char*)s)-1;
*fp = SDS_TYPE_5 | (newlen << SDS_TYPE_BITS);
}
break;
//其他类型则是统一修改len属性的值
case SDS_TYPE_8:
SDS_HDR(8,s)->len = newlen;
break;
case SDS_TYPE_16:
SDS_HDR(16,s)->len = newlen;
break;
case SDS_TYPE_32:
SDS_HDR(32,s)->len = newlen;
break;
case SDS_TYPE_64:
SDS_HDR(64,s)->len = newlen;
break;
}
}
//按照指定数值,增加字符串长度
static inline void sdsinclen(sds s, size_t inc) {
unsigned char flags = s[-1];//获取flags
switch(flags&SDS_TYPE_MASK) {
//SDS_TYPE_5类型使用上面的函数,获取长度、更新、设置
case SDS_TYPE_5:
{
unsigned char *fp = ((unsigned char*)s)-1;
unsigned char newlen = SDS_TYPE_LEN(flags)+inc;
*fp = SDS_TYPE_5 | (newlen << SDS_TYPE_BITS);
}
break;
//其他类型则直接通过SDS_HDR函数,更新len值
case SDS_TYPE_8:
SDS_HDR(8,s)->len += inc;
break;
case SDS_TYPE_16:
SDS_HDR(16,s)->len += inc;
break;
case SDS_TYPE_32:
SDS_HDR(32,s)->len += inc;
break;
case SDS_TYPE_64:
SDS_HDR(64,s)->len += inc;
break;
}
}
//获取动态字符串的总内存
static inline size_t sdsalloc(const sds s) {
unsigned char flags = s[-1];//获取flags
switch(flags&SDS_TASK_MASK) {
//SDS_TYPE_5直接通过SDS_TYPE_5_LEN函数返回
case SDS_TYPE_5:
return SDS_TYPE_5_LEN(flags);
//其他类型则返回头部信息中的alloc属性
case SDS_TYPE_8:
return SDS_HDR(8,s)->alloc;
case SDS_TYPE_16:
return SDS_HDR(16,s)->alloc;
case SDS_TYPE_32:
return SDS_HDR(32,s)->alloc;
case SDS_TYPE_64:
return SDS_HDR(64,s)->alloc;
}
return 0;
}
//重置字符串内存大小
static inline size_t sdssetalloc(sds s, size_t newlen) {
unsigned cahr flags = s[-1];//获取flags
switch(flags&SDS_TASK_MASK) {
case SDS_TYPE_5:
//官方注释,SDS_TYPE_5不做任何操作
/*Nothing to do, this type has no total allocation info. */
break;
//其他类型直接修改头部信息中的alloc属性
case SDS_TYPE_8:
SDS_HDR(8,s)->alloc = newlen;
break;
case SDS_TYPE_16:
SDS_HDR(16,s)->alloc = newlen;
break;
case SDS_TYPE_32:
SDS_HDR(32,s)->alloc = newlen;
break;
case SDS_TYPE_64:
SDS_HDR(64,s)->alloc = newlen;
break;
}
}
上述的几个函数,
sdslen,
sdsavail,
sdssetlen,
sdsinclen,
sdsalloc,
sdssetalloc函数,都是基本的头部属性操作函数。代码的难度也不大,可以直观的阅读、理解。
接下来的sds的相关api,数量有点多,之后的dict、zskiplist也是有大量api,挑部分代码较多,需要逐行理解的函数来记录、分析。
//创建一个新的sds对象
sds sdsnewlen(const void *init, size_t initlen) {
void *sh;
sds s;
char type = sdsReqType(initlen);//根据初始化长度获取对应结构体类型
if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;//若长度为0的则初始化为SDS_TYPE_8类型
int hdrlen = sdsHdrSize(type);
unsigned char *fp;
sh = s_malloc(hdrlen+initlen+1);//申请足够的内存,头部大小+初始化大小+结尾符
if (!init)
memset(sh, 0, hdrlen+initlen+1);
if (sh == NULL) return NULL;
s = (char*)sh+hdrlen;//获取字符串起始字节
fp = ((unsigned char*)s)-1;//获取flags字节
switch(type) {
//SDS_TYPE_5又是特立独行了,有自己的初始化方案,我都懒得说明了。。。
case SDS_TYPE_5: {
*fp = type | (initlen << SDS_TYPE_BITS);
break;
}
//其他类型通过SDS_HDR_VAR函数获取头部信息,并逐步初始化
case SDS_TYPE_8: {
SDS_HDR_VAR(8,s);
sh->len = initlen;
sh->alloc = initlen;
*fp = type;
break;
}
case SDS_TYPE_16: {
SDS_HDR_VAR(16,s);
sh->len = initlen;
sh->alloc = initlen;
*fp = type;
break;
}
case SDS_TYPE_32: {
SDS_HDR_VAR(32,s);
sh->len = initlen;
sh->alloc = initlen;
*fp = type;
break;
}
case SDS_TYPE_64: {
SDS_HDR_VAR(64,s);
sh->len = initlen;
sh->alloc = initlen;
*fp = type;
break;
}
}
//根据init与initlen,将内容复制给字符串
if (initlen && init)
memcpy(s, init, initlen);
//打上结尾符
s[initlen] = '\0';
return s;
}
sdsnewlen根据参数给予的init字符串与initlen初始长度,生成并返回一个动态字符串。代码都打上了注释,阅读已经没什么难度了。