redis源码分析(一)内存管理
一,redis内存管理介绍 redis是一个基于内存的key-value的数据库,其内存管理是非常重要的,为了屏蔽不同平台之间的差异,以及统计内存占用量等,redis对内存分配函数进行了一层封装,程序中统一使用zmalloc,zfree一系列函数,其对应的源码在src/zmalloc.h
一,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#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 #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 #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系统,则使用
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函数的情形,我们可以很容易得知分配内存的大小通过统一化的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下有这样一段代码:
ifeq ($(uname_S),Linux) MALLOC=jemalloc可以知道在linux系统上默认使用jemalloc, 在redis发布的源码中有相关的库 deps/jemalloc 。
总的来说 redis则完全自主分配内存,在请求到的时候实时根据内建的算法分配内存,完全自主控制内存的管理。简单即是没吧,不过功能确实强大。
推荐阅读
-
Python的socket模块源码中的一些实现要点分析
-
go开源项目influxdb-relay源码分析(一)
-
scrapy-redis源码分析之发送POST请求详解
-
vuex 源码分析(一) 使用方法和代码结构
-
一个Vue页面的内存泄露分析详解
-
kubernetes垃圾回收器GarbageCollector源码分析(一)
-
一文了解 Redis 内存监控和内存消耗
-
netty源码解析(4.0)-28 ByteBuf内存池:PooledByteBufAllocator-把一切组装起来
-
Flink中watermark为什么选择最小一条(源码分析)
-
Python的socket模块源码中的一些实现要点分析