Redis原始碼剖析--記憶體管理zmalloc
功能函式總覽
在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釋放記憶體空間 void zmalloc_set_oom_handler(void (*oom_handler)(size_t)); // 可自定義設定記憶體溢位的處理方法 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
Redis的記憶體申請函式zmalloc本質就是呼叫了系統的malloc函式,然後對其進行了適當的封裝,加上了異常處理函式和記憶體統計。在zmalloc函式中,實際可能會每次多申請一個 PREFIX_SIZE的空間。從如下的程式碼中看出,如果定義了巨集HAVE_MALLOC_SIZE,那麼 PREFIX_SIZE的長度為0。其他的情況下,都會多分配額外的PREFIX_SIZE記憶體空間來儲存記憶體空間大小。其原始碼如下:
// 定義了HAVE_MALLOC_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
void *zmalloc(size_t size) { // 呼叫malloc函式進行記憶體申請 // 多申請的PREFIX_SIZE大小的記憶體用於記錄該段記憶體的大小 void *ptr = malloc(size+PREFIX_SIZE); // 如果ptr為NULL,則呼叫異常處理函式 if (!ptr) zmalloc_oom_handler(size); // 更新use_memory的大小 #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 }
這麼做的原因是因為: tcmalloc 和 Mac平臺下的 malloc 函式族提供了計算已分配空間大小的函式(分別是tc_malloc_size和malloc_size),所以就不需要單獨分配一段空間記錄大小了。而針對linux和sun平臺則要記錄分配空間大小。對於linux,使用sizeof(size_t)定長欄位記錄;對於sun os,使用sizeof(long long)定長欄位記錄。因此當巨集HAVE_MALLOC_SIZE沒有被定義的時候,就需要在多分配出的空間內記錄下當前申請的記憶體空間的大小。
對於巨集HAVE_MALLOC_SIZE的定義如下(此處僅給出tcmalloc部分):
#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) // 安裝tcmalloc時,通過巨集使用計算分配空間大小的函式tc_malloc_size() #define HAVE_MALLOC_SIZE 1 #define zmalloc_size(p) tc_malloc_size(p) #else #error "Newer version of tcmalloc required" #endif
update_zmalloc_stat_alloc
update_zmalloc_stat_alloc 是一個巨集,因為sizeof(long) == 8 [64位系統中],所以其實第一個if的程式碼等價於if(_n&7) _n += 8 - (_n&7); 這段程式碼就是判斷分配的記憶體空間的大小是不是8的倍數。如果記憶體大小不是8的倍數,就加上相應的偏移量使之變成8的倍數。_n&7 在功能上等價於 _n%8,不過位操作的效率顯然更高。第二個if主要判斷當前是否處於執行緒安全的情況下。如果處於執行緒安全的情況下,就使用update_zmalloc_stat_add巨集來更改全域性變數used_memory。否則的話就直接加上n。
#define update_zmalloc_stat_alloc(__n) do { \ size_t _n = (__n); \ // 將_n調整為sizeof(long)的整數倍 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)
update_zmalloc_stat_add()的定義如下:
// __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)
zfree
有分配就有記憶體回收,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 }
上面的程式碼可以看出,根據用的庫不相同,回收的時候也採用了不同的方法。
可以發現如果使用的libc庫,則需要將ptr指標向前偏移PREFIX_SIZE個位元組的長度,回退到最初malloc返回的地址,然後通過型別轉換再取指標所指向的值。通過zmalloc()函式的分析,可知這裡儲存著我們最初需要分配的記憶體大小(zmalloc中的size),這裡賦值給oldsize。update_zmalloc_stat_free()也是一個巨集函式,和zmalloc中update_zmalloc_stat_alloc()大致相同,唯一不同之處是前者在給變數used_memory減去分配的空間,而後者是加上該空間大小。
最後free(realptr),清除空間。
在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釋放記憶體空間 void zmalloc_set_oom_handler(void (*oom_handler)(size_t)); // 可自定義設定記憶體溢位的處理方法 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
Redis的記憶體申請函式zmalloc本質就是呼叫了系統的malloc函式,然後對其進行了適當的封裝,加上了異常處理函式和記憶體統計。在zmalloc函式中,實際可能會每次多申請一個 PREFIX_SIZE的空間。從如下的程式碼中看出,如果定義了巨集HAVE_MALLOC_SIZE,那麼 PREFIX_SIZE的長度為0。其他的情況下,都會多分配額外的PREFIX_SIZE記憶體空間來儲存記憶體空間大小。其原始碼如下:
// 定義了HAVE_MALLOC_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
void *zmalloc(size_t size) { // 呼叫malloc函式進行記憶體申請 // 多申請的PREFIX_SIZE大小的記憶體用於記錄該段記憶體的大小 void *ptr = malloc(size+PREFIX_SIZE); // 如果ptr為NULL,則呼叫異常處理函式 if (!ptr) zmalloc_oom_handler(size); // 更新use_memory的大小 #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 }
這麼做的原因是因為: tcmalloc 和 Mac平臺下的 malloc 函式族提供了計算已分配空間大小的函式(分別是tc_malloc_size和malloc_size),所以就不需要單獨分配一段空間記錄大小了。而針對linux和sun平臺則要記錄分配空間大小。對於linux,使用sizeof(size_t)定長欄位記錄;對於sun os,使用sizeof(long long)定長欄位記錄。因此當巨集HAVE_MALLOC_SIZE沒有被定義的時候,就需要在多分配出的空間內記錄下當前申請的記憶體空間的大小。
對於巨集HAVE_MALLOC_SIZE的定義如下(此處僅給出tcmalloc部分):
#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) // 安裝tcmalloc時,通過巨集使用計算分配空間大小的函式tc_malloc_size() #define HAVE_MALLOC_SIZE 1 #define zmalloc_size(p) tc_malloc_size(p) #else #error "Newer version of tcmalloc required" #endif
update_zmalloc_stat_alloc
update_zmalloc_stat_alloc 是一個巨集,因為sizeof(long) == 8 [64位系統中],所以其實第一個if的程式碼等價於if(_n&7) _n += 8 - (_n&7); 這段程式碼就是判斷分配的記憶體空間的大小是不是8的倍數。如果記憶體大小不是8的倍數,就加上相應的偏移量使之變成8的倍數。_n&7 在功能上等價於 _n%8,不過位操作的效率顯然更高。第二個if主要判斷當前是否處於執行緒安全的情況下。如果處於執行緒安全的情況下,就使用update_zmalloc_stat_add巨集來更改全域性變數used_memory。否則的話就直接加上n。
#define update_zmalloc_stat_alloc(__n) do { \ size_t _n = (__n); \ // 將_n調整為sizeof(long)的整數倍 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)
update_zmalloc_stat_add()的定義如下:
// __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)
zfree
有分配就有記憶體回收,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 }
上面的程式碼可以看出,根據用的庫不相同,回收的時候也採用了不同的方法。
可以發現如果使用的libc庫,則需要將ptr指標向前偏移PREFIX_SIZE個位元組的長度,回退到最初malloc返回的地址,然後通過型別轉換再取指標所指向的值。通過zmalloc()函式的分析,可知這裡儲存著我們最初需要分配的記憶體大小(zmalloc中的size),這裡賦值給oldsize。update_zmalloc_stat_free()也是一個巨集函式,和zmalloc中update_zmalloc_stat_alloc()大致相同,唯一不同之處是前者在給變數used_memory減去分配的空間,而後者是加上該空間大小。
最後free(realptr),清除空間。