Redis源碼剖析--內存分配

請持續關注我的個人博客:https://zcheng.ren

深受侯捷老師的《STL源碼剖析》一書的影響,在該書中開篇就對STL的空間配置器進行了一個詳盡的介紹。以應用的角度而言,空間配置器是最不需要介紹的,它總是隱藏在一切組件的背後;可是,就源碼分析而言,空間配置是最爲關鍵的,也是分析源碼之路的第一步!Redis在內存分配方面,僅僅是對系統的malloc/free做了一層簡單的封裝,然後加上了異常處理功能和內存統計功能。其實現主要在zmalloc.c和zmalloc.h文件中。

功能函數總覽

在zmalloc.h中,定義了Redis內存分配的主要功能函數,這些函數基本上實現了Redis內存申請,釋放和統計等功能,其函數聲明如下:

void *zmalloc(size_t size); // 調用zmalloc函數,申請size大小的空間
void *zcalloc(size_t size); // 調用系統函數calloc申請內存空間
void *zrealloc(void *ptr, size_t size); // 原內存重新調整爲size空間的大小
void zfree(void *ptr);  // 調用zfree釋放內存空間
char *zstrdup(const char *s); // 字符串複製方法
size_t zmalloc_used_memory(void); // 獲取當前以及佔用的內存空間大小
void zmalloc_enable_thread_safeness(void); // 是否設置線程安全模式
void zmalloc_set_oom_handler(void (*oom_handler)(size_t)); // 可自定義設置內存溢出的處理方法
float zmalloc_get_fragmentation_ratio(size_t rss); // 獲取所給內存和已使用內存的大小之比
size_t zmalloc_get_rss(void); // 獲取RSS信息(Resident Set Size)
size_t zmalloc_get_private_dirty(void); // 獲得實際內存大小
size_t zmalloc_get_smap_bytes_by_field(char *field); // 獲取/proc/self/smaps字段的字節數
size_t zmalloc_get_memory_size(void); // 獲取物理內存大小
void zlibc_free(void *ptr); // 原始系統free釋放方法

另外,我們還要注意到zmalloc.c中的幾個變量和概念,

static size_t used_memory = 0;  // 已使用內存的大小
static int zmalloc_thread_safe = 0; // 線程安全模式狀態
pthread_mutex_t used_memory_mutex = PTHREAD_MUTEX_INITIALIZER; // 爲此服務器

接下來,我分幾個章節來一一剖析zmalloc.c中的函數實現。

內存管理函數

內存申請函數zmalloc

Redis的內存申請函數zmalloc本質就是調用了系統的malloc函數,然後對其進行了適當的封裝,加上了異常處理函數和內存統計。其源代碼如下:

void *zmalloc(size_t size) {
    // 調用malloc函數進行內存申請
    // 多申請的PREFIX_SIZE大小的內存用於記錄該段內存的大小
    void *ptr = malloc(size+PREFIX_SIZE);

    // 如果ptr爲NULL,則調用異常處理函數
    if (!ptr) zmalloc_oom_handler(size);
    // 以下是內存統計
    *((size_t*)ptr) = size;
    update_zmalloc_stat_alloc(size+PREFIX_SIZE); // 更新used_memory的值
    return (char*)ptr+PREFIX_SIZE;
}

上述代碼中的PREFIX_SIZE解釋:由於malloc函數申請的內存不會標識內存塊的大小,而我們需要統計內存大小,所以需要在多申請PREFIX_SIZE大小的內存,用於存放該大小。

其中,異常處理函數如下:

static void zmalloc_default_oom(size_t size) {
    fprintf(stderr, "zmalloc: Out of memory trying to allocate %zu bytes\n", // 打印輸出日誌
        size);
    fflush(stderr);
    abort(); // 中斷退出
}

更新used_memory值得函數以宏定義給出,其代碼和註釋如下:

#define update_zmalloc_stat_alloc(__n) do { \
    size_t _n = (__n); \
    if (_n&(sizeof(long)-1)) _n += sizeof(long)-(_n&(sizeof(long)-1)); \   // 將_n調整爲sizeof(long)的整數倍
    if (zmalloc_thread_safe) { \ // 如果啓用了線程安全模式
        update_zmalloc_stat_add(_n); \   // 調用原子操作加(+)來更新已用內存
    } else { \
        used_memory += _n; \   // 不考慮線程安全,則直接更新已用內存
    } \
} while(0)

