Redis源码解析——内存管理
? ? ? ? 在《Redis源碼解析——源碼工程結構》一文中,我們介紹了Redis可能會根據環境或用戶指定選擇不同的內存管理庫。在linux系統中,Redis默認使用jemalloc庫。當然用戶可以指定使用tcmalloc或者libc的原生內存管理庫。本文介紹的內容是在這些庫的基礎上,Redis封裝的功能。(轉載請指明出于breaksoftware的csdn博客)
統一函數名
? ? ? ? 首先Redis需要判斷最終選擇的內存管理庫是否可以滿足它的基礎需求。比如Redis需要能夠通過一個堆上分配的指針知曉其空間大小。但是并不是所有內存管理庫的每個版本都有這個方法。于是對于不滿足的就報錯
#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 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
? ? ? ? 上面這段代碼除了判斷內存庫的支持能力,還順帶統一zmalloc_size方法的實現。其實需要統一的方法不止這一個。比如libc的malloc方法在jemalloc中叫做je_malloc,而在tcmalloc中叫tc_malloc。這些基礎方法并不多,它們分別是單片內存分配的malloc方法、多片內存分配calloc方法、內存重分配的realloc方法和內存釋放函數free。經過統一命令后,之后使用這些方法的地方就不用考慮基礎庫不同的問題了。
#if defined(USE_TCMALLOC)
#define malloc(size) tc_malloc(size)
#define calloc(count,size) tc_calloc(count,size)
#define realloc(ptr,size) tc_realloc(ptr,size)
#define free(ptr) tc_free(ptr)
#elif defined(USE_JEMALLOC)
#define malloc(size) je_malloc(size)
#define calloc(count,size) je_calloc(count,size)
#define realloc(ptr,size) je_realloc(ptr,size)
#define free(ptr) je_free(ptr)
#endif
記錄堆空間申請大小
? ? ? ? Redis內存管理模塊需要實時知道已經申請了多少空間,它通過一個全局變量保存:
static size_t used_memory = 0;
? ? ? ? 由于內存分配可能發生在各個線程中,所以對這個數據的管理要做到原子性。但是不同平臺原子性操作的方法不同,有的甚至不支持原子操作,這個時候Redis就要統一它們的行為
pthread_mutex_t used_memory_mutex = PTHREAD_MUTEX_INITIALIZER;
……
#if defined(__ATOMIC_RELAXED)
#define update_zmalloc_stat_add(__n) __atomic_add_fetch(&used_memory, (__n), __ATOMIC_RELAXED)
#define update_zmalloc_stat_sub(__n) __atomic_sub_fetch(&used_memory, (__n), __ATOMIC_RELAXED)
#elif defined(HAVE_ATOMIC)
#define update_zmalloc_stat_add(__n) __sync_add_and_fetch(&used_memory, (__n))
#define update_zmalloc_stat_sub(__n) __sync_sub_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)#define update_zmalloc_stat_sub(__n) do { \pthread_mutex_lock(&used_memory_mutex); \used_memory -= (__n); \pthread_mutex_unlock(&used_memory_mutex); \
} while(0)#endif
? ? ? ? 一般來說,鎖操作比原子操作慢。但是在不支持原子操作的系統上只能使用鎖機制了。
? ? ? ? 但是作為一個基礎庫,它不能僅僅考慮到多線程的問題。比如用戶系統上不支持原子操作,而用戶也不希望擁有多線程安全特性(可能它只有一個線程在運行),那么上述接口在計算時就必須使用鎖機制,這樣對于性能有苛刻要求的場景是不能接受的。于是Redis暴露了一個方法用于讓用戶指定是否需要啟用線程安全特性
static int zmalloc_thread_safe = 0;void zmalloc_enable_thread_safeness(void) {zmalloc_thread_safe = 1;
}
? ? ? ? 相應的,線程安全的方法update_zmalloc_stat_add和update_zmalloc_stat_free需要被封裝,以滿足不同模式:
#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)
? ? ? ??之后我們在堆上分配釋放空間時,就需要使用update_zmalloc_stat_alloc和update_zmalloc_stat_free方法實時更新堆空間申請的情況。而獲取其值則需要下面的方法:
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);
#elsepthread_mutex_lock(&used_memory_mutex);um = used_memory;pthread_mutex_unlock(&used_memory_mutex);
#endif}else {um = used_memory;}return um;
}
內存分配和釋放
? ? ? ? 之前我們講過,Redis的內存分配庫需要底層庫支持通過堆上指針獲取該空間大小的功能,但是一些低版本的內存管理庫并不支持。針對這種場景Redis還是做了兼容,它設計的內存結構是Header+Body。在Header中保存了該堆空間Body的大小信息,而Body則用于返回給內存申請者。我們看下malloc的例子:
void *zmalloc(size_t size) {void *ptr = malloc(size+PREFIX_SIZE);if (!ptr) zmalloc_oom_handler(size);
#ifdef HAVE_MALLOC_SIZEupdate_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直接分配了一個比申請空間大的空間,這就意味著無論是否支持獲取申請空間大小的內存庫,它都一視同仁了——實際申請比用戶要求大一點。
? ? ? ? 如果內存庫支持,則通過zmalloc_size獲取剛分配的空間大小,并累計到記錄整個程序申請的堆空間大小上,然后返回申請了的地址。此時雖然用戶申請的只是size的大小,但是實際給了size+PREFIX_SIZE的大小。
? ? ? ? 如果內存庫不支持,則在申請的內存前sizeof(size_t)大小的空間里保存用戶需要申請的空間大小size。累計到記錄整個程序申請堆空間大小上的也是實際申請的大小。最后返回的是偏移了頭大小的內存地址。此時用戶拿到的空間就是自己要求申請的空間大小。
? ? ? ? 多片分配空間的zcalloc函數實現也是類似的,稍微有點區別的是重新分配空間的zrealloc方法,它需要在統計程序以申請堆空間大小的數據上減去以前該塊的大小,再加上新申請的空間大小
void *zrealloc(void *ptr, size_t size) {
#ifndef HAVE_MALLOC_SIZEvoid *realptr;
#endifsize_t oldsize;void *newptr;if (ptr == NULL) return zmalloc(size);
#ifdef HAVE_MALLOC_SIZEoldsize = 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;
#elserealptr = (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
}
? ? ? ? 還有就是zfree函數的實現,它需要釋放的空間起始地址要視庫的支持能力決定。如果庫不支持獲取區塊大小,則需要將傳入的指針前移PREFIX_SIZE,然后釋放該起始地址的空間。
void zfree(void *ptr) {
#ifndef HAVE_MALLOC_SIZEvoid *realptr;size_t oldsize;
#endifif (ptr == NULL) return;
#ifdef HAVE_MALLOC_SIZEupdate_zmalloc_stat_free(zmalloc_size(ptr));free(ptr);
#elserealptr = (char*)ptr-PREFIX_SIZE;oldsize = *((size_t*)realptr);update_zmalloc_stat_free(oldsize+PREFIX_SIZE);free(realptr);
#endif
}
? ? ? ? 最后我們看下Redis在內存分配時處理內存溢出的處理。它提供了一個接口,讓用戶處理內存溢出問題。當然它也有自己默認的處理邏輯:
static void (*zmalloc_oom_handler)(size_t) = zmalloc_default_oom;static void zmalloc_default_oom(size_t size) {fprintf(stderr, "zmalloc: Out of memory trying to allocate %zu bytes\n",size);fflush(stderr);abort();
}void zmalloc_set_oom_handler(void (*oom_handler)(size_t)) {zmalloc_oom_handler = oom_handler;
}
獲取進程內存信息
? ? ? ? Redis不僅在代碼層面要統計已申請的堆空間,還要通過其他方法獲取本進程中一些內存信息。比如它要通過zmalloc_get_rss方法獲取當前進程的實際使用物理內存。這個也要按系統支持來區分實現,比如支持/proc/%pid%/stat的使用:
size_t zmalloc_get_rss(void) {int page = sysconf(_SC_PAGESIZE);size_t rss;char buf[4096];char filename[256];int fd, count;char *p, *x;snprintf(filename,256,"/proc/%d/stat",getpid());if ((fd = open(filename,O_RDONLY)) == -1) return 0;if (read(fd,buf,4096) <= 0) {close(fd);return 0;}close(fd);p = buf;count = 23; /* RSS is the 24th field in /proc/<pid>/stat */while(p && count--) {p = strchr(p,' ');if (p) p++;}if (!p) return 0;x = strchr(p,' ');if (!x) return 0;*x = '\0';rss = strtoll(p,NULL,10);rss *= page;return rss;
}
? ? ? ? 如果支持使用task_for_pid方法的則使用:
size_t zmalloc_get_rss(void) {task_t task = MACH_PORT_NULL;struct task_basic_info t_info;mach_msg_type_number_t t_info_count = TASK_BASIC_INFO_COUNT;if (task_for_pid(current_task(), getpid(), &task) != KERN_SUCCESS)return 0;task_info(task, TASK_BASIC_INFO, (task_info_t)&t_info, &t_info_count);return t_info.resident_size;
}
? ? ? ? 獲取完物理內存數據后,可以通過和累計的分配內存大小相除,算出內存使用效率:
float zmalloc_get_fragmentation_ratio(size_t rss) {return (float)rss/zmalloc_used_memory();
}
? ? ? ? Redis源碼說明上指出上述獲取RSS信息的方法是不高效的。可以通過RedisEstimateRSS()方法高效獲取。
? ? ? ? 除了上面這些方法,Redis還有獲取已被修改的私有頁面大小函數zmalloc_get_private_dirty以及獲取物理內存((RAM))大小的zmalloc_get_memory_size方法。這些方法都是些系統性方法,我就不在這兒做說明了。
總結
以上是生活随笔為你收集整理的Redis源码解析——内存管理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Redis源码解析——前言
- 下一篇: Redis源码解析——字典结构