redis5.0源码浅析1-sds

sds调试

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 = <optimized out>

经过查资料和多次尝试 在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 <string.h> 功能:由src所指内存区域复制count个字节到dest所指内存区域。 说明:src和dest所指内存区域可以重叠,但复制后src内容会被更改。函数返回指向dest的指针。

unsigned 表示无符号类型,可以表示大的整数,通常用于记数.

16位的unsigned int 允许的范围是0~65535 而 int 允许的范围是 -32768~32767

 

sds-简单动态字符串 优点如下

  1. 获取长度时间复杂度为O(1)(sdslen)
  2. 空间分配策略防止有缓存溢出。(sdscatlen)
  3. 通过 空间预分配和 惰性空间 减少 内存 扩展的次数。(sdscatlen,sdstrim)
  4. 二进制安全,可以保存任何二进制的数据(sdsnewlen)。
  5. 兼容C字符串。

初始化没有预分配空间,执行拼接sdscatlen进行空间预分配。sdstrim 执行后没有释放原来的内存空间,实现了惰性空间。 sdscatlen的空间预分配和sdstrim惰性空间减少了 内存扩展次数。

sdsnewlen 通过指定 长度创建,是二进制安全的。遵循了C语言以空字符的惯例。可以重用<string.h>中的函数,如:strcasecmp(sds->buf,"hello world "); strcat(c_string,sds->buf)

sds结构体分析

为了节省内存空间,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分为结构体和数据体两个部分,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;

}

 

 

SDS内存管理

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)

 

 

引用 tcmalloc,jemalloc库

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 <google/tcmalloc.h>

#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 <jemalloc/jemalloc.h>

#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 <malloc/malloc.h>

#define HAVE_MALLOC_SIZE 1

#define zmalloc_size(p) malloc_size(p)

#endif

定义了 TCmalloc,jemalloc或是苹果系统 define HAVE_MALLOC_SIZE 1 ??

 

malloc库size差异处理

 

如果使用标准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获取的差异,记录了减少和增加内存长度。

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值