Redis源码解读——sds
Posted WoLannnnn
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Redis源码解读——sds相关的知识,希望对你有一定的参考价值。
文章目录
“真实”的 sdshdr 结构
在 《Redis 设计与实现》中,提到 sds 的实现结构 sdshdr 是这样的:
struct sdshdr
// 记录buf数组已使用字节的数量
// 等于SDS所保存字符串的长度
int len;
// 记录buf数组中未使用的字节数
int free;
// 字节数组,用于保存字符串
char buf[];
;
这可能是 Redis 以前的版本是这样的,笔者查看的源码是 7.0
在 Redis 7.0 中,sdshdr (在 sds.h 中)结构是这样的:
/* 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[];
;
根据字符串长度的不同,用来存放它的 sdshdr 类型也是不同的
在这段代码的上方有一段注释,意思是sdshdr5 结构是不使用的(网上有人说是使用的,具体可以看【Redis源码分析】一个对SDSHDR5是否使用的疑问 - SegmentFault 思否)。
接下来我们分析除 sdshdr5 之外,其他结构中的各个成员:
len: 已使用的字符串长度;
alloc: 为字符串分配的空间总长度;
flags: 标记当前结构的类型,即当前结构体是 8/16/32/64 位;
flags 只使用低三位字节,高五位是不使用的。三位足够表示5个数字,不同的值代表不同的类型,下面是关于不同类型的常量定义:
#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
buf: 指向字符串;
现在我们可以观察到 《Redis 设计与实现》中的 sdshdr 与 7.0 的 sdshdr 的区别,多了一个 flag 标识,且并没有 free 属性,而是 alloc 属性,而 alloc - len 即代表 free 的值。
在定义 sdshdr 结构时,我们发现前面加上了 __attribute__ ((__packed__))
,它的作用是取消编译器的对齐,即结构 内的成员在内存中是紧凑的。
为什么不内存对齐呢?因为省一点点内存吗?
我们先看对齐后 SDS_TYPE_8
、 SDS_TYPE_16
、 SDS_TYPE_32
、SDS_TYPE_64
的内存布局是怎样的:
可以看到不同类型的 sdshdr 对齐的字节数不同,这就让 (char*)buf - 1
无法让每种 sdshdr 都定位到 flags 的地址,如果想通过 buf 定位到 flags 的地址,需要进行类型判断,并且不同的系统可能有不同的对齐方式。
那为了这个就舍弃内存对齐从而降低效率吗?
在一篇文章中解释了 Redis 的另外一种内存对齐((3条消息) redis源码解读(一):基础数据结构之SDS_czrzchao的博客-CSDN博客):
redis 通过自己在malloc等c语言内存分配函数上封装了一层zmalloc,将内存分配收敛,并解决了内存对齐的问题。在内存分配前有这么一段代码:
if (_n&(sizeof(long)-1)) _n += sizeof(long)-(_n&(sizeof(long)-1)); \\ // 确保内存对齐!
这段代码写的比较抽象,简而言之就是先判断当前要分配的_n
个内存是否是long
类型的整数倍,如果不是就在_n
的基础上加上内存大小差值,从而达到了内存对齐的保证。
虽然设计了 sdshdr 这几种结构,但实际在使用 sds 时,我们都是使用 sds 的接口来实现对 sds 的修改,并没有直接使用到这些结构。
通过 sds 获取其结构地址
在 sds.h 中,有这样两个宏:
#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))))
关于 sdshdr##T
中的 ##
我们就不详细解释了,在这里理解成它将 sdshdr
和 T
连接在一起,即表示不同的 sdshdr 类型
关于 SDS_HDR_VAR(T,s)
, 参数 s 是sdshdr 结构体中的字符串指针,即等价于 buf,参数 T 则是表示不同类型的 sdshdr,取值可以为 8/16/32/64。然后看其实现,struct sdshdr##T *sh
是宏定义的一个变量, void*
是将结果转换为void*
类型,以便 sh
接收。而 (s) - ( sizeof(struct sdshdr##T) )则表示指向结构体变量的地址。首先sizeof(struct sdshdr##T) 计算的大小不包含 buf (柔性数组特点),而 s 的指向的地址就跟在 sdshdr 之后(在接下来的 _sdsnewlen 函数中会实现),我们看图示:
注意,是 (s) - ( sizeof(struct sdshdr##T) )
,s 的值是所存放字符串的起始地址,而不是 s 的地址。因此,该表达式得到的是结构体变量 sdshdr##T
的起始地址。
关于变量 struct sdshdr##T *sh
大家可能会有疑惑,其实就是相当于在调用宏的地方定义了一个 sh
变量。我们用一个例子模拟一下:
#include <stdio.h>
#define INTPTR_VAR int* p = NULL;
int main()
int a = 10;
INTPTR_VAR;
p = &a;
printf("%d\\n", *p);
return 0;
结果:
输出 10
根据宏的特点,在预处理时,宏就直接被表达式替换了,上述例子就等价于:
#include <stdio.h>
int main()
int a = 10;
int* p = NULL; // INTPTR_VAR
p = &a;
printf("%d\\n", *p);
return 0;
至此,我们就明白了 SDS_HDR_VAR
是定义一个指向保存字符串 s 的结构体指针。
这样我们就好理解 SDS_HDR(T, S)
的作用了,即返回一个 保存字符串 s 的结构体地址
注意两者的差别:SDS_HDR_VAR
是定义变量保存结构体地址,SDS_HDR
是返回结构体地址。
关于sds属性的函数(sds.h)
sdslen
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;
顾名思义,该函数是用来获取字符串长度的。
参数 sds
:
typedef char * sds;
这里我们就知道了上面讲的 SDS_HDR
的作用了,通过字符串指针 s 来获取包含它的结构体地址,进一步访问其中的成员。
s[-1] 即表示 sdshdr 结构中的成员 flags,通过 flags 我们可以判断 sdshdr 的类型。
switch 的表达式 flags & SDS_TYPE_MASK
, SDS_TYPE_MASK 的定义:#define SDS_TYPE_MASK 7
,由于这里采用的是按位与操作,所以我们把 7 转换成二进制:000…0111,所以 SDS_TYPE_MASK
的作用就是取 flags 的低3位。
case 表达式的变量就是对应类型的值。
我们看进入 SDS_TYPE_5
时,返回的是一个宏 SDS_TYPE_5_LEN
的结果,SDS_TYPE_5_LEN
的定义:#define SDS_TYPE_5_LEN(f) ((f) >> SDS_TYPE_BITS)
, SDS_TYPE_BITS
的定义: #define SDS_TYPE_BITS 3
,由此我们可以看到,SDS_TYPE_5_LEN
是将 flags 右移三位,即返回 0。因为 sdshdr5 是不使用的,所以对其长度也返回 0.
其他 case 分支就是返回它们的 len
变量。
sdsavail
static inline size_t sdsavail(const sds s)
unsigned char flags = s[-1];
switch(flags&SDS_TYPE_MASK)
case SDS_TYPE_5:
return 0;
case SDS_TYPE_8:
SDS_HDR_VAR(8,s);
return sh->alloc - sh->len;
case SDS_TYPE_16:
SDS_HDR_VAR(16,s);
return sh->alloc - sh->len;
case SDS_TYPE_32:
SDS_HDR_VAR(32,s);
return sh->alloc - sh->len;
case SDS_TYPE_64:
SDS_HDR_VAR(64,s);
return sh->alloc - sh->len;
return 0;
返回 sds 的可用空间。sds 与传统的C字符串不同,C字符串以 ‘\\0’ 结尾,而 sds 则是为字符串多分配了一段空间,减少之后增容所带来的开销。
sdssetlen
static inline void sdssetlen(sds s, size_t newlen)
unsigned char flags = s[-1];
switch(flags&SDS_TYPE_MASK)
case SDS_TYPE_5:
unsigned char *fp = ((unsigned char*)s)-1;
*fp = SDS_TYPE_5 | (newlen << SDS_TYPE_BITS);
break;
case SDS_TYPE_8:
SDS_HDR(8,s)->len = newlen;
break;
case SDS_TYPE_16:
SDS_HDR(16,s)->len = newlen;
break;
case SDS_TYPE_32:
SDS_HDR(32,s)->len = newlen;
break;
case SDS_TYPE_64:
SDS_HDR(64,s)->len = newlen;
break;
修改 sds 的有效长度。
sdsinclen
static inline void sdsinclen(sds s, size_t inc)
unsigned char flags = s[-1];
switch(flags&SDS_TYPE_MASK)
case SDS_TYPE_5:
unsigned char *fp = ((unsigned char*)s)-1;
unsigned char newlen = SDS_TYPE_5_LEN(flags)+inc;
*fp = SDS_TYPE_5 | (newlen << SDS_TYPE_BITS);
break;
case SDS_TYPE_8:
SDS_HDR(8,s)->len += inc;
break;
case SDS_TYPE_16:
SDS_HDR(16,s)->len += inc;
break;
case SDS_TYPE_32:
SDS_HDR(32,s)->len += inc;
break;
case SDS_TYPE_64:
SDS_HDR(64,s)->len += inc;
break;
增加 sds 的有效长度。
sdsalloc
static inline size_t sdsalloc(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)->alloc;
case SDS_TYPE_16:
return SDS_HDR(16,s)->alloc;
case SDS_TYPE_32:
return SDS_HDR(32,s)->alloc;
case SDS_TYPE_64:
return SDS_HDR(64,s)->alloc;
return 0;
返回 sds 的已分配空间大小。也可以通过 sdsavail() + sdslen()
来获取
sdssetalloc
static inline void sdssetalloc(sds s, size_t newlen)
unsigned char flags = s[-1];
switch(flags&SDS_TYPE_MASK)
case SDS_TYPE_5:
/* Nothing to do, this type has no total allocation info. */
break;
case SDS_TYPE_8:
SDS_HDR(8,s)->alloc = newlen;
break;
case SDS_TYPE_16:
SDS_HDR(16,s)->alloc = newlen;
break;
case SDS_TYPE_32:
SDS_HDR(32,s)->alloc = newlen;
break;
case SDS_TYPE_64:
SDS_HDR(64,s)->alloc = newlen;
break;
设置 sds 的已分配空间大小
创建/修改/销毁 sds(sds.c)
要创建一个 sds 对象,首先要确认其 sds 结构属于哪一种,所以我们要根据字符串长度来选择 sdshdr 。
下面是根据字符串长度来确认 sds 类型的几个相关函数:
sdsReqType
static inline char sdsReqType(size_t string_size)
if (string_size < 1<<5)
return SDS_TYPE_5;
if (string_size < 1<<8)
return SDS_TYPE_8;
if (string_size < 1<<16)
return SDS_TYPE_16;
#if (LONG_MAX == LLONG_MAX) // 64位
if (string_size < 1ll<<32)
return SDS_TYPE_32;
return SDS_TYPE_64;
#else // 32 位
return SDS_TYPE_32;
#endif
根据字符串长度来确定 sds 的类型。
我们看其中有一个 #if
的预处理符号,在不同机器下(32 位与 64 位)返回不同结果。long 和 size_t 在 32位下是4字节,64位下是8字节
sdsHdrSize
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 的类型返回对应 sdshdr 的大小
sdsTypeMaxSize
static inline size_t sdsTypeMaxSize(char type)
if (type == SDS_TYPE_5)
return (1<<5) - 1;
if (type == SDS_TYPE_8)
return (1<<8) - 1;
if (type == SDS_TYPE_16)
return (1<<16) - 1;
#if (LONG_MAX == LLONG_MAX)
if (type == SDS_TYPE_32)
return (1ll<<32) - 1;
#endif
return -1; /* this is equivalent to the max SDS_TYPE_64 or SDS_TYPE_32 */
根据 sds 的类型返回其类型最大值
了解这些接口后,我们接下来介绍创建一个 sdshdr 结构体的函数:
sdsnewlen
sds sdsnewlen(const void *init, size_t initlen)
return _sdsnewlen(init, initlen, 0);
sdsnewlen
是用来创建一个长度为 initlen 的 sds,并使用 init 指向的字符串来初始化 sds ,如果 init 为 NULL,则将 sds 全部初始化为 ‘\\0’。可以看到它是调用了 _sdsnewlen
接口,该接口源码如下:
_sdsnewlen
sds _sdsnewlen(const void *init, size_t initlen, int trymalloc)
void *sh;
sds s;
char type = sdsReqType(initlen);
/* Empty strings are usually created in order to append. Use type 8
* since type 5 is not good at this. */
if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
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);
if (sh == NULL) return NULL;
if (init==SDS_NOINIT)
init = NULL;
else if (!init)
memset(sh, 0, hdrlen+initlen+1);
s = (char*)sh+hdrlen;
fp = ((unsigned char*)s)-1;
usable = usable-hdrlen-1<以上是关于Redis源码解读——sds的主要内容,如果未能解决你的问题,请参考以下文章