天天看點

Redis記憶體管理的基石zmallc.c源碼解讀(一)源碼結構字長與位元組對齊zmalloczfreezcalloczrealloczstrdup

        當我第一次閱讀了這個檔案的源碼的時候,我笑了,忽然想起前幾周阿裡電話二面的時候,問到了自定義記憶體管理函數并處理8位元組對齊問題。當時無言以對,在面試官無數次的提示下才答了出來,結果顯而易見,挂掉了二面。而這份源碼中函數zmalloc()和zfree()的設計思路和實作原理,正是面試官想要的答案。

源碼結構

zmalloc.c檔案的内容如下:

主要函數

  • zmalloc()
  • zfree()
  • zcalloc()
  • zrelloc()
  • zstrdup()

字長與位元組對齊

        CPU一次性能讀取資料的二進制位數稱為 字長,也就是我們通常所說的32位系統(字長4個位元組)、64位系統(字長8個位元組)的由來。所謂的8位元組對齊,就是指變量的起始位址是8的倍數。比如程式運作時(CPU)在讀取long型資料的時候,隻需要一個總線周期,時間更短,如果不是8位元組對齊的則需要兩個總線周期才能讀完資料。          本文中我提到的8位元組對齊是針對64位系統而言的,如果是32位系統那麼就是4位元組對齊。實際上Redis源碼中的位元組對齊是軟編碼,而非寫死。裡面多用sizeof(long)或sizeof(size_t)來表示。size_t(gcc中其值為long unsigned int)和long的長度是一樣的,long的長度就是計算機的字長。這樣在未來的系統中如果字長(long的大小)不是8個位元組了,該段代碼依然能保證相應代碼可用。

zmalloc

        輔助的函數:

  • malloc()
  • zmalloc_oom_handler【函數指針】
  • zmalloc_default_oom()【被上面的函數指針所指向】
  • update_zmalloc_stat_alloc()【宏函數】
  • update_zmalloc_stat_add()【宏函數】

zmalloc()和malloc()有相同的函數接口(參數,傳回值)。 

zmalloc()源碼

void *zmalloc(size_t size) {
    void *ptr = malloc(size+PREFIX_SIZE);

    if (!ptr) zmalloc_oom_handler(size);
#ifdef HAVE_MALLOC_SIZE
    update_zmalloc_stat_alloc(zmalloc_size(ptr));
    return ptr;
#else
    *((size_t*)ptr) = size;
    update_zmalloc_stat_alloc(size+PREFIX_SIZE);
    return (char*)ptr+PREFIX_SIZE;
#endif
}
           

        參數size是我們 需要配置設定的記憶體大小。實際上我們調用malloc 實際配置設定的大小是size+PREFIX_SIZE。PREFIX_SIZE是一個條件編譯的宏,不同的平台有不同的結果,在Linux中其值是sizeof(size_t),是以我們多配置設定了一個字長(8個位元組)的空間(後面代碼可以看到多配置設定8個位元組的目的是用于儲存size的值)。         如果ptr指針為NULL(記憶體配置設定失敗),調用zmalloc_oom_handler(size)。該函數實際上是一個函數指針指向函數zmalloc_default_oom,其主要功能就是列印錯誤資訊并終止程式。

// oom是out of memory(記憶體不足)的意思
static void zmalloc_default_oom(size_t size) {
    fprintf(stderr, "zmalloc: Out of memory trying to allocate %zu bytes\n",
        size);
    fflush(stderr);
    abort();
}
           

接下來是宏的條件編譯,我們聚焦在#else的部分。

*((size_t*)ptr) = size;
    update_zmalloc_stat_alloc(size+PREFIX_SIZE);
    return (char*)ptr+PREFIX_SIZE;
           

第一行就是在已配置設定空間的第一個字長(前8個位元組)處存儲 需要配置設定的位元組大小(size)。

第二行調用了update_zmalloc_stat_alloc()【宏函數】,它的功能是更新全局變量used_memory(已配置設定記憶體的大小)的值(源碼解讀見下一節)。 第三行傳回的(char *)ptr+PREFIX_SIZE。就是将已配置設定記憶體的起始位址向右偏移PREFIX_SIZE * sizeof(char)的長度(即8個位元組),此時得到的新指針指向的記憶體空間的大小就等于size了。 接下來,分析一下update_zmalloc_stat_alloc的源碼

update_zmalloc_stat_alloc源碼

#define update_zmalloc_stat_alloc(__n) do { \
    size_t _n = (__n); \
    if (_n&(sizeof(long)-1)) _n += sizeof(long)-(_n&(sizeof(long)-1)); \
    if (zmalloc_thread_safe) { \
        update_zmalloc_stat_add(_n); \
    } else { \
        used_memory += _n; \
    } \
} while(0)
           

