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

分配的内存前加一个固定大小的prefis-size空间,用于记录该段内存的大小,size所占领的内存大小是已知的,为size_t类型的长度,因此通过*((size_t*)ptr) = size; 就可以对当前内存块大小进行指定。每次分配内存后,返回的实际地址指针为指向memorysize的地址( (char*)ptr+PREFIX_SIZE; ),通过该指针,能够非常easy的计算出实际内存的头地址,从而释放内存。

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下有这样一段代码:

ifeq ($(uname_S),Linux)
MALLOC=jemalloc

能够知道在linux系统上默认使用jemalloc, 在redis公布的源代码中有相关的库 deps/jemalloc 。

总的来说 redis则全然自主分配内存,在请求到的时候实时依据内建的算法分配内存,全然自主控制内存的管理。简单即是没吧,只是功能确实强大。

參考:

http://blog.ddup.us/?p=136

上一篇:Go的pprof使用


下一篇:Zookeeper的安装的配置