国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專(zhuān)欄INFORMATION COLUMN

【Redis5源碼學(xué)習(xí)】2019-04-15 簡(jiǎn)單動(dòng)態(tài)字符串SDS

Vixb / 3549人閱讀

摘要:關(guān)于結(jié)構(gòu)體內(nèi)存對(duì)齊是什么,請(qǐng)參考源碼學(xué)習(xí)內(nèi)存管理筆記。這說(shuō)明在當(dāng)前情況下,字符串結(jié)構(gòu)中的柔性數(shù)組的起始位置并不受是否加關(guān)鍵字而影響,是緊跟在結(jié)構(gòu)體后面的,所以節(jié)省內(nèi)存這個(gè)說(shuō)法并不成立。

baiyan

全部視頻:https://segmentfault.com/a/11...

今天我們正式進(jìn)入redis5源碼的學(xué)習(xí)。redis是一個(gè)由C語(yǔ)言編寫(xiě)、基于內(nèi)存、單進(jìn)程、可持久化的Key-Value型數(shù)據(jù)庫(kù),解決了磁盤(pán)存取速度慢的問(wèn)題,大幅提升了數(shù)據(jù)訪(fǎng)問(wèn)速度,所以它常常被用作緩存。

那么為什么redis會(huì)如此之快呢?讓我們首先從內(nèi)部存儲(chǔ)的數(shù)據(jù)結(jié)構(gòu)的角度,一步一步揭開(kāi)它神秘的面紗。

在redis的set、get等常用命令中,最嘗試用的就是字符串類(lèi)型。在redis中,存儲(chǔ)字符串的數(shù)據(jù)類(lèi)型,叫做簡(jiǎn)單動(dòng)態(tài)字符串(Simple Dynamic String),即SDS,它在redis中是如何實(shí)現(xiàn)的呢?

引入

回顧我們之前在PHP7源碼分析中講到的zend_string結(jié)構(gòu):

struct _zend_string {
    zend_refcounted_h gc;         /*引用計(jì)數(shù),與垃圾回收相關(guān),暫不展開(kāi)*/
    zend_ulong        h;          /* 冗余的hash值,計(jì)算數(shù)組key的哈希值時(shí)避免重復(fù)計(jì)算*/
    size_t            len;        /* 存長(zhǎng)度 */
    char              val[1];     /* 柔性數(shù)組,真正存放字符串值 */
};

之前在【PHP7源碼學(xué)習(xí)】2019-03-13 PHP字符串筆記文中提到,設(shè)計(jì)一個(gè)存儲(chǔ)字符串的結(jié)構(gòu),最重要的就是存儲(chǔ)其長(zhǎng)度字符串本身的內(nèi)容。至于為什么存儲(chǔ)長(zhǎng)度,是為了解決二進(jìn)制安全的問(wèn)題,且能夠以常量復(fù)雜度訪(fǎng)問(wèn)到字符串的長(zhǎng)度,詳情可以到上述文章中查看。

SDS新老結(jié)構(gòu)的對(duì)比

在redis3.2.x之前,SDS的存儲(chǔ)結(jié)構(gòu)如下:

struct sdshdr {
    int len; //存長(zhǎng)度
    int free; //存字符串內(nèi)容的柔性數(shù)組的剩余空間
    char buf[]; //柔性數(shù)組,真正存放字符串值 
}; 

以“Redis”字符串為例,我們看一下它在舊版SDS結(jié)構(gòu)中是如何存儲(chǔ)的:

free字段為0,代表buf字段沒(méi)有剩余存儲(chǔ)空間

len字段為5,代表字符串長(zhǎng)度為5

buf字段存儲(chǔ)真正的字符串內(nèi)容“Redis”

存儲(chǔ)字符串內(nèi)容的柔性數(shù)組占用內(nèi)存大小為6字節(jié),其余字段所占用8個(gè)字節(jié)(4+4+6 = 14字節(jié))

