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长度即得到真实的内存大小。
其他还有好几个方法,实现都比较简单,不再熬述。