Redis底層詳解(三) 內存管理

一、內存分配概述

        redis 的內存分配,實質上是對 tcmalloc / jemalloc 的封裝。內存分配本質就是給定需要分配的大小,以字節爲單位,然後返回一個指向一段分配好的連續的內存空間的首指針。
        通過這個首指針,我們需要知道它的連續空間的大小,才能進行內存統計,某些低版本的 tcmalloc / jemalloc 不支持通過給定指針獲取它申請的內存塊的大小,如果能夠通過接口獲得這個大小,那麼我們就定義宏 HAVE_MALLOC_SIZE 爲 1,並且定義 zmalloc_size 爲相應的接口函數。實現在 zmalloc.h 中:

        這段代碼的核心是宏定義 zmalloc_size,如果是用 jemalloc,那麼它就是 je_malloc_usable_size;如果是用 tcmalloc,那麼它就是 tc_malloc_size;如果是在 mac 上編譯,那麼就是 malloc_size 。
        從上面的宏定義可以看出:版本號小於1.6的 tcmalloc 以及 版本號小於2.1的 jemalloc, HAVE_MALLOC_SIZE 均爲未定義(本文的末尾,會給出 HAVE_MALLOC_SIZE 未定義的情況下 zmalloc_size 的實現方式)。

二、內存管理模型

        如果 HAVE_MALLOC_SIZE 未定義,那麼就代表在申請內存空間的時候,需要額外申請一塊空間來記錄這個需要申請的空間的實際字節數(方便申請和釋放的時候做內存統計),這個 “額外空間” 被放在申請空間的前面,它的字節數被定義爲 PREFIX_SIZE,定義在 zmalloc.c 中:

       __sun、__sparc、__sparc__的含義知不知道都無所謂,主要是對平臺的判斷。當 PREFIX_SIZE 不爲0的時候,內存模型如下圖所示:

三、內存分配

      接下來看下內存分配的幾個常用函數的宏替換,同樣定義在 zmalloc.c 中:

       經典的內存分配函數主要有3個:malloc(size)、calloc(count, size)、realloc(ptr, size)。
       malloc(size):分配 size 個字節的內存空間,返回值爲分配到的連續內存的首地址。分配的數據不做初始化。
       calloc(count, size):分配 count * size 個字節的內存空間,返回值爲分配到的連續內存的首地址。並且對分配後的數據進行初始化。
       realloc(ptr, size) 從 ptr 地址開始重新分配 size 個字節的空間,可以比原本 ptr 指向的連續空間的長度小或者大。

1、zmalloc

        接下來看下 redis 是如何對這幾個內存分配函數進行封裝的,首先是 zmalloc (size_t size),實現在 zmalloc.c 中:

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
}

        代碼比較簡短,其中 malloc 用來分配內存,長度爲 size+PREFIX_SIZE,返回指針 ptr。如果得到的 ptr 爲空,則表明內存分配失敗,一般是內存溢出了,直接調用內存溢出處理函數 zmalloc_oom_handler 進行處理 (oom 即 out of memory),zmalloc_oom_handler 是個函數指針,有個默認處理函數 zmalloc_default_oom,當然也可以通過 zmalloc_set_oom_handler (void (*oom_handler)(size_t)) 對默認處理函數進行替換。
       如果 HAVE_MALLOC_SIZE 未定義,則在 ptr 指向的位置的首地址上將 size 記錄下來,並且實際返回的地址是偏移了 PREFIX_SIZE 個字節的,即 (char*)ptr+PREFIX_SIZE 。因爲對用戶來說,這個 size 是它不需要關心的,它只關心申請到的內存。
       然後我們發現這裏做了一步操作,就是 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)); \     /* 1 */
    if (zmalloc_thread_safe) { \                                             /* 2 */
        update_zmalloc_stat_add(_n); \                                       /* 3 */
    } else { \ 
        used_memory += _n; \                                                 /* 4 */
    } \
} while(0)

       1、這段代碼比較有意思,首先我們看 sizeof(long),在32位機器下它的值是4,64位機器下值爲8。也就是無論如何都是2的冪,那麼 (_n&(sizeof(long)-1)) 的含義就是 (_n % sizeof(long) != 0),這句話的意思就是將 _n 向上補齊爲 sizeof(long) 的倍數。因爲 malloc 在分配內存的時候已經做了內存對齊(一定是 sizeof(long) 的倍數),所以補齊後的 _n 纔是真正的申請出來的內存大小。
       2、zmalloc_thread_safe 標記是否線程安全,通過 zmalloc_enable_thread_safeness(void) 函數來開啓。
       3、update_zmalloc_stat_add 是個宏定義,即線程安全版的 used_memory += _n。
       4、used_memory 是一個靜態變量,用於記錄一共分配了多少個字節的內存。