這個宏函數最外圈有一個do{...}while(0)循環看似毫無意義,實際上大有深意。這部分内容不是本文讨論的重點,這裡不再贅述。具體請看網上的這篇文章  http://www.spongeliu.com/415.html。         因為 sizeof(long) = 8 【64位系統中】,是以上面的第一個if語句,可以等價于以下代碼:

if(_n&7) _n += 8 - (_n&7);
           

         這段代碼就是判斷配置設定的記憶體空間的大小是不是8的倍數。如果記憶體大小不是8的倍數,就加上相應的偏移量使之變成8的倍數。_n&7 在功能上等價于 _n%8,不過位操作的效率顯然更高。         malloc()本身能夠保證所配置設定的記憶體是8位元組對齊的:如果你要配置設定的記憶體不是8的倍數,那麼malloc就會多配置設定一點,來湊成8的倍數。是以update_zmalloc_stat_alloc函數(或者說zmalloc()相對malloc()而言)真正要實作的功能 并不是進行8位元組對齊(malloc已經保證了),它的真正目的是使變量used_memory精确的維護實際已配置設定記憶體的大小。                第2個if的條件是一個整型變量zmalloc_thread_safe。顧名思義,它的值表示操作是否是線程安全的,如果不是線程安全的(else),就給變量used_memory加上n。used_memory是zmalloc.c檔案中定義的全局靜态變量,表示已配置設定記憶體的大小。如果是記憶體安全的就使用update_zmalloc_stat_add來給used_memory加上n。         update_zmalloc_stat_add也是一個宏函數(Redis效率之高,速度之快,這些宏函數可謂功不可沒)。它也是一個條件編譯的宏,依據不同的宏有不同的定義,這裡我們來看一下#else後面的定義的源碼【zmalloc.c有多處條件編譯的宏,為了把精力都集中在記憶體管理的實作算法上,這裡我隻關注Linux平台下使用glibc的malloc的情況】。

#define update_zmalloc_stat_add(__n) do { \
    pthread_mutex_lock(&used_memory_mutex); \
    used_memory += (__n); \
    pthread_mutex_unlock(&used_memory_mutex); \
} while(0)
           

        pthread_mutex_lock()和pthread_mutex_unlock()使用互斥鎖(mutex)來實作線程同步,前者表示加鎖,後者表示解鎖,它們是POSIX定義的線程同步函數。當加鎖以後它後面的代碼在多線程同時執行這段代碼的時候就隻會執行一次,也就是實作了線程安全。

zfree

        zfree()和free()有相同的程式設計接口,它負責清除zmalloc()配置設定的空間。 輔助函數:

  • free()
  • update_zmalloc_free()【宏函數】
  • update_zmalloc_sub()【宏函數】
  • zmalloc_size()

zfree()源碼

void zfree(void *ptr) {
#ifndef HAVE_MALLOC_SIZE
    void *realptr;
    size_t oldsize;
#endif

    if (ptr == NULL) return;
#ifdef HAVE_MALLOC_SIZE
    update_zmalloc_stat_free(zmalloc_size(ptr));
    free(ptr);
#else
    realptr = (char*)ptr-PREFIX_SIZE;
    oldsize = *((size_t*)realptr);
    update_zmalloc_stat_free(oldsize+PREFIX_SIZE);
    free(realptr);
#endif
}
           

重點關注#else後面的代碼

realptr = (char *)ptr - PREFIX_SIZE;
           

表示的是ptr指針向前偏移8個位元組的長度,即回退到最初malloc傳回的位址,這裡稱為realptr。然後

oldsize = *((size_t*)realptr);
           

先進行類型轉換再取指針所指向的值。通過zmalloc()函數的分析,可知這裡存儲着我們最初需要配置設定的記憶體大小(zmalloc中的size),這裡指派個oldsize

update_zmalloc_stat_free(oldsize+PREFIX_SIZE);
           

update_zmalloc_stat_free()也是一個宏函數,和zmalloc中update_zmalloc_stat_alloc()大緻相同,唯一不同之處是前者在給變量used_memory減去配置設定的空間,而後者是加上該空間大小。

最後free(realptr),清除空間

update_zmalloc_free源碼

#define update_zmalloc_stat_free(__n) do { \
    size_t _n = (__n); \
    if (_n&(sizeof(long)-1)) _n += sizeof(long)-(_n&(sizeof(long)-1)); \
    if (zmalloc_thread_safe) { \
        update_zmalloc_stat_sub(_n); \
    } else { \
        used_memory -= _n; \
    } \
} while(0)
           

其中的函數update_zmalloc_sub與zmalloc()中的update_zmalloc_add相對應,但功能相反,提供線程安全地used_memory減法操作。

#define update_zmalloc_stat_sub(__n) do { \
    pthread_mutex_lock(&used_memory_mutex); \
    used_memory -= (__n); \
    pthread_mutex_unlock(&used_memory_mutex); \
} while(0)
           

