最近打算閱讀redis源碼,可是擔憂讀完就忘了,因此決定把閱讀的筆記在簡書裏記錄起來,但願可以堅持讀下去。之因此選擇3.2是由於公司把redis升級成了這個版本。程序員
redis在處理字符串的時候沒有直接使用以'0'結尾的C語言字符串,而是封裝了一下C語言字符串並命名爲sds(simple dynamic string),在sds.h文件裏咱們能夠看到以下類型定義:typedef char *sds;
也就是說實際上sds類型就是char*類型,那sds和char*有什麼區別呢?
主要區別就是:sds必定有一個所屬的結構(sdshdr),這個header結構在每次建立sds時被建立,用來存儲sds以及sds的相關信息(下文sds的含義僅僅是redis的字符串,sdshdr才表示sds的header)。redis
那爲何redis不直接使用char*呢?總結起來理由以下:算法
這只是我看完代碼後得出的結論,看不懂也沒事,先列出來只是爲了直觀一點。固然還有其餘使用sds的理由,想到再加上。接下來看代碼:數組
sdshdr和sds是一一對應的關係,一個sds必定會有一個sdshdr用來記錄sds的信息。在redis3.2分支出現以前sdshdr只有一個類型,定義以下:安全
struct sdshdr { unsigned int len;//表示sds當前的長度 unsigned int free;//已爲sds分配的長度-sds當前的長度 char buf[];//sds實際存放的位置 };
這些版本的redis每次建立一個sds 無論sds實際有多長,都會分配一個大小固定的sdshdr。根據成員len的類型可知,sds最多能存長度爲2^(8*sizeof(unsigned int))的字符串。
而3.2分支引入了五種sdshdr類型,每次在建立一個sds時根據sds的實際長度判斷應該選擇什麼類型的sdshdr,不一樣類型的sdshdr佔用的內存空間不一樣。這樣細分一下能夠省去不少沒必要要的內存開銷,下面是3.2的sdshdr定義:app
struct __attribute__ ((__packed__)) sdshdr5 { //實際上這個類型redis不會被使用。他的內部結構也與其餘sdshdr不一樣,直接看sdshdr8就好。 unsigned char flags; //一共8位,低3位用來存放真實的flags(類型),高5位用來存放len(長度)。 char buf[];//sds實際存放的位置 }; struct __attribute__ ((__packed__)) sdshdr8 { uint8_t len;//表示當前sds的長度(單位是字節) uint8_t alloc; //表示已爲sds分配的內存大小(單位是字節) unsigned char flags; //用一個字節表示當前sdshdr的類型,由於有sdshdr有五種類型,因此至少須要3位來表示000:sdshdr5,001:sdshdr8,010:sdshdr16,011:sdshdr32,100:sdshdr64。高5位用不到因此都爲0。 char buf[];//sds實際存放的位置 }; 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[]; };
根據以上定義,我畫了一個sdshdr8圖來講明它們的內存佈局:curl
首先要說明之因此sizeof(struct sdshdr8)的大小是len+alloc+flags 是由於這個struct擁有一個柔性數組成員 buf,柔性數組成員是C99以後引入的一個新feature,這裏能夠經過sizeof整個struct給出buf變量的偏移量,從而肯定buf的位置。Flexible array member, is a feature introduced in the [C99] standard of the C programming language,which is an array without a given dimension, and it must be the last member of such a struct. The 'sizeof' operator on such a struct is required to give the offset of the flexible array member. --維基百科ide
其次須要說明的是定義sdshdr的這部分代碼用了__attribute__ ((__packed__)),這個語法不存在於任何C語言標準,是GCC的一個extension,用來告訴編譯器使用最小的內存來存儲sdshdr。函數
packed: ...,This attribute, attached to struct or union type definition, specifies that each member of the structure or union is placed to minimize the memory required.佈局
引用裏"minimize the memory required"其實就是讓編譯器儘可能不使用內存對齊(alignment),以免沒必要要的空間浪費,但其實這麼作會有時間上的開銷,假設CPU老是從存儲器中讀取8個字節,則變量地址必須爲8的倍數,爲了獲取一個沒對齊的8字節的uint8_t數據,CPU須要執行兩次內存訪問 從兩個8字節的內存塊中取出完整的8字節數據。關於內存對齊的更多信息,《深刻理解計算機系統》第三章和《程序員的自我修養》 都有很是詳細的描述。但這裏咱們只須要知道禁用(準確地說是儘可能不使用)內存對齊是redis爲了節省內存開支的一種手段。
接下來分析每一個成員:
#define SDS_TYPE_5 0 //00000000 #define SDS_TYPE_8 1 //00000001 #define SDS_TYPE_16 2 //00000010 #define SDS_TYPE_32 3 //00000011 #define SDS_TYPE_64 4 //00000100 #define SDS_TYPE_MASK 7 //00000111,做爲取flags低3位的掩碼
要判斷一個sds屬於什麼類型的sdshdr,只需 flags&SDS_TYPE_MASK和SDS_TYPE_n比較便可(之因此須要SDS_TYPE_MASK是由於有sdshdr5這個特例,它的高5位不必定爲0,參考上面sdshdr5定義裏的代碼註釋)
sds.h裏全部給出定義的內聯函數都是經過sds做爲參數,經過比較flags&SDS_TYPE_MASK和SDS_TYPE_n來判斷該sds屬於哪一種類型sdshdr,再按照指定的sdshdr類型取出sds的相關信息。
例如sdslen函數:
#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T)))) //返回一個類型爲T包含s字符串的sdshdr的指針 #define SDS_TYPE_5_LEN(f) ((f)>>SDS_TYPE_BITS) //用sdshdr5的flags成員變量作參數返回sds的長度,這實際上是一個沒辦法的hack #define SDS_TYPE_BITS 3 static inline size_t sdslen(const sds s) { unsigned char flags = s[-1]; //sdshdr的flags成員變量 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;//取出sdshdr的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; }
第一行裏的雙井號##的意思是在一個宏(macro)定義裏鏈接兩個子串(token),鏈接以後這##號兩邊的子串就被編譯器識別爲一個。
sdslen函數裏第一行出現了s[-1],看起來感受會是一個undefined behavior,其實不是,這是一種正常又正確的使用方式,它就等同於*(s-1)。The definition of the subscript operator [] is that E1[E2] is identical to (*((E1)+(E2))). --C99。又由於s是一個sds(char*)因此s指向的類型是char,-1就是-1*sizeof(char),因爲sdshdr結構體內禁用了內存對齊,因此這也恰好是一個flags(unsigned char)的地址,因此經過s[-1]咱們能夠得到sds所屬的sdshdr的成員變量flags。
相似sdslen這樣利用sds找到sdshdr類型的還有以下幾個函數,就不一一分析了:
static inline size_t sdsavail(const sds s) static inline void sdssetlen(sds s, size_t newlen) static inline void sdsinclen(sds s, size_t inc) static inline size_t sdsalloc(const sds s) static inline void sdssetalloc(sds s, size_t newlen)
前面說的是在已有結果的狀況下,根據一個sds經過flags變量來判斷它的sdshdr類型。那麼最開始建立一個sds時應該選用什麼類型的sdshdr來存放它的信息呢?這就得根據要存儲的sds的長度決定了,redis在建立一個sds以前會調用sdsReqType(size_t string_size)來判斷用哪一個sdshdr。該函數傳遞一個sds的長度做爲參數,返回應該選用的sdshdr類型。
static inline char sdsReqType(size_t string_size) { if (string_size < 1<<5) //小於2^5,flags成員的高5位便可表示 return SDS_TYPE_5; if (string_size < 1<<8) //小於2^8,8位整數(sdshdr8裏的uint8_t)便可表示string_size return SDS_TYPE_8; if (string_size < 1<<16) //小於2^16,16位整數(sdshdr16裏的uint16_t)便可表示string_size return SDS_TYPE_16; if (string_size < 1ll<<32) /小於2^32,32位整數(sdshrd32裏的uint32_t)便可表示string_size,1ll是指1long long(至少64位)的意思,若是沒有ll,1就是一個int,假設int爲4字節32位,1<<32就會致使undefined behavior. return SDS_TYPE_32; return SDS_TYPE_64; //若sds的長度超過2^64,則全部類型都不法表示這個sds的len }
知道了建立一個sds時應選用什麼類型的sdshdr後咱們就能夠看看建立sds的函數了:
//用init指針指向的內存的內容截取initlen長度來new一個sds,這個函數是二進制安全的 sds sdsnewlen(const void *init, size_t initlen) { void *sh;//sdshdr的指針 sds s; //char * s; char type = sdsReqType(initlen);//根據須要的長度決定sdshdr的類型 /* 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;//若是initlen爲空而且sdshdr的類型爲sdshdr5,則將類型設置爲sdshdr8 int hdrlen = sdsHdrSize(type);//每一個sdshdr類型的大小都不同,根據類型返回sdshdr的大小以計算須要分配的空間 unsigned char *fp; /* flags pointer. */ sh = s_malloc(hdrlen+initlen+1);//在heap裏申請一段連續的空間給sdshdr和屬於它的sds,+1是由於要在尾部放置'\0' if (!init) memset(sh, 0, hdrlen+initlen+1);//若是init爲空,則整個sdshdr都用0即字符'\0'初始化 if (sh == NULL) return NULL; s = (char*)sh+hdrlen;//經過sdshdr指針找到sds的位置 fp = ((unsigned char*)s)-1;//找到flags的位置,等同於&s[-1] switch(type) { case SDS_TYPE_5: { *fp = type | (initlen << SDS_TYPE_BITS);//initlen左移3位到高5位,給type騰出位置,和type作或運算 break; } case SDS_TYPE_8: { SDS_HDR_VAR(8,s);//#define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T))); 能夠理解爲在switch做用域下申明瞭一個新的局部變量sh,類型是struct sdshdr##T,跟外面的sh值同樣,變量名同樣,但不是一個東西。 sh->len = initlen; sh->alloc = initlen; *fp = type;//設置flags 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); //memcpy不會由於'\0'而停下,支持二進制數據的拷貝 s[initlen] = '\0'; //不論是不是二進制數據,尾部都會加上'\0' return s; } static inline int sdsHdrSize(char type) { switch(type&SDS_TYPE_MASK) { case SDS_TYPE_5: return sizeof(struct sdshdr5);//以前說的柔性數組成員不會計入struct的大小,因此這個hdrsize沒有包括sds的長度 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的代碼,流程寫在註釋裏可能仍是不夠清晰,結合圖來看應該會好一點(sdshdr5的圖不同,我就偷懶不畫了,反正也不用它):
流程以下:
若是用字符串"PHP is the best programming language"(長度爲36) 調用sdsnewlen(),最終會產生以下佈局:
上文提到 redis不直接使用C語言字符串還有個緣由是爲了定製的本身的內存重分配的方法,減小因堆內存申請與釋放產生的時間開銷。
若是redis使用的是普通的C語言字符串char*,那麼每次拼接或者截斷一個字符串以前都須要從新分配/釋放內存,不然會形成內存溢出或泄露。可是每次進行分配/釋放內存的操做又很是影響性能,因此redis作了兩件事:
下面是sdsMakeRoomFor的源碼:
//注意:這個函數是在擴充sds前調用,sds不會被擴充也不會改變len sds sdsMakeRoomFor(sds s, size_t addlen) { //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); if (newlen < SDS_MAX_PREALLOC) //擴充後的總長度小於1M(1024*1024),則直接多分配newlen個字節閒置。 newlen *= 2; else //擴充後的總長度大於1M(1024*1024),則多分配1M字節閒置 newlen += SDS_MAX_PREALLOC; type = sdsReqType(newlen);//根據擴充後的總長度決定須要這個sds要用什麼類型的sdshdr /* 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) { //若是擴充後的sdshdr類型不變,則在原有的地方realloc就好。由於len和alloc的類型仍是原來的。 //ps: s_realloc封裝了realloc,realloc返回的指針未必是sh指向的地址,可能爲了內存對齊移動了這塊內存 newsh = s_realloc(sh, hdrlen+newlen+1); if (newsh == NULL) return NULL; s = (char*)newsh+hdrlen; } else { //若是擴充後的sdshdr類型變了,那就只能從新在別的地方分配內存,而後從新賦值,釋放掉舊的內存。 /* 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); } sdssetalloc(s, newlen); return s; }
剛纔已經建立了的sds"PHP is the best programming language",它的len是36,alloc也是36,如今給它擴充一下,把" in the world"加上,算上空格一共要加13個字節,這時會調用sdscatlen()函數完成整個拼接sds的操做,它內部須要先調用sdsMakeRoomFor(s, 13)走一遍內存重分配的算法,如下是sdsMakeRoomFor的流程
最終得到以下內存佈局:
接下來sdscatlen()函數用memcpy把" in the world"append到這個已經經歷過內存分配的sds的尾部,並更新len:
附上sdscatlen的代碼:
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; }
我粗略地在源碼裏找了找,縮短sds字符串的有三個函數:sdsclear、sdstrim、sdsrange,他們都不會改變alloc的大小即不會釋聽任何內存,這就是sds字符串內存管理的一種方式:惰性釋放。額外調用sdsRemoveFreeSpace釋放內存,這樣就節省了每次sds縮減長度而致使的內存釋放開銷。
三個縮短sds的函數就不一一介紹了,有興趣直接去代碼裏看就好,須要注意的這些函數裏移動字符串用的memmove()是容許內存重疊的,這點跟memcpy()不同。
下面介紹一下sdsRemoveFreeSpace,先放源碼:
//這個函數壓縮內存,讓alloc=len。若是type變小了,則另開一片內存複製,若是type不變,則realloc sds sdsRemoveFreeSpace(sds s) { void *sh, *newsh; char type, oldtype = s[-1] & SDS_TYPE_MASK; int hdrlen; size_t len = sdslen(s); sh = (char*)s-sdsHdrSize(oldtype); type = sdsReqType(len); hdrlen = sdsHdrSize(type); //這以後的代碼就跟sdsMakeRoomFor後面的代碼差很少了,釋放掉多餘內存並重置alloc。 if (oldtype==type) { newsh = s_realloc(sh, hdrlen+len+1); if (newsh == NULL) return NULL; s = (char*)newsh+hdrlen; } 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); } sdssetalloc(s, len); return s; }
繼續拿以前用sdscatlen()擴充的sds執行一遍sdsRemoveFreeSpace(),會獲得以下佈局:
以上就是sds相關代碼分析,還有不少針對sds的基本操做函數在這裏就不一一列舉了。