在新版本redis5中,為了進(jìn)一步減少字符串存儲(chǔ)過(guò)程中的內(nèi)存占用,劃分了5種適應(yīng)不同字符串長(zhǎng)度專(zhuān)用的存儲(chǔ)結(jié)構(gòu):

struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; //低三位存儲(chǔ)類(lèi)型,高5位存儲(chǔ)字符串長(zhǎng)度,這種字符串存儲(chǔ)類(lèi)型很少使用
    char buf[]; //存儲(chǔ)字符串內(nèi)容的柔性數(shù)組
};
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; //字符串長(zhǎng)度
    uint8_t alloc; //已分配的總空間
    unsigned char flags; //標(biāo)識(shí)是哪種存儲(chǔ)類(lèi)型
    char buf[]; //存儲(chǔ)字符串內(nèi)容的柔性數(shù)組
};
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; //字符串長(zhǎng)度
    uint16_t alloc;  //已分配的總空間
    unsigned char flags; //標(biāo)識(shí)是哪種存儲(chǔ)類(lèi)型
    char buf[]; //存儲(chǔ)字符串內(nèi)容的柔性數(shù)組
};
struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len; //字符串長(zhǎng)度
    uint32_t alloc;  //已分配的總空間
    unsigned char flags; //標(biāo)識(shí)是哪種存儲(chǔ)類(lèi)型
    char buf[]; //存儲(chǔ)字符串內(nèi)容的柔性數(shù)組
};
struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; //字符串長(zhǎng)度
    uint64_t alloc;  //已分配的總空間
    unsigned char flags; //標(biāo)識(shí)是哪種存儲(chǔ)類(lèi)型
    char buf[]; //存儲(chǔ)字符串內(nèi)容的柔性數(shù)組
};

我們可以看到,SDS的存儲(chǔ)結(jié)構(gòu)由一種變成了五種,他們之間的不同就在于存儲(chǔ)字符串長(zhǎng)度的len字段和存儲(chǔ)已分配字節(jié)數(shù)的alloc字段的類(lèi)型,分別占用了1、2、4、8字節(jié)(不考慮sdshdr5類(lèi)型),這決定了這種結(jié)構(gòu)能夠最大存儲(chǔ)多長(zhǎng)的字符串(2^8/2^16/2^32/2^64)。

我們注意,這些結(jié)構(gòu)體中都帶有__attribute__ ((__packed__))關(guān)鍵字,它告訴編譯器不進(jìn)行結(jié)構(gòu)體的內(nèi)存對(duì)齊。這個(gè)關(guān)鍵字我們下文會(huì)詳細(xì)講解。關(guān)于結(jié)構(gòu)體內(nèi)存對(duì)齊是什么,請(qǐng)參考【PHP7源碼學(xué)習(xí)】2019-03-08 PHP內(nèi)存管理2筆記。

利用gdb查看SDS的存儲(chǔ)結(jié)構(gòu)

接著說(shuō)我們之前存儲(chǔ)“Redis”的例子,我們需要先對(duì)其進(jìn)行g(shù)db,觀察"Redis”字符串使用了哪種結(jié)構(gòu),gdb的步驟如下:

首先到官網(wǎng)下載源碼包,編譯

啟動(dòng)一個(gè)終端,進(jìn)入redis源碼的src目錄下,后臺(tái)啟動(dòng)一個(gè)redis-server:

./redis-server &

然后查看當(dāng)前redis的后臺(tái)進(jìn)程的pid:

ps -aux |grep redis

記錄下這個(gè)pid,然后利用gdb -p命令調(diào)試該端口(如端口號(hào)是11430):

gdb -p 11430

接著在setCommand函數(shù)處打一個(gè)斷點(diǎn),這個(gè)函數(shù)用來(lái)執(zhí)行set命令,然后使用c命令執(zhí)行到斷點(diǎn)處:

(gdb) b setCommand
(gdb) c

