SDS是一个C的动态字符串库,即

image.png
1554909660672013.png

redis抛弃了glibc的字符串库,自己实现了一个更好的字符串库,为什么要造这个轮子,SDS比起glibc的一套更好在哪儿?这就是我们要探究的核心问题。(以下代码位于sds.h)


SDS字符串的类型很简单

image.png
1554909827468057.png

当然不是每个人都熟悉C,在此简单解释一下这样做会有什么意义。C/C++中一个char*的指针,往往意味着它可以去逐字节的遍历其后内容,这是非常方便的。

下面有五个结构

image.png
1554910089292019.png

(PS:第一个结构sdshdr5从来不会被使用,它存在的意义不得而知。所有结构在之后用sdshdr*统一表示)

这里很多人多多少少会有一些疑惑的地方了,因为有没见过的类型与用法(即使是一些熟悉C的人)

首先关于类型unint*_t到底是什么。

很多人会猜它是int型的某种重定义?前缀u似乎是unsigned?恭喜猜对了,不是什么新类型,就是下面这个:

image.png
1554910649451579.png

以上位于/usr/include/stdint.h,正如其名,里面全部是对于int类型的重定义,其中的所有类型不会有因为不同的系统而导致长度不同,主要也就是为了移植性,在此不赘述。

关于struct后的__attributed__ ((packed)),很多人可能从来都不知道结构体后还能这样写,事实上这种用法在内核源码以及网络协议栈中非常的常用,这属于GNU的特色扩展属性,不经可以对类型设置,还可以对函数设置、变量设置,在此也不展开,有很多关于这个的参考资料。我们这的packed属性设定后,就是告诉编译器,不用你给我结构体对齐了,你挨个紧紧的存放!字节对齐的意义本来就只是为了提高CPU的访问效率(自行思考或查阅),这个对齐是否存在对于程序员来说并不会带来任何便,反而有些场景下会有麻烦。因此packed属性在很多场景下是必加的属性,最常见的是网络协议栈中,当传输结构性数据而不是字节流数据时,不同机器对其方式都可以不一样,如果大家都按自己的对齐后结构体来访问,那么根本不可能同一。这个属性在我们这最主要的好处就是可以很方便的在结构体内逐字节的精准移动,后面你会明白的。

关于sdshdr*结构中最后的长度不定的数组,可能和多人并没有见过,这是C语言中的柔性数组,在此不具体展开,其作用是在可以在结构体最后放一个长度不定或填为0的数组来用于存放长度不定的数据,柔性数组在我们这显然就是非常必要的,同时需要注意的是,在计算结构体类型的字节数时,柔性数组不计(柔性数组在某些网络数据的传输中也很常用,其他场景可能一般不太需要)。

最后解释一下sdshdr*中的hdr是指的什么,hdr即header,以免有些人不理解这个命名。

sdshdr*结构的内容存储结构:

image.png
1555066642140843.png

简单解释以下每个字段(不讨论sdshdr5这个从不被使用的特例)

len:当前字符串的实际长度

alloc:总容量

flags:低3比特位存类型,高5比特位暂时没有被使用

buf:内容

SDS优点 1:O(1)复杂度获取字符串长度,同时不以'\0’识别结尾因此二进制安全。


image.png
1554978983958606.png

flags字段的低3比特位用于存储类型,对应类型宏就是这五个,下面一个用来取出类型字段的MASK宏

下面解释一下两个比较重要的宏函数:

SDS_HDR_VAR(T,s):

首先先说明白一个问题,虽然这几个sds结构除字符串内容本身外还有好几个字段,但是这个对于使用者来说sds结构的其他字段是黑盒,交给(面向对象的设计里也常爱说暴露)用户或者说使用者的就是其中的char buf[ ]的buf,即字符串指针。

我们回到这个函数本身,显然这个函数是用来获取该sds的头部指针,并且下文可以直接使用一个叫sh的指针来进行操作。参数T是用于使命该sdshdr结构的类型,使用##连接宏连接"sdshdr"与参数T得到一个完整的sdshdr类型,例如T为"8"(注意宏不识别类型,因此参数直接就是8而不是字符串"8")便会拼接成,sdshdr8;参数s显然是sds结构中的buf,s指针减去sizeof(struct sdshdr##T)后,因为s是char*指针,那么便可以获取到s往前sizeof(struct sdshdr##T)字节的地址,事实上,也就是跳到了个这个sds字符串结构体的头部地址,即len对应的首地址,在这个函数里,这个地址直接被赋给了宏函数中定义的sh指针,因此在使用该宏的下文中,直接通过这个变量名为sh的指针就可以获取这个字符串的各种信息了。如下图

image.png
1555068802801198.png

第二个宏函数的作用也就不用多说了,直接返回sds的头部指针。


下面是一些获取len、alloc、flag等字段的基本方法,明白这两个宏函数后,每个函数都非常简单,同样,显然每个传入的参数都是sdshdr结构的buf字段

image.png
1554982458596497.png

获取字符串长度

image.png
1554982494390368.png

获取剩余可用长度,即容量alloc-当前长度len

image.png
1554982614309553.png

设定长度len字段

image.png
1554982683976598.png

增加长度len字段

image.png
1554982727904860.png

获取容量字段alloc

image.png
1554982763547907.png

设定容量字段alloc


下面是所有sds字符串的方法声明,glibc那一套有的这当然都有,接下来我们再继续探究某些方法,来看看SDS库到底好在哪儿,让redis不屑于使用glibc

image.png
1554983151600453.png

image.png
1554983182281887.png

本篇最后在此还是要说明一下关于sds的内存管理,目前redis默认就是使用自己的zmalloc,上面最后三个函数在文件sdsalloc.h中定义了其实现,如下

image.png
1554983481483929.png

当让你完全可以使用自己的一套malloc,如果有必要的话(极大多数情况下必然是不用的,相信zmalloc!)