Redis源碼分析-redis3.2-動態字符串sds

最近打算閱讀redis源碼,可是擔憂讀完就忘了,因此決定把閱讀的筆記在簡書裏記錄起來,但願可以堅持讀下去。之因此選擇3.2是由於公司把redis升級成了這個版本。程序員

本文先介紹redis動態字符串(sds)。

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*呢?總結起來理由以下:算法

  • 想用O(1)的時間複雜度獲取字符串長度(利用sdshdr)。
  • sds實現了部分本身的字符串處理函數,可以存儲二進制字符串 保證二進制安全,而全部C語言str前綴的字符串處理函數不保證二進制安全(遇到'0'就停下,認爲它是字符串的結尾,不能存二進制數據)。
  • 制定內存重分配方法,減小 因修改字符串而致使的 內存分配和釋放 的次數。

這只是我看完代碼後得出的結論,看不懂也沒事,先列出來只是爲了直觀一點。固然還有其餘使用sds的理由,想到再加上。接下來看代碼:數組

1.sdshdr定義

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爲了節省內存開支的一種手段。

接下來分析每一個成員:

  • len表示sds當前sds的長度(單位是字節),不包括'0'終止符,經過len直接獲取字符串長度,不須要掃一遍string,這就是上文說的封裝sds的理由之一
  • alloc表示當前爲sds分配的大小(單位是字節)(3.2之前的版本用的free是表示還剩free字節可用空間),不包括'0'終止符;
  • flags表示當前sdshdr的類型,聲明爲char 一共有1個字節(8位),僅用低三位就能夠表示全部5種sdshdr類型(詳見上文代碼註釋):
#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_MASKSDS_TYPE_n比較便可(之因此須要SDS_TYPE_MASK是由於有sdshdr5這個特例,它的高5位不必定爲0,參考上面sdshdr5定義裏的代碼註釋)

sds.h裏全部給出定義的內聯函數都是經過sds做爲參數,經過比較flags&SDS_TYPE_MASKSDS_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)

2.建立一個sds

前面說的是在已有結果的狀況下,根據一個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的圖不同,我就偷懶不畫了,反正也不用它):

流程以下:

  • 根據sds的長度判斷須要選用sdshdr的類型
  • 根據sdshdr的類型用sdsHdrSize函數獲得hdrlen(其實就是sizeof(struct sdshdr))
  • 爲sdshdr分配一個hdrlen+initlen+1大小的堆內存(+1是爲了放置'0',這個'0'不計入alloc或len)
  • 按參數填充成員變量len、alloc和type
  • 用memcpy給sds賦值,並在尾部加上'0'

若是用字符串"PHP is the best programming language"(長度爲36) 調用sdsnewlen(),最終會產生以下佈局:

3.內存重分配

上文提到 redis不直接使用C語言字符串還有個緣由是爲了定製的本身的內存重分配的方法,減小因堆內存申請與釋放產生的時間開銷。
若是redis使用的是普通的C語言字符串char*,那麼每次拼接或者截斷一個字符串以前都須要從新分配/釋放內存,不然會形成內存溢出或泄露。可是每次進行分配/釋放內存的操做又很是影響性能,因此redis作了兩件事:

  • 提供一個函數sdsMakeRoomFor,當須要擴充一個sds字符串時調用它,它的內部實現了sds的內存分配算法,儘量地下降內存分配次數。
  • 在截斷一個sds字符串時不馬上釋放掉以前爲它申請的內存,而是經過alloc成員變量記錄下這個值,供後續操做使用,而後提供一個函數sdsRemoveFreeSpace讓咱們根據須要釋放掉多出的內存。

1.sds加長

下面是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的流程

  • len+addlen=49個字節,小於SDS_MAX_PREALLOC(1M),因此最終會分配49*2=98字節的內存給這個須要擴充的sds字符串(實際是99,多分配一字節的'0')。
  • 根據sdsReqType()函數,98字節小於2^8字節,因此擴充後的類型還是sdshdr8,realloc一片內存。
  • 重置sds的alloc爲98。

最終得到以下內存佈局:

接下來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;
}

2.sds縮減

我粗略地在源碼裏找了找,縮短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(),會獲得以下佈局:

4.小結

以上就是sds相關代碼分析,還有不少針對sds的基本操做函數在這裏就不一一列舉了。

相關文章
相關標籤/搜索