有了redis服務(wù)端,我們還要啟動(dòng)一個(gè)redis客戶(hù)端,接下來(lái)啟動(dòng)另一個(gè)終端(同樣在src目錄下),啟動(dòng)客戶(hù)端:

./redis-cli

接著我們?cè)趓edis客戶(hù)端中執(zhí)行set命令,我們?cè)O(shè)置了一個(gè)key為Redis,值為1的key-value對(duì):

127.0.0.1:6379> set Redis 1

返回我們之前終端中的服務(wù)端,我們發(fā)現(xiàn)它停在了setCommand處:

接著一直n下去,直到setGenericCommand函數(shù),s進(jìn)去,就可以看到我們的key “Redis”了,它是一個(gè)rObj結(jié)構(gòu)(我們暫時(shí)不看),里面的ptr就指向字符串結(jié)構(gòu)的buf字段,我們強(qiáng)轉(zhuǎn)一下,能夠看到字符串內(nèi)容“Redis”。

我們知道,無(wú)論是這五種結(jié)構(gòu)中的哪一種,其前一位一定是flag字段,我們打印它的值,它的值為1。那么1是什么含義呢,它被用來(lái)標(biāo)識(shí)是這五種字符串結(jié)構(gòu)中的哪一種:

#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

它的值為1,代表是sdshdr8類(lèi)型,我們可以畫(huà)出當(dāng)前字符串的存儲(chǔ)結(jié)構(gòu)圖:

我們可以看到,它總共占用3+6 = 9字節(jié),比之前的14字節(jié)節(jié)省了5字節(jié)。通過(guò)對(duì)之前長(zhǎng)度和alloc字段的細(xì)化(由之前的int轉(zhuǎn)為int8、int16、int32、int64),這樣一來(lái),就會(huì)大大節(jié)省redis存儲(chǔ)字符串所占用的內(nèi)存空間。內(nèi)存空間是非常寶貴的,而且redis中最常用的數(shù)據(jù)類(lèi)型就是字符串類(lèi)型。雖然看起來(lái)節(jié)省的空間很少,但由于它非常常用,所以這樣做的好處是無(wú)窮大的。

關(guān)鍵字__attribute__ ((packed))的作用

該關(guān)鍵字用來(lái)告知編譯器不需要進(jìn)行結(jié)構(gòu)體的內(nèi)存對(duì)齊

為了測(cè)試__attribute__ ((packed))關(guān)鍵字在redis字符串結(jié)構(gòu)中的作用,我們寫(xiě)如下一段測(cè)試代碼:

  #include "stdio.h"
 
  int main(){
      struct __attribute__ ((__packed__))  sdshdr64{
          long long len;
          long long alloc;
          unsigned char flags;
          char buf[];
      };
 
      struct sdshdr64 s;
      s.len = 1;
      s.alloc = 2;
      printf("sizeof sds64 is %d", sizeof(s));
      return 1;
  }

我們定義一個(gè)結(jié)構(gòu)體,其字段和redis中的字符串結(jié)構(gòu)基本一致。如果加上__attribute__ ((__packed__)) ,應(yīng)該不是內(nèi)存對(duì)齊的。如果去掉它,就應(yīng)該是內(nèi)存對(duì)齊的,會(huì)比前一種情況更加浪費(fèi)內(nèi)存,所以會(huì)對(duì)齊會(huì)節(jié)省內(nèi)存。我們現(xiàn)在猜想的內(nèi)存結(jié)構(gòu)圖應(yīng)該如下所示:

我們首先驗(yàn)證加上__attribute__ ((__packed__)) 的情況,我們預(yù)期應(yīng)該是不對(duì)齊的,在gdb中內(nèi)存地址如下:

我們看到,buf確實(shí)是從0x171地址處開(kāi)始的,并沒(méi)有對(duì)齊。那么我們看另一種情況,去掉__attribute__ ((__packed__)),再進(jìn)行g(shù)db調(diào)試:

