redis 源代码分析(一) 内存管理
一,redis内存管理介绍
redis是一个基于内存的key-value的数据库,其内存管理是很重要的,为了屏蔽不同平台之间的差异,以及统计内存占用量等,redis对内存分配函数进行了一层封装,程序中统一使用zmalloc,zfree一系列函数,其相应的源代码在src/zmalloc.h和src/zmalloc.c两个文件里,源代码点这里。
二,redis内存管理源代码分析
redis封装是为了屏蔽底层平台的差异,同一时候方便自己实现相关的函数,我们能够通过src/zmalloc.h 文件里的相关宏定义来分析redis是怎么实现底层平台差异的屏蔽的,zmalloc.h 中相关宏声明例如以下:
#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 ... #ifndef HAVE_MALLOC_SIZE size_t zmalloc_size(void *ptr); #endif
通过上面的宏的预处理我们能够发现redis为了屏蔽不同系统(库)的差异进行了例如以下预处理:
A,若系统中存在Google的TC_MALLOC库,则使用tc_malloc一族函数取代原本的malloc一族函数。
B,若系统中存在FaceBook的JEMALLOC库,则使用je_malloc一族函数取代原本的malloc一族函数。
C,若当前系统是Mac系统,则使用<malloc/malloc.h>中的内存分配函数。
D,其它情况,在每一段分配好的空间前头,同一时候多分配一个定长的字段,用来记录分配的空间大小。
tc_malloc是google开源处理的一套内存管理库,是用C++实现的,主页在这里。TCMalloc给每一个线程分配了一个线程局部缓存。小分配能够直接由线程局部缓存来满足。须要的话,会将对象从中央数据结构移动到线程局部缓存中,同一时候定期的垃圾收集将用于把内存从线程局部缓存迁移回中央数据结构中。这篇文章里对TCMalloc有个具体的介绍。
jemalloc 也是一个内存创管理库,其创始人Jason Evans也是在FreeBSD非常有名的开发者,參见这里。Jemalloc聚集了malloc的使用过程中所验证的非常多技术。忽略细节,从架构着眼,最出色的部分仍是arena和thread cache。
读者一定会有疑问系统不是有了malloc 吗,为什么还有这种内存管理库?? 因为经典的libc的分配器碎片率为较高,能够查看这篇文章的分析,关于内存碎片不太了解的童鞋请參考这里, malloc 和free 怎么工作的參考这里。 关于ptmalloc,tcmalloc和jemalloc内存分配策略的一篇总结不错的文章,请点这里。
以下介绍redis封装的内存管理相关函数,src/zmalloc.h有相关声明。
void *zmalloc(size_t size);//malloc void *zcalloc(size_t size);//calloc void *zrealloc(void *ptr, size_t size);/realloc void zfree(void *ptr);//free 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(void); size_t zmalloc_get_rss(void); size_t zmalloc_get_private_dirty(void); void zlibc_free(void *ptr);
如今主要介绍下redis内存分配函数 void *zmalloc(size_t size),其相应的声明形式例如以下:
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 }
阅读源代码我们发现有个PREFIX_SIZE 宏,其宏定义形式例如以下:
/* zmalloc.c */ #ifdef HAVE_MALLOC_SIZE #define PREFIX_SIZE (0) #else #if defined(__sun) #define PREFIX_SIZE (sizeof(long long)) #else #define PREFIX_SIZE (sizeof(size_t)) #endif #endif
结合src/zmalloc.h有相关宏声明,我们发现,由于 tc_malloc 、je_malloc 和 Mac平台下的 malloc 函数族提供了计算已分配空间大小的函数(各自是tc_malloc_size, je_malloc_usable_size和malloc_size),所以就不须要单独分配一段空间记录大小了。在linux和sun平台则要记录分配空间大小。对于linux,使用sizeof(size_t)定长字段记录;对于sun 系统,使用sizeof(long long)定长字段记录,其相应源代码中的 PREFIX_SIZE 宏。
PREFIX_SIZE 有什么用呢?
为了统计当前进程究竟占用了多少内存。在 zmalloc.c 中,有一个静态变量:
static size_t used_memory = 0;这个变量它记录了进程当前占用的内存总数。每当要分配内存或是释放内存的时候,都要更新这个变量(当然能够是线程安全的)。由于分配内存的时候,须要指定分配多少内存。可是释放内存的时候,(对于未提供malloc_size函数的内存库)通过指向要释放内存的指针是不能知道释放的空间究竟有多大的。这时候,上面提到的PREFIX_SIZE就起作用了,能够通过当中记录的内容得到空间的大小。(只是在linux系统上也有对应的函数获得分配内存空间的大小,參见这里)。
通过zmalloc的源代码我们能够发现,其分配空间代码为void *ptr = malloc(size+PREFIX_SIZE); 显然其分配空间大小为:size+PREFIX_SIZE ,对于使用tc_malloc或je_malloc的情况或mac系统,其 PREFIX_SIZE 为0。当分配失败时有对应的出错处理 。
前面我们已经说过redis通过使用used_memory 的变量来统计当前进程究竟占用了多少内存,因此在分配和释放内存时我们须要紧接着更新used_memory 的相应值,相应到redis源代码中为:
#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上面的代码有事宏预处理 #ifdef HAVE_MALLOC_SIZE 显然是上面我们说过的利用的tc_malloc je_malloc Mac等提供malloc_size函数的情形,我们能够非常easy得知分配内存的大小通过统一化的malloc_size函数就可以。可是对于没有提供malloc_size功能的函数,redis是怎么处理的呢?看上面的源代码 #else以下的代码即是事实上现,其相应的内存结构例如以下:
prefix-size | memory size |
redis通过update_zmalloc_stat_alloc(__n,__size) 和 update_zmalloc_stat_free(__n) 这两个宏负责在分配内存或是释放内存的时候更新used_memory变量。update_zmalloc_stat_alloc定义例如以下:
#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)redis把这个更新操作写成宏的形式主要是处于效率的考虑。
上面的代码中
A,if (_n&(sizeof(long)-1)) _n += sizeof(long)-(_n&(sizeof(long)-1));
主要是考虑对齐问题,保证新增的_n 是 sizeof(long)的倍数。
B, if (zmalloc_thread_safe) { \
update_zmalloc_stat_add(_n); \
}
假设进程中有多个线程存在,并保证线程安全zmalloc_thread_safe,则在更新变量的时候要加锁。 通过宏HAVE_ATOMIC选择对应的同步机制。
zmalloc_calloc、zmalloc_free等的实现就不细致介绍了详情參见源代码。
最后解说下 zmalloc_get_rss()函数。
这个函数用来获取进程的RSS。神马是RSS?全称为Resident Set Size,指实际使用物理内存(包括共享库占用的内存)。在linux系统中,能够通过读取/proc/pid/stat文件系统获取,pid为当前进程的进程号。读取到的不是byte数,而是内存页数。通过系统调用sysconf(_SC_PAGESIZE)能够获得当前系统的内存页大小。 获得进程的RSS后,能够计算眼下数据的内存碎片大小,直接用rss除以used_memory。rss包括进程的全部内存使用,包括代码,共享库,堆栈等。 哪来的内存碎片?上面我们已经说明了通常考虑到效率,往往有内存对齐等方面的考虑,所以,碎片就在这里产生了。相比传统glibc中的malloc的内存利用率不是非常高通常会使用别的内存库系统。在redis中默认的已经不使用简单的malloc了而是使用
jemalloc, 在源文件src/Makefile下有这样一段代码:
能够知道在linux系统上默认使用jemalloc, 在redis公布的源代码中有相关的库 deps/jemalloc 。ifeq ($(uname_S),Linux)MALLOC=jemalloc
总的来说 redis则全然自主分配内存,在请求到的时候实时依据内建的算法分配内存,全然自主控制内存的管理。简单即是没吧,只是功能确实强大。
參考:
http://blog.ddup.us/?p=136