在上述函數中,又用到了原子加操作,其代碼和註釋如下:

// __atomic_add_fetch是C++11特性中提供的原子加操作
#if defined(__ATOMIC_RELAXED)
#define update_zmalloc_stat_add(__n) __atomic_add_fetch(&used_memory, (__n), __ATOMIC_RELAXED)
// 如果不支持C++11,則調用GCC提供的原子加操作
#elif defined(HAVE_ATOMIC)
#define update_zmalloc_stat_add(__n) __sync_add_and_fetch(&used_memory, (__n))
// 如果上述都沒有,則只能採用加鎖操作
#else
#define update_zmalloc_stat_add(__n) do { \
    pthread_mutex_lock(&used_memory_mutex); \
    used_memory += (__n); \
    pthread_mutex_unlock(&used_memory_mutex); \
} while(0)

內存申請函數zcalloc

與malloc一樣,zcalloc調用的是系統給的calloc()來申請內存。

void *zcalloc(size_t size) {
    void *ptr = calloc(1, size+PREFIX_SIZE);
    // 異常處理函數
    if (!ptr) zmalloc_oom_handler(size);
    // 內存統計函數
    *((size_t*)ptr) = size;
    update_zmalloc_stat_alloc(size+PREFIX_SIZE);
    return (char*)ptr+PREFIX_SIZE;
}

內存調整函數zrecalloc

Redis定義的zrecalloc用於調整已申請內存的大小,其本質也是直接調用系統函數recalloc()

void *zrealloc(void *ptr, size_t size) {
    size_t oldsize;
    void *newptr;
    // 爲空直接退出
    if (ptr == NULL) return zmalloc(size);
    // 找到內存真正的起始位置
    realptr = (char*)ptr-PREFIX_SIZE;
    oldsize = *((size_t*)realptr);
    // 調用recalloc函數
    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;
}

內存釋放函數

與內存申請函數調用malloc一樣,內存釋放也是調用系統的free()函數來實現內存釋放

void zfree(void *ptr) {
    if (ptr == NULL) return;  // 爲空直接返回
    realptr = (char*)ptr-PREFIX_SIZE; // 找到該段內存真正的起始位置
    oldsize = *((size_t*)realptr);
    update_zmalloc_stat_free(oldsize+PREFIX_SIZE);// 更新use_memory函數
    free(realptr); // 調用系統的內存釋放函數
}

其中,內存狀態統計函數的代碼實現如下:

#define update_zmalloc_stat_free(__n) do { \
    size_t _n = (__n); \ 
    if (_n&(sizeof(long)-1)) _n += sizeof(long)-(_n&(sizeof(long)-1)); \  // 將內存大小調整爲sizeof(long)的整數倍
    if (zmalloc_thread_safe) { \  // 如果開啓了線程安全模式
        update_zmalloc_stat_sub(_n); \ // 更新use_memory值(與上述的update_zmalloc_stat_add這裏就不贅述了)
    } else { \
        used_memory -= _n; \ // 沒有線程安全則直接減
    } \
} while(0)

講到這裏,Redis基本的內存處理函數已經分析完畢了。另外,Redis還提供了一下輔助函數,我這裏簡要的分析一下。

輔助函數

字符串複製方法

暫時不清楚這個函數爲什麼要放在這裏。

char *zstrdup(const char *s) {
    size_t l = strlen(s)+1; 
    char *p = zmalloc(l); // 開闢一段新內存

    memcpy(p,s,l); // 調用字符串複製函數
    return p;
}

設置異常處理函數

Redis允許自行設定異常處理函數,也提供瞭如下的函數。

void zmalloc_set_oom_handler(void (*oom_handler)(size_t)) {
    zmalloc_oom_handler = oom_handler; // 綁定自定義的異常處理函數
}

開啓線程安全

void zmalloc_enable_thread_safeness(void) {
    zmalloc_thread_safe = 1;  // 此參數用來控制是否開啓線程安全
}

獲取已使用內存

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; // 未開啓則直接使用used_memory
    }

    return um;
}

到此,Redis的內存管理函數已分析完。
end…

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