最近打算阅读redis源码,可是担忧读完就忘了,因此决定把阅读的笔记在简书里记录起来,但愿可以坚持读下去。之因此选择3.2是由于公司把redis升级成了这个版本。程序员
redis在处理字符串的时候没有直接使用以'0'结尾的C语言字符串,而是封装了一下C语言字符串并命名为sds(simple dynamic string),在sds.h文件里咱们能够看到以下类型定义:typedef char *sds;
也就是说实际上sds类型就是char*类型,那sds和char*有什么区别呢?
主要区别就是:sds必定有一个所属的结构(sdshdr),这个header结构在每次建立sds时被建立,用来存储sds以及sds的相关信息(下文sds的含义仅仅是redis的字符串,sdshdr才表示sds的header)。redis
那为何redis不直接使用char*呢?总结起来理由以下:算法
这只是我看完代码后得出的结论,看不懂也没事,先列出来只是为了直观一点。固然还有其余使用sds的理由,想到再加上。接下来看代码:数组
sdshdr和sds是一一对应的关系,一个sds必定会有一个sdshdr用来记录sds的信息。在redis3.2分支出现以前sdshdr只有一个类型,定义以下:安全
struct sdshdr { unsigned int len;//表示sds当前的长度 unsigned int free;//已为sds分配的长度-sds当前的长度 char buf[];//sds实际存放的位置 };
这些版本的redis每次建立一个sds 无论sds实际有多长,都会分配一个大小固定的sdshdr。根据成员len的类型可知,sds最多能存长度为2^(8*sizeof(unsigned int))的字符串。
而3.2分支引入了五种sdshdr类型,每次在建立一个sds时根据sds的实际长度判断应该选择什么类型的sdshdr,不一样类型的sdshdr占用的内存空间不一样。这样细分一下能够省去不少没必要要的内存开销,下面是3.2的sdshdr定义:app
struct __attribute__ ((__packed__)) sdshdr5 { //实际上这个类型redis不会被使用。他的内部结构也与其余sdshdr不一样,直接看sdshdr8就好。 unsigned char flags; //一共8位,低3位用来存放真实的flags(类型),高5位用来存放len(长度)。 char buf[];//sds实际存放的位置 }; struct __attribute__ ((__packed__)) sdshdr8 { uint8_t len;//表示当前sds的长度(单位是字节) uint8_t alloc; //表示已为sds分配的内存大小(单位是字节) unsigned char flags; //用一个字节表示当前sdshdr的类型,由于有sdshdr有五种类型,因此至少须要3位来表示000:sdshdr5,001:sdshdr8,010:sdshdr16,011:sdshdr32,100:sdshdr64。高5位用不到因此都为0。 char buf[];//sds实际存放的位置 }; struct __attribute__ ((__packed__)) sdshdr16 { uint16_t len; /* used */ uint16_t alloc; /* excluding the header and null terminator */ unsigned char flags; /* 3 lsb of type, 5 unused bits */ char buf[]; }; struct __attribute__ ((__packed__)) sdshdr32 { uint32_t len; /* used */ uint32_t alloc; /* excluding the header and null terminator */ unsigned char flags; /* 3 lsb of type, 5 unused bits */ char buf[]; }; struct __attribute__ ((__packed__)) sdshdr64 { uint64_t len; /* used */ uint64_t alloc; /* excluding the header and null terminator */ unsigned char flags; /* 3 lsb of type, 5 unused bits */ char buf[]; };
根据以上定义,我画了一个sdshdr8图来讲明它们的内存布局:curl
首先要说明之因此sizeof(struct sdshdr8)的大小是len+alloc+flags 是由于这个struct拥有一个柔性数组成员 buf,柔性数组成员是C99以后引入的一个新feature,这里能够经过sizeof整个struct给出buf变量的偏移量,从而肯定buf的位置。Flexible array member, is a feature introduced in the [C99] standard of the C programming language,which is an array without a given dimension, and it must be the last member of such a struct. The 'sizeof' operator on such a struct is required to give the offset of the flexible array member. --维基百科ide
其次须要说明的是定义sdshdr的这部分代码用了__attribute__ ((__packed__)),这个语法不存在于任何C语言标准,是GCC的一个extension,用来告诉编译器使用最小的内存来存储sdshdr。函数
packed: ...,This attribute, attached to struct or union type definition, specifies that each member of the structure or union is placed to minimize the memory required.布局
引用里"minimize the memory required"其实就是让编译器尽可能不使用内存对齐(alignment),以免没必要要的空间浪费,但其实这么作会有时间上的开销,假设CPU老是从存储器中读取8个字节,则变量地址必须为8的倍数,为了获取一个没对齐的8字节的uint8_t数据,CPU须要执行两次内存访问 从两个8字节的内存块中取出完整的8字节数据。关于内存对齐的更多信息,《深刻理解计算机系统》第三章和《程序员的自我修养》 都有很是详细的描述。但这里咱们只须要知道禁用(准确地说是尽可能不使用)内存对齐是redis为了节省内存开支的一种手段。
接下来分析每一个成员:
#define SDS_TYPE_5 0 //00000000 #define SDS_TYPE_8 1 //00000001 #define SDS_TYPE_16 2 //00000010 #define SDS_TYPE_32 3 //00000011 #define SDS_TYPE_64 4 //00000100 #define SDS_TYPE_MASK 7 //00000111,做为取flags低3位的掩码
要判断一个sds属于什么类型的sdshdr,只需 flags&SDS_TYPE_MASK和SDS_TYPE_n比较便可(之因此须要SDS_TYPE_MASK是由于有sdshdr5这个特例,它的高5位不必定为0,参考上面sdshdr5定义里的代码注释)
sds.h里全部给出定义的内联函数都是经过sds做为参数,经过比较flags&SDS_TYPE_MASK和SDS_TYPE_n来判断该sds属于哪一种类型sdshdr,再按照指定的sdshdr类型取出sds的相关信息。
例如sdslen函数:
#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T)))) //返回一个类型为T包含s字符串的sdshdr的指针 #define SDS_TYPE_5_LEN(f) ((f)>>SDS_TYPE_BITS) //用sdshdr5的flags成员变量作参数返回sds的长度,这实际上是一个没办法的hack #define SDS_TYPE_BITS 3 static inline size_t sdslen(const sds s) { unsigned char flags = s[-1]; //sdshdr的flags成员变量 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;//取出sdshdr的len成员 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; }
第一行里的双井号##的意思是在一个宏(macro)定义里链接两个子串(token),链接以后这##号两边的子串就被编译器识别为一个。
sdslen函数里第一行出现了s[-1],看起来感受会是一个undefined behavior,其实不是,这是一种正常又正确的使用方式,它就等同于*(s-1)。The definition of the subscript operator [] is that E1[E2] is identical to (*((E1)+(E2))). --C99。又由于s是一个sds(char*)因此s指向的类型是char,-1就是-1*sizeof(char),因为sdshdr结构体内禁用了内存对齐,因此这也恰好是一个flags(unsigned char)的地址,因此经过s[-1]咱们能够得到sds所属的sdshdr的成员变量flags。
相似sdslen这样利用sds找到sdshdr类型的还有以下几个函数,就不一一分析了:
static inline size_t sdsavail(const sds s) static inline void sdssetlen(sds s, size_t newlen) static inline void sdsinclen(sds s, size_t inc) static inline size_t sdsalloc(const sds s) static inline void sdssetalloc(sds s, size_t newlen)
前面说的是在已有结果的状况下,根据一个sds经过flags变量来判断它的sdshdr类型。那么最开始建立一个sds时应该选用什么类型的sdshdr来存放它的信息呢?这就得根据要存储的sds的长度决定了,redis在建立一个sds以前会调用sdsReqType(size_t string_size)来判断用哪一个sdshdr。该函数传递一个sds的长度做为参数,返回应该选用的sdshdr类型。
static inline char sdsReqType(size_t string_size) { if (string_size < 1<<5) //小于2^5,flags成员的高5位便可表示 return SDS_TYPE_5; if (string_size < 1<<8) //小于2^8,8位整数(sdshdr8里的uint8_t)便可表示string_size return SDS_TYPE_8; if (string_size < 1<<16) //小于2^16,16位整数(sdshdr16里的uint16_t)便可表示string_size return SDS_TYPE_16; if (string_size < 1ll<<32) /小于2^32,32位整数(sdshrd32里的uint32_t)便可表示string_size,1ll是指1long long(至少64位)的意思,若是没有ll,1就是一个int,假设int为4字节32位,1<<32就会致使undefined behavior. return SDS_TYPE_32; return SDS_TYPE_64; //若sds的长度超过2^64,则全部类型都不法表示这个sds的len }
知道了建立一个sds时应选用什么类型的sdshdr后咱们就能够看看建立sds的函数了:
//用init指针指向的内存的内容截取initlen长度来new一个sds,这个函数是二进制安全的 sds sdsnewlen(const void *init, size_t initlen) { void *sh;//sdshdr的指针 sds s; //char * s; char type = sdsReqType(initlen);//根据须要的长度决定sdshdr的类型 /* Empty strings are usually created in order to append. Use type 8 * since type 5 is not good at this. */ if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;//若是initlen为空而且sdshdr的类型为sdshdr5,则将类型设置为sdshdr8 int hdrlen = sdsHdrSize(type);//每一个sdshdr类型的大小都不同,根据类型返回sdshdr的大小以计算须要分配的空间 unsigned char *fp; /* flags pointer. */ sh = s_malloc(hdrlen+initlen+1);//在heap里申请一段连续的空间给sdshdr和属于它的sds,+1是由于要在尾部放置'\0' if (!init) memset(sh, 0, hdrlen+initlen+1);//若是init为空,则整个sdshdr都用0即字符'\0'初始化 if (sh == NULL) return NULL; s = (char*)sh+hdrlen;//经过sdshdr指针找到sds的位置 fp = ((unsigned char*)s)-1;//找到flags的位置,等同于&s[-1] switch(type) { case SDS_TYPE_5: { *fp = type | (initlen << SDS_TYPE_BITS);//initlen左移3位到高5位,给type腾出位置,和type作或运算 break; } case SDS_TYPE_8: { SDS_HDR_VAR(8,s);//#define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T))); 能够理解为在switch做用域下申明了一个新的局部变量sh,类型是struct sdshdr##T,跟外面的sh值同样,变量名同样,但不是一个东西。 sh->len = initlen; sh->alloc = initlen; *fp = type;//设置flags 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; } } if (initlen && init) memcpy(s, init, initlen); //memcpy不会由于'\0'而停下,支持二进制数据的拷贝 s[initlen] = '\0'; //不论是不是二进制数据,尾部都会加上'\0' return s; } static inline int sdsHdrSize(char type) { switch(type&SDS_TYPE_MASK) { case SDS_TYPE_5: return sizeof(struct sdshdr5);//以前说的柔性数组成员不会计入struct的大小,因此这个hdrsize没有包括sds的长度 case SDS_TYPE_8: return sizeof(struct sdshdr8); case SDS_TYPE_16: return sizeof(struct sdshdr16); case SDS_TYPE_32: return sizeof(struct sdshdr32); case SDS_TYPE_64: return sizeof(struct sdshdr64); } return 0; }
这就是用于新建或拷贝sds的代码,流程写在注释里可能仍是不够清晰,结合图来看应该会好一点(sdshdr5的图不同,我就偷懒不画了,反正也不用它):
流程以下:
若是用字符串"PHP is the best programming language"(长度为36) 调用sdsnewlen(),最终会产生以下布局:
上文提到 redis不直接使用C语言字符串还有个缘由是为了定制的本身的内存重分配的方法,减小因堆内存申请与释放产生的时间开销。
若是redis使用的是普通的C语言字符串char*,那么每次拼接或者截断一个字符串以前都须要从新分配/释放内存,不然会形成内存溢出或泄露。可是每次进行分配/释放内存的操做又很是影响性能,因此redis作了两件事:
下面是sdsMakeRoomFor的源码:
//注意:这个函数是在扩充sds前调用,sds不会被扩充也不会改变len sds sdsMakeRoomFor(sds s, size_t addlen) { //addlen是扩充部分的长度 void *sh, *newsh; size_t avail = sdsavail(s); size_t len, newlen; char type, oldtype = s[-1] & SDS_TYPE_MASK; int hdrlen; /* Return ASAP if there is enough space left. */ if (avail >= addlen) return s;//若是足够存放扩充的部分,则直接返回不申请内存。 len = sdslen(s); sh = (char*)s-sdsHdrSize(oldtype); newlen = (len+addlen); if (newlen < SDS_MAX_PREALLOC) //扩充后的总长度小于1M(1024*1024),则直接多分配newlen个字节闲置。 newlen *= 2; else //扩充后的总长度大于1M(1024*1024),则多分配1M字节闲置 newlen += SDS_MAX_PREALLOC; type = sdsReqType(newlen);//根据扩充后的总长度决定须要这个sds要用什么类型的sdshdr /* Don't use type 5: the user is appending to the string and type 5 is * not able to remember empty space, so sdsMakeRoomFor() must be called * at every appending operation. */ if (type == SDS_TYPE_5) type = SDS_TYPE_8; hdrlen = sdsHdrSize(type); if (oldtype==type) { //若是扩充后的sdshdr类型不变,则在原有的地方realloc就好。由于len和alloc的类型仍是原来的。 //ps: s_realloc封装了realloc,realloc返回的指针未必是sh指向的地址,可能为了内存对齐移动了这块内存 newsh = s_realloc(sh, hdrlen+newlen+1); if (newsh == NULL) return NULL; s = (char*)newsh+hdrlen; } else { //若是扩充后的sdshdr类型变了,那就只能从新在别的地方分配内存,而后从新赋值,释放掉旧的内存。 /* Since the header size changes, need to move the string forward, * and can't use realloc */ newsh = s_malloc(hdrlen+newlen+1); if (newsh == NULL) return NULL; memcpy((char*)newsh+hdrlen, s, len+1); s_free(sh); s = (char*)newsh+hdrlen; s[-1] = type; sdssetlen(s, len); } sdssetalloc(s, newlen); return s; }
刚才已经建立了的sds"PHP is the best programming language",它的len是36,alloc也是36,如今给它扩充一下,把" in the world"加上,算上空格一共要加13个字节,这时会调用sdscatlen()函数完成整个拼接sds的操做,它内部须要先调用sdsMakeRoomFor(s, 13)走一遍内存重分配的算法,如下是sdsMakeRoomFor的流程
最终得到以下内存布局:
接下来sdscatlen()函数用memcpy把" in the world"append到这个已经经历过内存分配的sds的尾部,并更新len:
附上sdscatlen的代码:
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; }
我粗略地在源码里找了找,缩短sds字符串的有三个函数:sdsclear、sdstrim、sdsrange,他们都不会改变alloc的大小即不会释听任何内存,这就是sds字符串内存管理的一种方式:惰性释放。额外调用sdsRemoveFreeSpace释放内存,这样就节省了每次sds缩减长度而致使的内存释放开销。
三个缩短sds的函数就不一一介绍了,有兴趣直接去代码里看就好,须要注意的这些函数里移动字符串用的memmove()是容许内存重叠的,这点跟memcpy()不同。
下面介绍一下sdsRemoveFreeSpace,先放源码:
//这个函数压缩内存,让alloc=len。若是type变小了,则另开一片内存复制,若是type不变,则realloc sds sdsRemoveFreeSpace(sds s) { void *sh, *newsh; char type, oldtype = s[-1] & SDS_TYPE_MASK; int hdrlen; size_t len = sdslen(s); sh = (char*)s-sdsHdrSize(oldtype); type = sdsReqType(len); hdrlen = sdsHdrSize(type); //这以后的代码就跟sdsMakeRoomFor后面的代码差很少了,释放掉多余内存并重置alloc。 if (oldtype==type) { newsh = s_realloc(sh, hdrlen+len+1); if (newsh == NULL) return NULL; s = (char*)newsh+hdrlen; } else { newsh = s_malloc(hdrlen+len+1); if (newsh == NULL) return NULL; memcpy((char*)newsh+hdrlen, s, len+1); s_free(sh); s = (char*)newsh+hdrlen; s[-1] = type; sdssetlen(s, len); } sdssetalloc(s, len); return s; }
继续拿以前用sdscatlen()扩充的sds执行一遍sdsRemoveFreeSpace(),会获得以下布局:
以上就是sds相关代码分析,还有不少针对sds的基本操做函数在这里就不一一列举了。