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初始长度,生成并返回一个动态字符串。代码都打上了注释,阅读已经没什么难度了。
相关推荐:
以上是Redis源码解析的详细内容。更多信息请关注PHP中文网其他相关文章!