zcalloc

        zcalloc()的實作基于calloc(),但是兩者程式設計接口不同。看一下對比:

void *calloc(size_t nmemb, size_t size);
void *zcalloc(size_t size);
           

calloc()的功能是也是配置設定記憶體空間,與malloc()的不同之處有兩點:

  1. 它配置設定的空間大小是 size * nmemb。比如calloc(10,sizoef(char)); // 配置設定10個位元組
  2. calloc()會對配置設定的空間做初始化工作(初始化為0),而malloc()不會

輔助函數

  • calloc()
  • update_zmalloc_stat_alloc()【宏函數】
  • update_zmalloc_stat_add()【宏函數】

zcalloc()源碼

void *zcalloc(size_t size) {
    void *ptr = calloc(1, size+PREFIX_SIZE);

    if (!ptr) zmalloc_oom_handler(size);
#ifdef HAVE_MALLOC_SIZE
    update_zmalloc_stat_alloc(zmalloc_size(ptr));
    return ptr;
#else
    *((size_t*)ptr) = size;
    update_zmalloc_stat_alloc(size+PREFIX_SIZE);
    return (char*)ptr+PREFIX_SIZE;
#endif
}
           

        zcalloc()中沒有calloc()的第一個函數nmemb。因為它每次調用calloc(),其第一個參數都是1。也就是說zcalloc()功能是每次配置設定 size+PREFIX_SIZE 的空間,并初始化。 其餘代碼的分析和zmalloc()相同,也就是說:         zcalloc()和zmalloc()具有相同的程式設計接口,實作功能基本相同,唯一不同之處是zcalloc()會做初始化工作,而zmalloc()不會。

zrealloc

        zrealloc()和realloc()具有相同的程式設計接口:

void *realloc (void *ptr, size_t size);
void *zrealloc(void *ptr, size_t size);
           

        realloc()要完成的功能是給首位址ptr的記憶體空間,重新配置設定大小。如果失敗了,則在其它位置建立一塊大小為size位元組的空間,将原先的資料複制到新的記憶體空間,并傳回這段記憶體首位址【原記憶體會被系統自然釋放】。         zrealloc()要完成的功能也類似。 輔助函數:

  • zmalloc()
  • zmalloc_size()
  • realloc()
  • zmalloc_oom_handler【函數指針】
  • update_zmalloc_stat_free()【宏函數】
  • update_zmalloc_stat_alloc()【宏函數】

zrealloc()源碼

void *zrealloc(void *ptr, size_t size) {
#ifndef HAVE_MALLOC_SIZE
    void *realptr;
#endif
    size_t oldsize;
    void *newptr;

    if (ptr == NULL) return zmalloc(size);
#ifdef HAVE_MALLOC_SIZE
    oldsize = zmalloc_size(ptr);
    newptr = realloc(ptr,size);
    if (!newptr) zmalloc_oom_handler(size);

    update_zmalloc_stat_free(oldsize);
    update_zmalloc_stat_alloc(zmalloc_size(newptr));
    return newptr;
#else
    realptr = (char*)ptr-PREFIX_SIZE;
    oldsize = *((size_t*)realptr);
    newptr = realloc(realptr,size+PREFIX_SIZE);
    if (!newptr) zmalloc_oom_handler(size);

    *((size_t*)newptr) = size;
    update_zmalloc_stat_free(oldsize);
    update_zmalloc_stat_alloc(size);
    return (char*)newptr+PREFIX_SIZE;
#endif
}
           

經過前面關于zmalloc()和zfree()的源碼解讀,相信您一定能夠很輕松地讀懂zrealloc()的源碼,這裡我就不贅述了。

zstrdup

        從這個函數名中,很容易發現它是string duplicate的縮寫,即字元串複制。它的代碼比較簡單。先看一下聲明:

char *zstrdup(const char *s);
           

功能描述:複制字元串s的内容,到新的記憶體空間,構造新的字元串【堆區】。并将這段新的字元串位址傳回。

zstrdup源碼

char *zstrdup(const char *s) {
    size_t l = strlen(s)+1;
    char *p = zmalloc(l);

    memcpy(p,s,l);
    return p;
}
           
  1. 首先,先獲得字元串s的長度,新聞strlen()函數是不統計'\0'的,是以最後要加1。
  2. 然後調用zmalloc()來配置設定足夠的空間,首位址為p。
  3. 調用memcpy來完成複制。
  4. 然後傳回p。

簡單介紹一下memcpy

memcpy

        這是标準C【ANSI C】中用于記憶體複制的函數,在頭檔案<string.h>中(gcc)。聲明如下:

void *memcpy(void *dest, const void *src, size_t n);
           

dest即目的位址,src是源位址。n是要複制的位元組數。

繼續閱讀