1. 程式人生 > >Redis 原始碼分析(zmalloc部分)

Redis 原始碼分析(zmalloc部分)

Redis 2.8.24

Redis在這個版本使用三種選擇作為allocator,

a) tcmalloc:一種比glibc 2.3更快的malloc實現,由google用於優化C++多執行緒應用而開發。Redis 需要1.6以上的版本。

b) jemalloc:第一次用在FreeBSD 的allocator,於2005年釋出的版本。強調降低碎片化,可擴充套件的並行支援。Redis需要2.1以上版本。

c) libc:最常使用的libc庫。GNU libc,預設使用此allocator。

Redis原始碼結構比較清晰,其中的記憶體分配器即是zmalloc部分。寫在zmalloc.h 和 zmalloc.c,先看標頭檔案:

#if defined(USE_TCMALLOC)
#define ZMALLOC_LIB ("tcmalloc-" __xstr(TC_VERSION_MAJOR) "." __xstr(TC_VERSION_MINOR))
#include <google/tcmalloc.h>
#if (TC_VERSION_MAJOR == 1 && TC_VERSION_MINOR >= 6) || (TC_VERSION_MAJOR > 1)
#define HAVE_MALLOC_SIZE 1
#define zmalloc_size(p) tc_malloc_size(p)
#else
#error "Newer version of tcmalloc required"
#endif

#elif defined(USE_JEMALLOC)
#define ZMALLOC_LIB ("jemalloc-" __xstr(JEMALLOC_VERSION_MAJOR) "." __xstr(JEMALLOC_VERSION_MINOR) "." __xstr(JEMALLOC_VERSION_BUGFIX))
#include <jemalloc/jemalloc.h>
#if (JEMALLOC_VERSION_MAJOR == 2 && JEMALLOC_VERSION_MINOR >= 1) || (JEMALLOC_VERSION_MAJOR > 2)
#define HAVE_MALLOC_SIZE 1
#define zmalloc_size(p) je_malloc_usable_size(p)
#else
#error "Newer version of jemalloc required"
#endif

#elif defined(__APPLE__)
#include <malloc/malloc.h>
#define HAVE_MALLOC_SIZE 1
#define zmalloc_size(p) malloc_size(p)
#endif

#ifndef ZMALLOC_LIB
#define ZMALLOC_LIB "libc"
#endif
由巨集USE_TCMALLOC,USE_JEMALLOC和__APPLE__控制要編譯進Redis的allocator,前兩個巨集從make 傳入,後面一個是作業系統巨集,若是Apple,則可以提供一個

malloc_size (),用於檢視指標指向記憶體的大小。此函式在jemalloc和tcmalloc中都有提供,但glibc中不提供此函式,巨集HAVE_MALLOC_SIZE即是用於控制此函式。使用glibc的情況下,將不會定義HAVE_MALLOC_SIZE巨集,標頭檔案中申明瞭使用glibc時提供的zmalloc_size ():

#ifndef HAVE_MALLOC_SIZE
size_t zmalloc_size(void *ptr);
#endif

接下來看看原始檔zmalloc.c;

此檔案中定義了三個全域性變數:

static size_t used_memory = 0;
static int zmalloc_thread_safe = 0;
pthread_mutex_t used_memory_mutex = PTHREAD_MUTEX_INITIALIZER;

used_memory:已經申請的記憶體總位元組數。

zmalloc_thread_safe:標識是否是執行緒安全的,預設為0,不安全。

used_memory_mutex:將used_memory作為臨界變數,鎖住此變數。

關於HAVE_MALLOC_SIZE,前面已經講過,使用glibc則沒有定義此巨集。此時申請的記憶體塊將多出PREFIX_SIZE位元組在記憶體塊起始地址處,用於儲存記憶體塊大小。隨後將記憶體地址偏移PREFIX_SIZE位元組,從此開始即是申請的可使用記憶體。PREFIX_SIZE 是一個巨集,不同作業系統(也可能是處理器)的值略有不同:

#ifdef HAVE_MALLOC_SIZE
#define PREFIX_SIZE (0)
#else
#if defined(__sun) || defined(__sparc) || defined(__sparc__)
#define PREFIX_SIZE (sizeof(long long))
#else
#define PREFIX_SIZE (sizeof(size_t))
#endif
#endif // HAVE_MALLOC_SIZE
此處的巨集__sun 或 __sparc 或 __sparc__ 貌似是標識處理器,或者作業系統。不過最重要的還是檢視glibc下的值,sizeof (size_t)。與機器有關,我的機器是64位,這個值應該是一個8bytes 整形。

下面看其他的API:

1、void* zmalloc (size_t size):基本思路剛才已經說過,在申請記憶體時多申請PREFIX_SIZE個位元組用於儲存記憶體塊大小,而後把指標偏移PREFIX_SIZE bytes後得到可以使用的記憶體。

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
}
此處的zmalloc_oom_handler () 將abort () 程式,用於處理OOM(out of memory)。其中用到一個update_zmalloc_stat_alloc ()函式,其實是一個巨集。tcmalloc和jemalloc因為提供malloc_size (),記憶體塊大小不需要我們記錄。所以直接呼叫__sync_add_and_fetch () 和 __sync_sub_and_fetch () 統計記憶體塊使用情況。先看這兩個巨集,分別在

update_zmalloc_stat_alloc () 和 update_zmalloc_stat_free () 中呼叫,用於記憶體使用情況資訊更新。

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

#define update_zmalloc_stat_sub(__n) do { \
    pthread_mutex_lock(&used_memory_mutex); \
    used_memory -= (__n); \
    pthread_mutex_unlock(&used_memory_mutex); \
} while(0)
程式碼很簡單,lock 臨界區,累加,unlock。以下是兩個狀態更新函式:
#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)

#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)
這裡使用了一個優化,用於機器字對齊,使記憶體訪問更快。_n & (sizeof (long) - 1)是否能被8 或者4整除(與機器有關)。若非0,則不能整除,加上剩餘位元組數使之能整除。

舉個例子:x64機器上,申請20bytes, 20 & (sizeof (long) - 1) = 20 & 7 = 4,非零,不能整除,即缺 8 - 4 = 4 bytes,於是申請的20 bytes多申請4bytes,24 mod 8 = 0正好。

注:預設此處的zmalloc_thread_safe為0,即是執行在單執行緒下,不用加鎖,直接在used_memory 累加累減即可。

2、 void zfree (void* ptr):在使用jemalloc 和 tcmalloc時,記憶體申請時的長度不加上PREFIX_SIZE,直接free ()即可,而glibc 要將指標偏移回PREFIX_SIZE,再呼叫 free ():

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
}

3、 size_t zmalloc_size (void* ptr):這個函式是為glibc定製的,只有用這個庫時,才能使用這個函式:

#ifndef HAVE_MALLOC_SIZE
size_t zmalloc_size(void *ptr) {
    void *realptr = (char*)ptr-PREFIX_SIZE;
    size_t size = *((size_t*)realptr);
    /* Assume at least that all the allocations are padded at sizeof(long) by
     * the underlying allocator. */
    if (size&(sizeof(long)-1)) size += sizeof(long)-(size&(sizeof(long)-1));
    return size+PREFIX_SIZE;
}
#endif
原理即時將指標偏移PREFIX_SIZE ,得到塊大小,再加上PREFIX_SIZE長度即得到真實的記憶體大小。

其他還有好幾個方法,實現都比較簡單,不再熬述。