sds.c有一个 sdsTest(),里面有sds的单元测试。
src目录下的 make有如下
test-sds: sds.c sds.h
$(REDIS_CC) sds.c zmalloc.c -DSDS_TEST_MAIN $(FINAL_LIBS) -o /home/work/redis/redis-5.0.3/codetest/sds_test
执行 make test-sds后生成sds_test,可以开始调试sds功能。
使用gdb调试,打印 type参数 出现如下
p type
$2 =
经过查资料和多次尝试 在make中增加参数 -O0
test-sds: sds.c sds.h
$(REDIS_CC) sds.c zmalloc.c -DSDS_TEST_MAIN $(FINAL_LIBS) -O0 -o /home/work/redis/redis-5.0.3/codetest/sds_test
/home/work/redis/redis-5.0.3/codetest/sds_test
知识点
memmove
用法:#include
unsigned 表示无符号类型,可以表示大的整数,通常用于记数.
16位的unsigned int 允许的范围是0~65535 而 int 允许的范围是 -32768~32767
初始化没有预分配空间,执行拼接sdscatlen进行空间预分配。sdstrim 执行后没有释放原来的内存空间,实现了惰性空间。 sdscatlen的空间预分配和sdstrim惰性空间减少了 内存扩展次数。
sdsnewlen 通过指定 长度创建,是二进制安全的。遵循了C语言以空字符的惯例。可以重用
为了节省内存空间,sds根据数据长度定义了不同的结构体
sds分为结构体和数据体两个部分。
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; //已经使用数据体的字节长度
//已经分配的字节长度,包括已经使用和未使用的数据体, 不包括头部和空的终结符。
//初始化时 alloc==len,调用sdscatlent后 alloc>len
uint8_t alloc;
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[];
};
sds分为结构体和数据体两个部分,sds的实现是一个字节数组, 的前n个
字节存放结构体,记录sds长度,数据类型。 后面的字节存放数据体部分(char[] 类型)。
//新创建一个sds,重新申请动态空间
//sds初始化
sds x = sdsnew("foo");
sds sdsnew(const char *init) {
//获取C字符串长度
size_t initlen = (init == NULL) ? 0 : strlen(init);
return sdsnewlen(init, initlen);
}
sdsnewlen是二进制安全的,sdsnew 不是二进制安全的
sdsnew通过标准C字符串创建,空字符后的字符不会被使用,是测试时使用的。 sdsnewlen 通过指定 长度创建,是二进制安全的。
sds sdsnewlen(const void *init, size_t initlen) {
void *sh;
sds s;
//sds定义了SDS_TYPE_5,SDS_TYPE_8,SDS_TYPE_16,SDS_TYPE_32,SDS_TYPE_64这几种类型,对应sdshdr5,sdshdr8,sdshdr16,sdshdr32,sdshdr64 结构体。这样做是因为不同的结构体占用空间不同,有利于节省空间。
//根据initlen获取不同的长度
char type = sdsReqType(initlen);
//SDS_TYPE_5通常不使用
/* Empty strings are usually created in order to append. Use type 8
* since type 5 is not good at this. */
//空字符串通常用做附加前的初始化。这中情况 type8更好。
if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
//计算type对应的sdshdr长度
int hdrlen = sdsHdrSize(type);
unsigned char *fp; /* flags pointer. */
//申请内存
sh = s_malloc(hdrlen+initlen+1);
if (init==SDS_NOINIT)
init = NULL;
else if (!init)
memset(sh, 0, hdrlen+initlen+1);
if (sh == NULL) return NULL;
//s是最终返回的sds buf[] 部分,需要将指针移动到 buf[]开始位置
s = (char*)sh+hdrlen;
//fp指向 flags部分
fp = ((unsigned char*)s)-1;
switch(type) {
case SDS_TYPE_5: {
*fp = type | (initlen << SDS_TYPE_BITS);
break;
}
case SDS_TYPE_8: {
//内联函数 sh 指向sdshdr结构体
SDS_HDR_VAR(8,s);
// 为sdshdr len和alloc赋值
sh->len = initlen;
//初始化时没有预分配空间
sh->alloc = initlen;
// flags部分 赋值type
*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;
}
}
if (initlen && init)
memcpy(s, init, initlen);
s[initlen] = '\0';
return s;
}
/* Duplicate an sds string. */
//创建数据一致的sds
sds sdsdup(const sds s) {
return sdsnewlen(s, sdslen(s));
}
获取sds长度时 通过获取 指向 sds 结构体的指针,获取len属性。
两个重要内联函数的区别
//指针sh指向sdshdr结构体
#define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T)));
// 返回 指向sdshdr结构体的指针
#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))))
//计算type对应的结构体char(8byte)长度,计算规则为8byte为1字节。char buf[] 未初始化 不计算
//长度分别为 1,3,5,9,17
static inline int sdsHdrSize(char type) {
switch(type&SDS_TYPE_MASK) {
case SDS_TYPE_5:
return sizeof(struct sdshdr5);
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字符串拼接方法
sds sdscat(sds s, const char *t) {
return sdscatlen(s, t, strlen(t));
}
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;
}
//根据alloc判断目前预留的字节数是否够用,如不够长,执行内存扩展.
sds sdsMakeRoomFor(sds s, size_t 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);
//执行后 如果新的字符串长度小于1MB, alloc = (len+addlen)*2 否则 新字符串alloc长度等于 本身长度加上1MB
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);
//类型不变 只扩展内存
if (oldtype==type) {
newsh = s_realloc(sh, hdrlen+newlen+1);
if (newsh == NULL) return NULL;
s = (char*)newsh+hdrlen;
} else {
//类型改变,为新字符串重新申请足够内存,释放旧的字符串,修改 sdshdr.len属性。
/* 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);
}
//修改alloc属性
sdssetalloc(s, newlen);
return s;
}
执行sdscat后 alloc等于扩展后的长度,len等于 拼接后的字符串长度。
截取字符串-sdstrim
// 如 x = sdsnew("xxciaoyyy"); sdstrim(x,"xy"); 执行后结果是 "ciao"
//接受一个sds和一个C字符串, 从被截取的字符串的两端分别移除包含在C字符串中的字符,中间含有不截取.
// 如 x = sdsnew("xxcixyaoyyy"); sdstrim(x,"xy"); 执行后结果是 "cixyao"
sds sdstrim(sds s, const char *cset) {
char *start, *end, *sp, *ep;
size_t len;
sp = start = s;
ep = end = s+sdslen(s)-1;
//从前向后检查直到 sds中有没有包含C字符串的字符
while(sp <= end && strchr(cset, *sp)) sp++;
//从后向前检查直到 sds中有没有包含C字符串的字符
while(ep > sp && strchr(cset, *ep)) ep--;
//如果中间还有剩余字符串,计算剩余字符串长度。
//没有剩余的字符串,因使用memmove截取,len=0即可
len = (sp > ep) ? 0 : ((ep-sp)+1);
//截取剩余字符串。
// 注意这里 没有回收原来的空间,实现了惰性空间释放的特性。
if (s != sp) memmove(s, sp, len);
s[len] = '\0';
//set sds len
sdssetlen(s,len);
return s;
}
memset:初始化内存
sizeof(struct sdshdr8):计算结构体字节数,8byte为一字节
动态分配内存 malloc.h 函数
void *malloc(size_t _size):分配定长的内存
void *calloc(size_t __nmemb, size_t __size):分配__nmemb* __size的内存,并初始化为0
void *realloc (void *__ptr, size_t __size):将ptr指向的内存扩展内存为指定长度
返回 执向内存首地址的指针, void * 表示指针类型 未确定, 使用时 根据具体类型转换,一般是 char *
size_t定义
#define __SIZE_TYPE__ long unsigned int
typedef __SIZE_TYPE__ size_t;
unsigned long int,在C语言中指无符号长整型的,是整型(整数类型)变量的一种。本类型与unsigned long(“无符号长”)是等价的,即定义的时候int(“整数”)可以不写。 都是 8字节长度
本类型取值范围: 0~4294967295 即 0~(2的32次方-1)
tcmallc(thread-caching Malloc ) 与标准准库glibc实现同样的,但是 TCmalloc的性能 和效率 比标准 malloc高很多
在我的 64位 centos7.0 版本上 redis使用了 jemalloc库
local: zmalloc.h
检查是否使用 tcmalloc
#if defined(USE_TCMALLOC)
#define ZMALLOC_LIB ("tcmalloc-" __xstr(TC_VERSION_MAJOR) "." __xstr(TC_VERSION_MINOR))
#include
#if (TC_VERSION_MAJOR == 1 && TC_VERSION_MINOR >= 6) || (TC_VERSION_MAJOR > 1)
#define HAVE_MALLOC_SIZE 1
#define zmalloc_size(p) tc_malloc_size(p)
#else
#error "Newer version of tcmalloc required"
#endif
//检查是否使用 jemalloc
#elif defined(USE_JEMALLOC)
#define ZMALLOC_LIB ("jemalloc-" __xstr(JEMALLOC_VERSION_MAJOR) "." __xstr(JEMALLOC_VERSION_MINOR) "." __xstr(JEMALLOC_VERSION_BUGFIX))
#include
#if (JEMALLOC_VERSION_MAJOR == 2 && JEMALLOC_VERSION_MINOR >= 1) || (JEMALLOC_VERSION_MAJOR > 2)
#define HAVE_MALLOC_SIZE 1
#define zmalloc_size(p) je_malloc_usable_size(p)
#else
#error "Newer version of jemalloc required"
#endif
//检查是否是苹果系统
#elif defined(__APPLE__)
#include
#define HAVE_MALLOC_SIZE 1
#define zmalloc_size(p) malloc_size(p)
#endif
定义了 TCmalloc,jemalloc或是苹果系统 define HAVE_MALLOC_SIZE 1 ??
如果使用标准glibc malloc 由于内存对齐 原因,不能正确获取内存大小,需要在申请内存前面加上 PREFIX_SIZE类型长度,用于统计内存大小
如果使用 TCmalloc,jemalloc,apple-os malloc函数,本身支持获取内存大小, 则不需要 统计内存大小。
example 如下
void *zmalloc(size_t size) {
void *ptr = malloc(size+PREFIX_SIZE);
if (!ptr) zmalloc_oom_handler(size); //异常处理
#ifdef HAVE_MALLOC_SIZE //如果 记录了size 调用获取size的函数
update_zmalloc_stat_alloc(zmalloc_size(ptr)); // 累加 申请的内存
return ptr;
#else // 没有记录 size 在内存 前缀中存储size
*((size_t*)ptr) = size;
update_zmalloc_stat_alloc(size+PREFIX_SIZE);//累加 申请的内存
return (char*)ptr+PREFIX_SIZE;
#endif
}
zrealloc 也有同样的处理
void *zrealloc(void *ptr, size_t size) {
#ifndef HAVE_MALLOC_SIZE
void *realptr;
#endif
size_t oldsize;
void *newptr;
if (ptr == NULL) return zmalloc(size);
#ifdef HAVE_MALLOC_SIZE 如果 记录了size 调用获取size的函数
oldsize = zmalloc_size(ptr);
newptr = realloc(ptr,size);
if (!newptr) zmalloc_oom_handler(size);
//记录减少和增加内存长度,用于统计 总共占用的内存长度
update_zmalloc_stat_free(oldsize);
update_zmalloc_stat_alloc(zmalloc_size(newptr));
return newptr;
#else// 没有记录size 通过前缀获取
realptr = (char*)ptr-PREFIX_SIZE;
oldsize = *((size_t*)realptr);
newptr = realloc(realptr,size+PREFIX_SIZE);
if (!newptr) zmalloc_oom_handler(size);
*((size_t*)newptr) = size;
update_zmalloc_stat_free(oldsize+PREFIX_SIZE);
update_zmalloc_stat_alloc(size+PREFIX_SIZE);
return (char*)newptr+PREFIX_SIZE;
#endif
}
学到的知识 分为 以下部分
C语言基础
定义结构体,定义常量,内联函数。if()判定,sizeof() 结构体属性使用,指针使用。
sds结构设计
sds是一个字符串数组类型,头部存放 结构体。创建后的sds 指向了 数组的数据部分地址
根据数据长度 结构体长度不同。便于节省空间。
总结:原生的C字符串有 SDS优点相对应的缺点,导致redis需要封装原生C字符串来满足需求。设计上使用字符数组作为载体,分为结构体和数据体两个部分,比较巧妙。结构体一般由 已使用长度len, 已申请长度 alloc, 类型标识 flags组成,数组体部分 是 char数组。 初始化时将 char 指针指向数数据体部分,通过 移动指针 强转类型获取 结构体指针,来设置属性,然后 返回 指向 数据体的指针。后续其他函数获取或设置属性都是通过这种办法。
内存管理方面, 使用中间库 引入了tcmalloc,jemalloc库, 处理了 size获取的差异,记录了减少和增加内存长度。