redis 5.0.7 源码阅读——动态字符串sds
redis中动态字符串sds相关的文件为:sds.h与sds.c
一、数据结构
redis中定义了自己的数据类型"sds",用于描述 char*,与一些数据结构
typedef char *sds; /* Note: sdshdr5 is never used, we just access the flags byte directly.
* However is here to document the layout of type 5 SDS strings. */
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[];
};
定义结构体时,加上了 __attribute__ ((__packed__)) 关键字,用于取消字节对齐,使其按照紧凑排列的方式,占用内存。这样做的目的并不仅仅只是为了节约内存的使用。结构体最后有一个 char buf[],查了资料之后了解到,其只是定义一个数组符号,并没有任何成员,不占用结构体的内存空间,其真实地址紧随结构体之后,可实现变长结构体。由此可以只根据sds字符串的真实地址,取到sds结构体的任意成员变量数据。如flags:
void func(const sds s)
{
unsigned char flags = s[-];
}
这个flags,从源码的注释上看,其低三位用于表示sds类型,高五位是当sds类型为sdshdr5时,表明字符串长度的。对于非sdshdr5的类型,有专门的成员变量描述当前已使用的长度,及总buffer长度。
sds类型:
#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
sds定义了两个宏,用于获取sds结构体首地址:
#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))))
由此可见sds结构体的大致结构为:
/*
sdshdr5
+--------+----...---+
|00011000|abc\0 |
+--------+----...---+
|flags |buf sdshdr8
+--------+--------+--------+----...---+
|00000011|00000011|00000001|abc\0 |
+--------+--------+--------+----...---+
|len |alloc |flags |buf
*/
sds的一些常规操作,如获取字符串长度、获取剩余buf长度等,都是其于以上操作,首先根据sds字符串地址获取其flags的值,根据flags低三位判断sds类型,接着使用宏SDS_HDR_VAR或SDS_HDR进行操作。如:
#define SDS_TYPE_MASK 7 //0000,0111 static inline size_t sdslen(const sds s) {
//获取flags
unsigned char flags = s[-];
//根据flags低三位取类型,根据类型做不同处理
switch(flags&SDS_TYPE_MASK) {
case SDS_TYPE_5:
return SDS_TYPE_5_LEN(flags);
case SDS_TYPE_8:
return SDS_HDR(,s)->len;
case SDS_TYPE_16:
return SDS_HDR(,s)->len;
case SDS_TYPE_32:
return SDS_HDR(,s)->len;
case SDS_TYPE_64:
return SDS_HDR(,s)->len;
}
return ;
}
关于sds结构体中的len与alloc,len表示的是sds字符串的当前长度,alloc表示的是buf的总长度。
二、一些操作。
首先是一个根据字符串长度来决定sds类型的方法
static inline char sdsReqType(size_t string_size) {
if (string_size < <<) //flags高五位最大数字为 1<<5 - 1
return SDS_TYPE_5;
if (string_size < <<) //uint8_t 最大数字为 1<<8 - 1
return SDS_TYPE_8;
if (string_size < <<) //uint16_t 最大数字为 1<<16 - 1
return SDS_TYPE_16;
#if (LONG_MAX == LLONG_MAX) //区分32位/64位系统
if (string_size < 1ll<<)
return SDS_TYPE_32;
return SDS_TYPE_64;
#else
return SDS_TYPE_32;
#endif
}
创建一个新的sds结构体:
sds sdsnewlen(const void *init, size_t initlen) {
void *sh;
sds s;
char type = sdsReqType(initlen);
if (type == SDS_TYPE_5 && initlen == ) type = SDS_TYPE_8;
int hdrlen = sdsHdrSize(type);
unsigned char *fp; /* flags pointer. */ sh = s_malloc(hdrlen+initlen+);
if (init==SDS_NOINIT)
init = NULL;
else if (!init)
memset(sh, , hdrlen+initlen+);
if (sh == NULL) return NULL;
s = (char*)sh+hdrlen;
fp = ((unsigned char*)s)-;
switch(type) {
case SDS_TYPE_5: {
*fp = type | (initlen << SDS_TYPE_BITS);
break;
}
case SDS_TYPE_8: {
SDS_HDR_VAR(,s);
sh->len = initlen;
sh->alloc = initlen;
*fp = type;
break;
}
case SDS_TYPE_16: {
//同SDS_TYPE_8,略
}
case SDS_TYPE_32: {
//同SDS_TYPE_8,略
}
case SDS_TYPE_64: {
//同SDS_TYPE_8,略
}
}
if (initlen && init)
memcpy(s, init, initlen);
s[initlen] = '\0';
return s;
}
由外部指定初始字符串与初始长度。先根据长度获取sds类型,然后根据不同类型,可以获得实际需要的总内存空间大小(包括sds结构体长度)。值得注意的是,如果初始长度为0的情况下,若为SDS_TYPE_5,则会被强制转为SDS_TYPE_8。根据源码的注释,空串的定义,通常是为了向后追加内容。SDS_TYPE_5并不适合这种场景。分配完内存空间之后,设置好sds结构体的值,再把初始字符串拷至sds字符串的实际初始位置上(如果有),就可以了。
本方法做为最底层的sds字符串初始化接口,被其它接口所调用,如:
//空string
sds sdsempty(void) {
return sdsnewlen("",);
} //指定string
sds sdsnew(const char *init) {
size_t initlen = (init == NULL) ? : strlen(init);
return sdsnewlen(init, initlen);
} //从现有sds string拷贝
sds sdsdup(const sds s) {
return sdsnewlen(s, sdslen(s));
}
sds的释放也不是简单地free sds字符串,同样,它要先找到sds结构体的首地址,再进行free:
void sdsfree(sds s) {
if (s == NULL) return;
s_free((char*)s-sdsHdrSize(s[-]));
}
做为一个变长字符串,与传统c字符串,最大的区别,是可以动态扩展,就像c++ stl里的变长数组 vector一样。sds的扩容有自己的机制:
sds sdsMakeRoomFor(sds s, size_t addlen) {
void *sh, *newsh;
size_t avail = sdsavail(s);
size_t len, newlen;
char type, oldtype = s[-] & 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)
newlen *= ;
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+);
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(hdrlen+newlen+);
if (newsh == NULL) return NULL;
memcpy((char*)newsh+hdrlen, s, len+);
s_free(sh);
s = (char*)newsh+hdrlen;
s[-] = type;
sdssetlen(s, len);
}
sdssetalloc(s, newlen);
return s;
}
本方法用于扩容sds,并可以指定长度。首先其先取出了当前还空闲的buf长度,方法如下:
static inline size_t sdsavail(const sds s) {
unsigned char flags = s[-];
switch(flags&SDS_TYPE_MASK) {
case SDS_TYPE_5: {
return ;
}
case SDS_TYPE_8: {
SDS_HDR_VAR(,s);
return sh->alloc - sh->len;
}
case SDS_TYPE_16: {
SDS_HDR_VAR(,s);
return sh->alloc - sh->len;
}
case SDS_TYPE_32: {
SDS_HDR_VAR(,s);
return sh->alloc - sh->len;
}
case SDS_TYPE_64: {
SDS_HDR_VAR(,s);
return sh->alloc - sh->len;
}
}
return ;
}
若当前空闲的长度,比需要的长度大,则认为不用再额外分配空间,直接return。否则就启用扩容操作。
扩容时,先根据当前已使用的长度len与需要增加的长度addlen,算出一个初始新长度newlen,然后对其进行判断,若newlen大于1M,则在newlen的基础上,继续增加1M,否则直接翻倍。然后再根据newlen的最终大小,获取sds的新类型。此时,若类型依然为SDS_TYPE_5,也要强行修正为SDS_TYPE_8。因为SDS_TYPE_5类型并不知道当前空闲空间的大小。此时,若sds的新类型与原来相同,则只需要调用realloc重新分配一下空间即可。此方法会分配出一块新空间的同时,把原来空间的内容拷过去,并释放原有空间。而sds类型发生改变的时候,就需要手动新造一个新的sds了。扩容完成之后,需要修正一下当前已使用的空间len,与总buf大小 alloc。
扩容完成之后,或者是其它什么操作,如人工修改了sds字符串,并更新的len的情况下,会存在空闲空间太大的情况。此时如果想释放这部分空间,sds也提供了相应的操作:
sds sdsRemoveFreeSpace(sds s) {
void *sh, *newsh;
char type, oldtype = s[-] & 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 == ) 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+);
if (newsh == NULL) return NULL;
s = (char*)newsh+oldhdrlen;
} else {
newsh = s_malloc(hdrlen+len+);
if (newsh == NULL) return NULL;
memcpy((char*)newsh+hdrlen, s, len+);
s_free(sh);
s = (char*)newsh+hdrlen;
s[-] = type;
sdssetlen(s, len);
}
sdssetalloc(s, len);
return s;
}
操作与扩容类似,同样是会根据sds类型是否发生变化 ,来决定是使用realloc还是重新造一个sds。
除此之外,sds还实现了一些转义、数据类型转换、一些类似c风格的字符串操作等。如:strcpy、strcat、strlen、strcmp等。只是其更加多样化,如sds的strcat实现,就可以支持类似printf的方式。如:
/* Like sdscatprintf() but gets va_list instead of being variadic. */
sds sdscatvprintf(sds s, const char *fmt, va_list ap) {
va_list cpy;
char staticbuf[], *buf = staticbuf, *t;
size_t buflen = strlen(fmt)*; /* We try to start using a static buffer for speed.
* If not possible we revert to heap allocation. */
if (buflen > sizeof(staticbuf)) {
buf = s_malloc(buflen);
if (buf == NULL) return NULL;
} else {
buflen = sizeof(staticbuf);
} /* Try with buffers two times bigger every time we fail to
* fit the string in the current buffer size. */
while() {
buf[buflen-] = '\0';
va_copy(cpy,ap);
vsnprintf(buf, buflen, fmt, cpy);
va_end(cpy);
if (buf[buflen-] != '\0') {
if (buf != staticbuf) s_free(buf);
buflen *= ;
buf = s_malloc(buflen);
if (buf == NULL) return NULL;
continue;
}
break;
} /* Finally concat the obtained string to the SDS string and return it. */
t = sdscat(s, buf);
if (buf != staticbuf) s_free(buf);
return t;
} /* Append to the sds string 's' a string obtained using printf-alike format
* specifier.
*
* After the call, the modified sds string is no longer valid and all the
* references must be substituted with the new pointer returned by the call.
*
* Example:
*
* s = sdsnew("Sum is: ");
* s = sdscatprintf(s,"%d+%d = %d",a,b,a+b).
*
* Often you need to create a string from scratch with the printf-alike
* format. When this is the need, just use sdsempty() as the target string:
*
* s = sdscatprintf(sdsempty(), "... your format ...", args);
*/
sds sdscatprintf(sds s, const char *fmt, ...) {
va_list ap;
char *t;
va_start(ap, fmt);
t = sdscatvprintf(s,fmt,ap);
va_end(ap);
return t;
}
其它方法在此不再过多描述
三、sds相比c的标准库优势:
1、相比于c标准库,获取字符串的len复杂读从O(N)降低到O(1),sds结构中存储了字符串的长度,所以类似strlen(str)的操作不会成为redis的性能瓶颈。
2、在内存分配策略上,redis总是会尝试多分配一些空间,比如小于1MB的字符串,总是分配2倍内存空间,对于大于1MB的空间追加1MB冗余空间,这对于字符串操作(如strcat等)能减少重新内存分配的几率,提升运行性能。
3、SDS总是安全的,sds总是会自动追加字符串结尾符号’\0’,有效防止溢出发生。
4、惰性释放内存,改变原字符串时,标准库需要重新分配内存的复杂度为O(N),SDS最大为O(N),最优情况下无需重新分配内存空间。
redis 5.0.7 下载链接
http://download.redis.io/releases/redis-5.0.7.tar.gz
源码阅读顺序参考:
https://github.com/huangz1990/blog/blob/master/diary/2014/how-to-read-redis-source-code.rst
其它参考资料:
redis 5.0.7 源码阅读——动态字符串sds的更多相关文章
- redis 5.0.7 源码阅读——整数集合intset
redis中整数集合intset相关的文件为:intset.h与intset.c intset的所有操作与操作一个排序整形数组 int a[N]类似,只是根据类型做了内存上的优化. 一.数据结构 ty ...
- redis 5.0.7 源码阅读——跳跃表skiplist
redis中并没有专门给跳跃表两个文件.在5.0.7的版本中,结构体的声明与定义.接口的声明在server.h中,接口的定义在t_zset.c中,所有开头为zsl的函数. 一.数据结构 单个节点: t ...
- redis 5.0.7 源码阅读——字典dict
redis中字典相关的文件为:dict.h与dict.c 与其说是一个字典,道不如说是一个哈希表. 一.数据结构 dictEntry typedef struct dictEntry { void * ...
- redis 5.0.7 源码阅读——双向链表
redis中双向链表相关的文件为:adlist.h与adlist.c 一.数据结构 redis里定义的双向链表,与普通双向链表大致相同 单个节点: typedef struct listNode { ...
- redis 5.0.7 源码阅读——压缩列表ziplist
redis中压缩列表ziplist相关的文件为:ziplist.h与ziplist.c 压缩列表是redis专门开发出来为了节约内存的内存编码数据结构.源码中关于压缩列表介绍的注释也写得比较详细. 一 ...
- Linux 0.11源码阅读笔记-文件管理
Linux 0.11源码阅读笔记-文件管理 文件系统 生磁盘 未安装文件系统的磁盘称之为生磁盘,生磁盘也可以作为文件读写,linux中一切皆文件. 磁盘分区 生磁盘可以被分区,分区中可以安装文件系统, ...
- Linux 0.11源码阅读笔记-中断过程
Linux 0.11源码阅读笔记-中断过程 是什么中断 中断发生时,计算机会停止当前运行的程序,转而执行中断处理程序,然后再返回原被中断的程序继续运行.中断包括硬件中断和软件中断,硬中断是由外设自动产 ...
- Linux 0.11源码阅读笔记-总览
Linux 0.11源码阅读笔记-总览 阅读源码的目的 加深对Linux操作系统的了解,了解Linux操作系统基本架构,熟悉进程管理.内存管理等主要模块知识. 通过阅读教复杂的代码,锻炼自己复杂项目代 ...
- redis 4.0.8 源码包安装集群
系统:centos 6.9软件版本:redis-4.0.8,rubygems-2.7.7,gcc version 4.4.7 20120313,openssl-1.1.0h,zlib-1.2.11 y ...
随机推荐
- IntelliJ IDEA编辑文件的时候CPU飙高问题的解决
原文地址:https://www.javatang.com/archives/2018/04/26/25582403.html 上篇文章中说明了解决IntelliJ IDEA中文输入法无提示的问题,最 ...
- typescript step by step two
- python应用airtest库的环境搭建
参考https://blog.csdn.net/ywyxb/article/details/64126927 注意:无论是在线还是离线安装,最好在管理员权限下执行命令 1.安装Python36(32位 ...
- SpringMVC 中的异常处理
目录 1.搭建编码分析 2.编写异常类 3.编写自定义异常处理器 4.在springmvc.xml中配置异常处理器 5.编写Error.jsp.index.jsp页面 6.编写collector代码模 ...
- Linux开发环境准备(一)
工欲善其事,必先利其器 今天是腊月廿九,明天春节.我想着在今天对2019年所学到的东西做下总结. 从操作系统的安全性,系统代码是否开源,对开发人员的友好性,以及学习和工作的需求,我最终选择了Linux ...
- WEB打印控件LODOP的使用
有人说她是Web打印控件,因为她能打印.在浏览器中以插件的形式出现,用简单一行语句就把整个网页打印出来: 有人说她是打印编程接口,因为她介于浏览器和打印设备之间,是个通道和桥梁,几乎能想到的打印控制事 ...
- Kubernetes学习(一)
一 安装 三台机器搭建K8S集群,一台master两台woker Docker 18.09.0---kubeadm-1.14.0-0 kubelet-1.14.0-0 kubectl-1.14.0-0 ...
- 最好用的web端代码文本编辑器ACE
使用足够简单,功能足够强大,体验足够优秀 之前有一个系列文章介绍我在运维系统开发过程中用到的那些顺手的前端插件,总共发了四篇文章介绍了三个非常棒的插件,分别是bootstrap-duallistbox ...
- openstack启动云主机的流程
看一下openstack启动云主机的流程图 通过上图分析可以知道: 1. 用户使用Dashboard或者CLI 把认证信息通过REST请求发送给keystone进行认证.2. Keystone校验用户 ...
- 使用IntelliJ IDEA创建Maven工程
1. 新建项目(点击“+ Create New Project”): 2. 选择Maven,不必选择项目骨架,直接点击Next 按钮即可: 3. 输入项目名称,选择项目位置,输入组织名称,模块名称,项 ...