redis6.0原始碼學習(二)sds
redis6.0原始碼學習(二)sds
文章目錄
1、資料結構
原始碼所在檔案 sds.h 和 sds.c
sds的定義
typedef char *sds;
sds字串根據字串的長度,劃分了五種結構體sdshdr5、sdshdr8、sdshdr16、sdshdr32、sdshdr64,分別對應的型別為SDS_TYPE_5、SDS_TYPE_8、SDS_TYPE_16、SDS_TYPE_32、SDS_TYPE_64
struct __attribute__ ((__packed__)) sdshdr5 { unsigned char flags; /* 低3位用來儲存型別,高5位用來儲存長度 */ char buf[]; }; struct __attribute__ ((__packed__)) sdshdr8 { uint8_t len; /* 字串在buf中實際佔用的位元組數(不包括'\0')*/ uint8_t alloc; /* 去除頭長度和結束符'\0'後的總長度 */ unsigned char flags; /* 低位的3個bit位用來表示結構型別,其餘5個bit位未使用 */ char buf[]; }; struct __attribute__ ((__packed__)) sdshdr16 { uint16_t len; /* 字串在buf中實際佔用的位元組數(不包括'\0')*/ uint16_t alloc; /* excluding the header and null terminator */ unsigned char flags; /* 低位的3個bit位用來表示結構型別,其餘5個bit位未使用 */ char buf[]; }; ......
- __attribute__ ((__packed__)) 告訴編譯分配的是緊湊記憶體,而不是位元組對齊的方式。
- len表示字串已使用的長度,buf長度
- alloc表示字串的容量
- flags表示字串型別標記SDS_TYPE_5、SDS_TYPE_8、SDS_TYPE_16、SDS_TYPE_32、SDS_TYPE_64
- buf[]表示柔性陣列。在分配記憶體的時候會指向字串的內容
2、sds建立
根據傳入的字串建立sds
/* Create a new sds string starting from a null terminated C string. */ sds sdsnew(const char *init) { size_t initlen = (init == NULL) ? 0 : strlen(init); return sdsnewlen(init, initlen); }
下面是主要建立的邏輯
sds sdsnewlen(const void *init, size_t initlen) { void *sh; sds s; //根據字串長度獲取SDS的型別 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. */ //分配記憶體 sh = s_malloc(hdrlen+initlen+1); 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; //根據型別初始化頭部、長度、容量、標記 switch(type) { case SDS_TYPE_5: { *fp = type | (initlen << SDS_TYPE_BITS); break; } case SDS_TYPE_8: { SDS_HDR_VAR(8,s); sh->len = initlen; sh->alloc = initlen; *fp = type; break; } case SDS_TYPE_16: { SDS_HDR_VAR(16,s); //使用巨集來獲取sdshdr結構體指標 sh->len = initlen; sh->alloc = initlen; *fp = type; break; } //......省略部分程式碼 } if (initlen && init) memcpy(s, init, initlen); s[initlen] = '\0'; return s; //返回值的首地址是buf地址,而不是整個sds的首地址 }
總結步驟就是:
- 1、根據傳入的字串長度獲取sds的型別(用適當的型別儲存,減少記憶體消耗)。如果初始化長度initlen為0,通常被認為要進行append操作,直接設定SDS型別為SDS_TYPE_8。
- 2、分配記憶體,大小為:hdrlen+initlen+1 (hdrlen:型別結構體的大小,initlen:字串大小, 1:\0 結束符的長度)
- 3、初始化sds結構體中alloc、len、 flag值
- 4、拷貝字串到sds結構體, 新增結束符
3、sds擴容
擴容
/* Enlarge the free space at the end of the sds string so that the caller
* is sure that after calling this function can overwrite up to addlen
* bytes after the end of the string, plus one more byte for nul term.
*
* Note: this does not change the *length* of the sds string as returned
* by sdslen(), but only the free buffer space we have. */
sds sdsMakeRoomFor(sds s, size_t addlen) {
void *sh, *newsh;
size_t avail = sdsavail(s); //獲取sds目前空餘的空間
size_t len, newlen;
char type, oldtype = s[-1] & SDS_TYPE_MASK; //獲取sds型別
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) //新的長度小於1024*1024,即兩倍的擴容
newlen *= 2;
else
newlen += SDS_MAX_PREALLOC; //新長度直接加上1024*1024大小
type = sdsReqType(newlen); //獲取儲存新sds的結構體型別
/* 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) {
//sds型別不變,重新分配記憶體
newsh = s_realloc(sh, hdrlen+newlen+1);
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 */
//sds型別發生改變,重新申請新記憶體
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); //更新sds已使用空間長度
}
sdssetalloc(s, newlen);//更新sds容量
return s;
}
步驟大致如下:
- 1、檢視sds中是否有足夠的剩餘空間容納addlen長度的字串,有則返回,無則繼續其它操作。
- 2、 計算需要重新分配的儲存空間的長度,包括原sds長度與addlen,另外預備一部分的剩餘空間。
- 3、如果新sds長度小於1M則預設兩倍擴容,否則只 擴容 (1M + addlen) 大小
- 4、根據新的長度,得到新的sds頭部型別,如果新的頭部型別與原型別相同,則使用s_realloc分配更多的空間;如果新的頭部型別與原型別不相同,則使用s_alloc重新分配記憶體,並將原sds內容copy到新分配的空間。
4、sds縮容
sds在縮容時,並不是立即釋放記憶體。比如sdstrim函式
/* Remove the part of the string from left and from right composed just of
* contiguous characters found in 'cset', that is a null terminted C string.
*
* 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("AA...AA.a.aa.aHelloWorld :::");
* s = sdstrim(s,"Aa. :");
* printf("%s\n", s);
*
* Output will be just "Hello World".
*從 SDS 左右兩端分別移除所有在 C 字串中出現過的字元。
*接受一個 SDS 和一個 C 字串作為引數, 從 SDS 左右兩端分別移除所有在 C 字串中出現過的字元。
*/
sds sdstrim(sds s, const char *cset) {
char *start, *end, *sp, *ep;
size_t len;
sp = start = s;
ep = end = s+sdslen(s)-1;
while(sp <= end && strchr(cset, *sp)) sp++;
while(ep > sp && strchr(cset, *ep)) ep--;
len = (sp > ep) ? 0 : ((ep-sp)+1);
if (s != sp) memmove(s, sp, len);
s[len] = '\0';
sdssetlen(s,len);
return s;
}
比如有個字串s1=“REDIS”,對s1進行sdstrim(s1," S")操作,執行完該操作之後Redis不會立即回收減少的部分,也就是而是會分配給下一個需要記憶體的程式。
下面的函式是真正釋放記憶體。
/* Reallocate the sds string so that it has no free space at the end. The
* contained string remains not altered, but next concatenation operations
* will require a reallocation.
*
* After the call, the passed sds string is no longer valid and all the
* references must be substituted with the new pointer returned by the call. */
sds sdsRemoveFreeSpace(sds s) {
void *sh, *newsh;
char type, oldtype = s[-1] & SDS_TYPE_MASK; //獲取sds型別
int hdrlen, oldhdrlen = sdsHdrSize(oldtype); //獲取sds頭的長度
size_t len = sdslen(s);
size_t avail = sdsavail(s); //獲取sds目前空餘的空間
sh = (char*)s-oldhdrlen;
/* Return ASAP if there is no space left. */
if (avail == 0) 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+1);
if (newsh == NULL) return NULL;
s = (char*)newsh+oldhdrlen;
} else {
newsh = s_malloc(hdrlen+len+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); //更新sds已使用空間長度
}
sdssetalloc(s, len);//更新sds容量
return s;
}
和擴容的步驟基本相似。
5、總結
SDS和C字串的區別
(1)獲取字串長度的時間複雜度
C字串不記錄字串本身是長度,因此需要遍歷字串才能得到字串的長度,時間複雜度為O(N),SDS字串用屬性len記錄了字串的長度,因此獲取字串長度的時間複雜度為O(1)。
(2)緩衝區溢位
如果對C字串使用strcat進行拼接,如果沒有提前對字串分配足夠的記憶體,則會導致緩衝區溢位。但是SDS會提前對字串所需要的記憶體進行檢測。
(3)修改字串時的記憶體重分配
當修改C字串的時候,無論是增長/縮短字串,都會通過記憶體重分配來擴充套件或者釋放記憶體,否則就會導致緩衝區溢位或者記憶體洩漏。SDS在擴容是採用的是預分配機制避免了頻繁擴容。
(4)二進位制安全性
如果一個字串儲存的時候是什麼樣子,輸出的時候也是什麼樣子,則稱為二進位制安全的,C字串以’\0’作為字串的結尾標識,會造成從中間截斷字串的情況。SDS使用len屬性記錄字串的長度,因此儲存二進位制資料是安全的。也就是說sds中間也可以存在 \0 字元。