大家看這張圖,是不是和上一張圖一摸一樣(我真的去掉了并且重新編譯了!!!)。這說(shuō)明在當(dāng)前情況下,redis字符串結(jié)構(gòu)中的柔性數(shù)組的起始位置并不受是否加__attribute__ ((__packed__))關(guān)鍵字而影響,是緊跟在結(jié)構(gòu)體后面的,所以節(jié)省內(nèi)存這個(gè)說(shuō)法并不成立。(不一定是所有情況下柔性數(shù)組都緊跟在結(jié)構(gòu)體后面,如果把buf的類(lèi)型改為int就不是緊跟在后面,大家感興趣可以自己調(diào)試一下)。

那么,為什么這里要加上__attribute__ ((__packed__)呢?我們換個(gè)思路,既然不能節(jié)省空間,那么能不能節(jié)省時(shí)間呢?會(huì)不會(huì)操作非對(duì)齊的結(jié)構(gòu)體性能更好、效率更高,或者是寫(xiě)代碼更方便、可閱讀性強(qiáng)呢?

筆者在這里的猜想是比較方便工程中的代碼編寫(xiě),可閱讀性更強(qiáng),我的參考如下:

在sizeof運(yùn)算符中,它返回的是結(jié)構(gòu)體占用空間的大小,和是否對(duì)齊有很大關(guān)系。比如上例中的結(jié)構(gòu)體,如果不加上__attribute__ ((__packed__)),說(shuō)明需要內(nèi)存對(duì)齊,sizeof(struct s)的返回結(jié)果應(yīng)該為24(8+8+8);如果加上__attribute__ ((__packed__)),說(shuō)明不需要對(duì)齊,返回的結(jié)果應(yīng)該為17(8+8+1),我們打印一下:

結(jié)果和我們預(yù)期的一致。我們知道,在之前我們gdb的時(shí)候,rObj的指針直接指向柔性數(shù)組buf的地址,即字符串內(nèi)容的起始地址。那么如何知道它的len和alloc的值呢?只需要用buf的地址ptr - sizeof(struct s)即可。在這里,如果加上__attribute__ ((__packed__)),它返回的結(jié)果是17,那么直接做減法,就可以到結(jié)構(gòu)體開(kāi)頭的位置,即可直接讀取len的值。如果不加__attribute__ ((__packed__)),它返回的結(jié)果是24,做減法就會(huì)的到錯(cuò)誤的位置,這就是原因所在,在源碼中我們也可以看到,它確實(shí)是這么找到當(dāng)前字符串結(jié)構(gòu)體的頭部的:

#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))))

那么我們可能會(huì)問(wèn)了,你剛才不是還用buf[-1]也能訪(fǎng)問(wèn)到嗎?或者buf[-17],應(yīng)該也能訪(fǎng)問(wèn)到len吧。這里筆者簡(jiǎn)單猜想可能是上一種寫(xiě)法,在工程的代碼實(shí)現(xiàn)中,更加易讀也更加方便。更加深層的原因仍待討論。

為什么需要alloc字段

在之前的講解中,我們一直沒(méi)有提到alloc字段的作用。我們知道,它是目前給存儲(chǔ)字符串的柔性數(shù)組總共分配了多少字節(jié)的空間。那么記錄這個(gè)字段的作用何在呢?那就是空間預(yù)分配惰性空間釋放的設(shè)計(jì)思想了。

空間預(yù)分配:在需要對(duì) SDS 進(jìn)行空間擴(kuò)展的時(shí)候, 程序不僅會(huì)為 SDS 分配修改所必須要的空間, 還會(huì)為 SDS 分配額外的未使用空間。舉一個(gè)例子,我們將字符串“Redis”擴(kuò)展到“Redis111”,應(yīng)用程序并不僅僅分配3個(gè)字節(jié),僅僅讓它恰好滿(mǎn)足分配的長(zhǎng)度,而是會(huì)額外分配一些空間。具體如何分配,見(jiàn)下述代碼注釋。我們講其中一種分配方式,假設(shè)它會(huì)分配8字節(jié)的內(nèi)存空間。現(xiàn)在總共的內(nèi)存空間為5+8 = 13,而我們只用了前8個(gè)內(nèi)存空間,還剩下5個(gè)內(nèi)存空間未使用。那么我們?yōu)槭裁匆@樣做呢?這是因?yàn)槿绻覀冊(cè)倮^續(xù)對(duì)它進(jìn)行擴(kuò)展,如改成“Redis11111”,在擴(kuò)展 SDS 空間之前,SDS API 會(huì)先檢查未使用空間是否足夠,如果足夠的話(huà),API 就會(huì)直接使用未使用空間那么我們就不用再進(jìn)行系統(tǒng)調(diào)用申請(qǐng)一次空間了,直接把追加的“11”放到之前分配過(guò)的空間處即可。這樣一來(lái),會(huì)大大減少使用內(nèi)存分配系統(tǒng)調(diào)用的次數(shù),提高了性能與效率。空間預(yù)分配的代碼如下:

