Redis底层数据结构分析——sds(simple dynamic string)
sbs源码分析
Redis字符串
sds(Simple Dynamic String)实现了一个简易的std::string,具备动态内存的特性,与std::string作为一个类不同,sds看起来就是char*
。
redis/src/sds.h
约300行,.c
文件约1300行+300行单测。
先在第一部分将涉及到的语言部分放上来
1. C 语法相关
1.1 __attribute__
关键字
声明函数,结构类型的属性值,给编译器看的。常见的如:
- `__attribute__((noreturn))` 不返回
- `__attribute__((packed))`取消内存对齐
- `__attribute__((__molloc__))`molloc返回指针不能指向已存在object
- `__attribute__((__nothrow__))`不抛异常
- `__attribute__ ((__leaf__))`不会再调用其他函数
struct node_{
char flag;
int id;
char buf[];
};
struct __attribute__((__packed__)) node{
char flag;
int id;
char buf[];
};
sizeof(node_)=8,sizeof(node)=5
1.2 C语言柔性数组
结构体中包含两个及以上属性,且定义最后一个元素为定义大小(或0)的数组时,该数组不计入结构体大小的计算,对struct申请内存是注意内存大小加上数组大小。realloc时,新旧的内存是连续的。
1.3 宏定义中的##
内容连接作用:`int##x=intx`
1.4 内存相关函数
-
malloc C语言中最常用的堆内存申请函数,gcc>3.3的定义为:
void *malloc (size_t __size) __THROW __attribute_malloc__ __wur
__THROW=define __THROW __attribute__ ((__nothrow__ __LEAF))
禁用throw;不会调用其他函数;不会返回错误码 -
size_t malloc_usable_size (void *__ptr) __THROW; 返回指针__ptr指向内容申请的内存大小
-
void *realloc (void *__ptr, size_t __size) __THROW __attribute_warn_unused_result__;
注意这里没有使用__attribute__((__malloc__))
表明其返回值可以是原指针 -
redis的sdsalloc.h文件中使用的内存相关函数,会使用原子变量记录memory_used,可能后面又内存动态管理吧,内存管理设计内容太丰富了
#define atomicIncr(var,count) atomic_fetch_add_explicit(&var,(count),memory_order_relaxed)
#define atomicDecr(var,count) atomic_fetch_sub_explicit(&var,(count),memory_order_relaxed)
2. sds定义
2.1 第一印象
初次打开redis源码文件 redis/src/sds.h
时, sds的定义是typedef char *sds;
,怎么不说是字符串对象吗?怎么是个char类型的指针,哪道是在字符串中定义有隐含的协议,例如:xxyyzz
,总长度表示长度,yy表示已经使用的长度,zz是实际内容? 带着这种疑问,查看了操作sds的API,发现这其中的秘密,sds与其背后的结构体定义。这里我觉得需要关注的是两者之间是如何转化的。 因为如果是我,也许直接让sds为其与之定义的结构体了
补:看完源码之后,多个操作会改变sds对应的sdshr结构,但是这种header的改变对调用方来说是无感知的,直接使用结构体则不然。
2.2 sds背后的结构体
先看源码中的定义,这里__attribute__
关键字的作用见 1.1, 柔性数组见 1.2.
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* used */
uint8_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
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[];
};
- len: buf已使用大小
- alloc: buf当前总的内存大小
- flags:标记uintxx_t,可能为下列值
#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
这里为什么不直接定义为uintxx_t对应的大小?
这种二进制(00,01,10,11,100)的定义方式,通过一个掩码与操作,可以达到与enum相同的目的, flag&b111, 结果被限制为上述几种宏的值
- buf:实际sds存储数据的buf, 也就是说sds值为buf的值,数组的起始地址。
现在知道sds指向buf,那么如何获取结构体的其他属性呢?-> 指针,由于内存对齐且unsiged char占用一个字节, 在内存连续的情况下,那么buf的前一个字节内存存储的就是flags的内容,通过flags能够获得整个结构体大小n,那么buf的前n个字节就是结构体的起始地址,通过强转得到指向该结构体的指针,从而获取结构体属性.看下源码是怎么实现的
2.2.1 以strlen函数看一下char* 和 sds结构体的转换
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;
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;
}
- 第一眼看到
s[-1]
,通常自己是不会这么写的。s[-1]等价于 s-1, 得益于s指向的内容单个元素大小恰为1byte,等于unsigend char大小,s-1指向flags,所以 s[-1]为flags的内容. - 获取到flags标识后通过
SDS_HDR
(sds_header)宏获得结构体起始地址
SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))))
s指针向后移动header大小到结构体起始地址,然后强转为sdshdT类型指针,就此实现 sds->sdshdT的转换.之后就可以通过指针去访问len,alloc,flags等属性。那么这一步为什么不继续使用指针后移然后进行强转了呢,例如sdshd8,flags向后移动一位指向alloc,将这片内存强转为uint8,例如:*(uint8_t*)(s-2)
?
2.2.2 转换相关的简易api
- sdsavail(alloc - len),sdssetlen(重设sds的len属性),sdsinclen(增加sds的len属性),sdsalloc(获取alloc属性),sdssetalloc(设置alloc), 这些api都是将sds转化为结构体后对其属性的读写操作。
2.3 sds对象相关操作
- 创建对象
- 根据输入initLen判断使用的sdshrT
- 申请 initLen+1+header大小的内存
- 根据类型填充sdshr其他属性值
sds _sdsnewlen(const void *init, size_t initlen, int trymalloc) {
void *sh;
sds s;
// 根据初始长度,判断使用哪种类型的sdshd类型与sds对应
char type = sdsReqType(initlen);
/* Empty strings are usually created in order to append. Use type 8
* since type 5 is not good at this. */
// 空字符串类型,初始化为最短的uint8_t相关的sdshr
if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
// 获取该类型header的大小
int hdrlen = sdsHdrSize(type);
unsigned char *fp; /* flags pointer. */
size_t usable;
assert(initlen + hdrlen + 1 > initlen); /* Catch size_t overflow */
sh = trymalloc?
s_trymalloc_usable(hdrlen+initlen+1, &usable) :
s_malloc_usable(hdrlen+initlen+1, &usable);
// 可以看到,此时申请内存大小为输入长度+header大小+'\0', usable代表实际分配的内存大小
// 例如hdrlen+initlen+1=15, useable可能为24
if (sh == NULL) return NULL;
if (init==SDS_NOINIT)
init = NULL;
else if (!init)
memset(sh, 0, hdrlen+initlen+1);
s = (char*)sh+hdrlen; // 使s指向buf
fp = ((unsigned char*)s)-1; // 指向flags
usable = usable-hdrlen-1; // buf还可以使用的内存大小
if (usable > sdsTypeMaxSize(type))
usable = sdsTypeMaxSize(type);
// 根据类型,初始化 len,flags,alloc属性
switch(type) {
case SDS_TYPE_5: {
*fp = type | (initlen << SDS_TYPE_BITS);
break;
}
case SDS_TYPE_8: {
SDS_HDR_VAR(8,s);
sh->len = initlen;
sh->alloc = usable;
*fp = type;
break;
}
case SDS_TYPE_16: {
SDS_HDR_VAR(16,s);
sh->len = initlen;
sh->alloc = usable;
*fp = type;
break;
}
case SDS_TYPE_32: {
SDS_HDR_VAR(32,s);
sh->len = initlen;
sh->alloc = usable;
*fp = type;
break;
}
case SDS_TYPE_64: {
SDS_HDR_VAR(64,s);
sh->len = initlen;
sh->alloc = usable;
*fp = type;
break;
}
}
if (initlen && init)
memcpy(s, init, initlen);
// 自己管理'\0'
s[initlen] = '\0';
return s;
}
- _sdsMakeRoomFor sds内存扩展
当sds变长时,可能没有足够的内存,这时需要扩展内存
- 这种扩展可以保证sds对象是内存连续的
- 如果可用空间不小于addlen时不进行扩展
- 计算新长度,newlen,预分配模式下,若是newlen<1M,则扩展长度为newlen的两倍;否则为newlen+1M
- 获取sdshrT的类型,若是类型没有发生变化,在原起始地址处realloc;若是类型发生变化,sds头需要改变,则需要重新申请内存,拷贝原内容到新地址
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;
/* Return ASAP if there is enough space left. */
if (avail >= addlen) return s;
len = sdslen(s);
sh = (char*)s-sdsHdrSize(oldtype);
reqlen = newlen = (len+addlen);
assert(newlen > len); /* Catch size_t overflow */
if (greedy == 1) {
if (newlen < SDS_MAX_PREALLOC)
newlen *= 2;
else
newlen += SDS_MAX_PREALLOC;
}
type = sdsReqType(newlen);
/* 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);
assert(hdrlen + newlen + 1 > reqlen); /* Catch size_t overflow */
if (oldtype==type) {
newsh = s_realloc_usable(sh, hdrlen+newlen+1, &usable);
if (newsh == NULL) return NULL;
s = (char*)newsh+hdrlen;
} else {
/* Since the header size changes, need to move the string forward,
* and can't use realloc */
newsh = s_malloc_usable(hdrlen+newlen+1, &usable);
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);
}
usable = usable-hdrlen-1;
if (usable > sdsTypeMaxSize(type))
usable = sdsTypeMaxSize(type);
sdssetalloc(s, usable);
return s;
}
- sdsRemoveFreeSpace
同理,也需要对内存进行释放,例如sbs结构向上增加后buf大小没有得到利用,len/alloc较小
- 根据已经使用的空间len计算类型sdshrT
- 若类型相同,或者是类型能够包下sdshrT,则header不需要变化,直接进行realloc,较小空间
- 若类型变小(SDS_TYPE_8=sdshr8类型),header也需要缩小,直接重新申请内存,copy
sds sdsRemoveFreeSpace(sds s) {
void *sh, *newsh;
char type, oldtype = s[-1] & SDS_TYPE_MASK;
int hdrlen, oldhdrlen = sdsHdrSize(oldtype);
size_t len = sdslen(s);
size_t avail = sdsavail(s);
sh = (char*)s-oldhdrlen;
/* Return ASAP if there is no space left. */
if (avail == 0) return s;
/* Check what would be the minimum SDS header that is just good enough to
* fit this string. */
type = sdsReqType(len);
hdrlen = sdsHdrSize(type);
/* If the type is the same, or at least a large enough type is still
* required, we just realloc(), letting the allocator to do the copy
* only if really needed. Otherwise if the change is huge, we manually
* reallocate the string to use the different header type. */
if (oldtype==type || type > SDS_TYPE_8) {
newsh = s_realloc(sh, oldhdrlen+len+1);
if (newsh == NULL) return NULL;
s = (char*)newsh+oldhdrlen;
} 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;
}
- 内存安全的API示例
在进行内容拷贝,连接等操作时会检查可用内存是否充足,否则调用 _sdsMakeRoomFor进行内存的扩展,例如sdscpylen
sds sdscpylen(sds s, const char *t, size_t len) {
if (sdsalloc(s) < len) {
// 当前可用内存小于len,则扩充 len-sdslen(s),默认是使用预分配算法的
s = sdsMakeRoomFor(s,len-sdslen(s));
if (s == NULL) return NULL;
}
memcpy(s, t, len);
s[len] = '\0';
sdssetlen(s, len);
return s;
}
2.4 sds结构体相比于char*的优势
- 首先sds结构体相当于在char*上加了一个header,多使用了部分内存(sizeof(header)+1)
- O(1) 时间获取字符串长度
- +1的字节是存储’\0’,也就是说对于内容中包含’\0’的字符串与普通字符串同等对待
- 由于记录了alloc和len的大小,在涉及到内存操作时api多了一步判断,增强了安全性
- 得益于alloc的伸缩方案,减少了频繁的内存申请和销毁
3.总结
- 理解sdshrT的定义,通过新增len,alloc属性,方便获取sds长度,进行更安全的内存操作和动态伸缩
- 采用柔性数组存储实际的字符串内容,自己管理null-terminated;在内存伸缩时,header变长必须重新malloc分配内存,否则realloc
- sds通过禁用内存对齐从而 sds-1获取flags, sds-header_size获取sdshrT, 实现sds到sdshrT对象转换;api入参和返回值仅是sds,实现统一
- 内存预分配时,1M是一个阈值,低于1M变为预设长度2倍,超出1M,每次新增1M
- 若非显示的调用内存缩小api,一般api操作引起的buf实际长度的较小,并不会立即释放多余的空间,仅是修改len长度。
在涉及内存边长的API中会显示调用_sdsMakeRoomFor,但是但从sds定义的文件中没有看到显示调用sdsRemoveFreeSpace或Resize,
那么什么时候显示调用内存缩小呢?