2、zcalloc

       zcalloc (size_t size) 的實現和 zmalloc (size_t size) 類似,malloc() 和 calloc() 的主要區別是前者不能初始化所分配的內存空間,而後者能。如果由 malloc() 函數分配的內存空間原來沒有被使用過,則其中的每一位可能都是0;反之,如果這部分內存曾經被分配過,則其中可能遺留有各種各樣的數據。zcalloc (size_t size) 的實現如下:

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
}

       基本上和 zmalloc (size_t size) 實現一模一樣,不再累述。

3、zrealloc

       接下來講下 zrealloc(void *ptr, size_t size),重分配函數實現如下:

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;                           /* 1 */
    oldsize = *((size_t*)realptr);                              /* 2 */
    newptr = realloc(realptr,size+PREFIX_SIZE);                 /* 3 */
    if (!newptr) zmalloc_oom_handler(size);                     /* 4 */

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

       HAVE_MALLOC_SIZE 在定義和未定義的情況下分別處理,這裏只介紹 HAVE_MALLOC_SIZE 未定義的情況(定義的情況相對較簡單):
       1、將當前指針 ptr 向前偏移 PREFIX_SIZE 個字節,得到真正內存分配的起始地址 realptr;
       2、取 realptr 位置上的值作爲該連續內存塊的大小,並且記錄在 oldsize 中;
       3、realloc 在 realptr 的位置分配 size+PREFIX_SIZE 的空間,返回 newptr。size 的值有可能比 oldsize 大或小,newptr 和 ptr 的值可能相同也可能不同,這個完全取決於 realloc 的實現。
       4、如若內存分配失敗,調用 out of memory 進行處理。
       5、將 size 記錄在 newptr 指向的位置上。
       6、update_zmalloc_stat_free 的作用和 update_zmalloc_stat_alloc 正好相反,都是操作 use_memory 這個靜態變量的。free 是減, alloc 是加。
       update_zmalloc_stat_free 的實現參考 update_zmalloc_stat_alloc,如下:

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

四、內存釋放

1、zfree

        有內存的分配,自然就有釋放,內存釋放的實現 zfree(void *ptr),定義在 zmalloc.c 中:

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
}

       在 HAVE_MALLOC_SIZE 未定義的情況下,實際分配的內存空間的指針位置需要向前偏移 PREFIX_SIZE 個字節,並且指針首地址內存的就是這次分配的內存空間的大小。調用 update_zmalloc_stat_free  更新 used_memory 的值後就可以調用 free(void *ptr) 進行內存釋放了。

五、其它

1、zmalloc_size

       到現在爲止,我們已經基本瞭解了 redis 的內存管理的實現。再回到文章開頭,只有當 HAVE_MALLOC_SIZE 被定義的情況下,才能獲取到 zmalloc_size,那麼如果系統沒有提供 zmalloc_size 函數的實現,我們要如何獲取當前指針的實際內存空間呢?
       在 HAVE_MALLOC_SIZE 未定義的情況下,zmalloc_size 實現如下:

#ifndef HAVE_MALLOC_SIZE
size_t zmalloc_size(void *ptr) {
    void *realptr = (char*)ptr-PREFIX_SIZE;
    size_t size = *((size_t*)realptr);
    if (size&(sizeof(long)-1)) size += sizeof(long)-(size&(sizeof(long)-1));
    return size+PREFIX_SIZE;
}
#endif

        首先 ptr 指針向前偏移 PREFIX_SIZE 個字節獲取到實際申請內存空間的起始地址 realptr, 從而得到這次分配的大小 size,再將 size 向上轉成 sizeof(long) 的倍數。這樣實際消耗內存大小就是 size + PREFIX_SIZE 了。

2、used_memory

        最後,靜態變量 used_memory,我們希望在外部獲取使用的內存大小,直接如下做法是有欠考慮的:

size_t zmalloc_used_memory(void) {
    return used_memory;
}

       原因是 used_memory 屬於共享資源,而 return 不是一個原子操作,我們需要考慮多線程的情況,正確實現如下:

size_t zmalloc_used_memory(void) {
    size_t um;

    if (zmalloc_thread_safe) {
#if defined(__ATOMIC_RELAXED) || defined(HAVE_ATOMIC)
        um = update_zmalloc_stat_add(0);
#else
        pthread_mutex_lock(&used_memory_mutex);
        um = used_memory;
        pthread_mutex_unlock(&used_memory_mutex);
#endif
    }
    else {
        um = used_memory;
    }

    return um;
}

       update_zmalloc_stat_add 之前提到過的 “原子加” 操作,pthread_mutex_lock 則是互斥鎖,在 線程安全標記 zmalloc_thread_safe 爲 1 的時候,需要進行原子操作將 used_memory 的值賦值給局部變量 um,然後再返回。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章