sds sdsMakeRoomFor(sds s, size_t addlen) {

    void *sh, *newsh;
    size_t avail = sdsavail(s); // 獲取當(dāng)前字符串可用剩余空間
    size_t len, newlen;
    char type, oldtype = s[-1] & SDS_TYPE_MASK;
    int hdrlen;

    /* 如果可用空間大于追加部分的長(zhǎng)度,說(shuō)明當(dāng)前字符串還有額外的空間,足夠容納擴(kuò)容后的字符串,不用分配額外空間,直接返回 */
    if (avail >= addlen) return s;

    len = sdslen(s);
    sh = (char*)s-sdsHdrSize(oldtype);
    newlen = (len+addlen);
    if (newlen < SDS_MAX_PREALLOC) //SDS_MAX_PREALLOC = 1MB,如果擴(kuò)容后的長(zhǎng)度小于1MB,直接額外分配擴(kuò)容后字符串長(zhǎng)度*2的空間
        newlen *= 2;
    else
        newlen += SDS_MAX_PREALLOC; //擴(kuò)容后長(zhǎng)度大于等于1MB,額外分配擴(kuò)容后字符串+1MB的空間

     ...
     真正的去分配空間
     ...
     sdssetalloc(s, newlen);
     return s;
}

上述sdsavail函數(shù)在獲取字符串剩余可用空間的時(shí)候,就會(huì)使用到alloc字段。它記錄了分配的總空間大小,方便我們?cè)谶M(jìn)行字符串追加操作的時(shí)候,判斷是否需要額外分配空間。當(dāng)前剩余的可用空間大小為alloc - len,即已分配總空間大小alloc - 當(dāng)前使用的空間大小len

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;
}

惰性空間釋放:惰性空間釋放用于優(yōu)化 SDS 的字符串截取或縮短操作。當(dāng) SDS 的 API 需要縮短 SDS 保存的字符串時(shí),程序并不立即回收縮短后多出來(lái)的字節(jié)。這樣一來(lái),如果將來(lái)要對(duì) SDS 進(jìn)行增長(zhǎng)操作的話(huà),這些未使用空間就可能會(huì)派上用場(chǎng)。比如我們將“Redis111”縮短為“Redis”,然后又改成“Redis111”,這樣,如果我們立刻回收縮短后多出來(lái)的字節(jié),然后再重新分配內(nèi)存空間,是非常浪費(fèi)時(shí)間的。如果等待一段時(shí)間之后再回收,可以很好地避免了縮短字符串時(shí)所需的內(nèi)存重分配操作, 并為將來(lái)可能有的增長(zhǎng)操作提供了擴(kuò)展空間。源碼中一個(gè)清空字符串的SDS API如下:

/* Modify an sds string in-place to make it empty (zero length).
 * However all the existing buffer is not discarded but set as free space
 * so that next append operations will not require allocations up to the
 * number of bytes previously available. */
void sdsclear(sds s) {
    sdssetlen(s, 0);
    s[0] = "