Redis源码解析2 - 内存管理
Redis提供了多种内置数据结构,在运行过程中的大部分内存都是动态分配的,所以在介绍数据结构的实现之前我们先来看一下Redis是如何进行内存管理的,相关的代码在zmalloc.h和zmalloc.c文件中。
// zmalloc.h
void *zmalloc(size_t size); // 分配内存
void *zcalloc(size_t size); // 分配内存,并初始化为0
void *zrealloc(void *ptr, size_t size); // 重新分配
void zfree(void *ptr); // 释放
char *zstrdup(const char *s); // 字符串复制
size_t zmalloc_used_memory(void); // 获取已用内存
void zmalloc_set_oom_handler(void (*oom_handler)(size_t)); // 设置默认oom处理函数
size_t zmalloc_get_rss(void); // 获取Resident Set Size
int zmalloc_get_allocator_info(size_t *allocated, size_t *active, size_t *resident);
size_t zmalloc_get_private_dirty(long pid); // 获取private dirty信息
size_t zmalloc_get_smap_bytes_by_field(char *field, long pid); // 获取smaps信息
size_t zmalloc_get_memory_size(void); // 获取物理内存大小
void zlibc_free(void *ptr); // libc释放内存
size_t zmalloc_size(void *ptr);
size_t zmalloc_usable(void *ptr);
C语言程序开发者在编写高性能的服务端程序时常常需要解决内存分配问题,一种方法是用自己实现的内存池来解决内存碎片问题,提高内存分配效率,比如Nginx的内存池实现,Python官方版C语言实现。另一种方法是使用成熟的内存分配器比如tcmalloc(Google开发),jemalloc,nedmalloc等等。Redis采用了后一种方法,没有实现内存池,而是使用了内存分配器进行内存分配,减少了大量内存管理的代码。根据这篇文章中的测试,使用了内存分配器后服务端程序的性能得到了很大提升。
我们主要分析Linux系统相关的代码,Redis在Linux系统中是默认使用了jemalloc,在zmalloc.c文件开头是一些宏的定义,当HAVE_MALLOC_SIZE定义的时候把PREFIX_SIZE置为0,否则为long long,或者size_t的字节数,通常为8字节。这两个宏在介绍内存分配的时候会详细解释。之后通过宏把标准库的malloc函数都替换成jemalloc的函数。
// zmalloc.c
#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
// 替换标准库中的函数
#define malloc(size) je_malloc(size)
#define calloc(count,size) je_calloc(count,size)
#define realloc(ptr,size) je_realloc(ptr,size)
#define free(ptr) je_free(ptr)
#define mallocx(size,flags) je_mallocx(size,flags)
#define dallocx(ptr,flags) je_dallocx(ptr,flags)
接下来是两个用宏定义的函数 update_zmalloc_stat_alloc和update_zmalloc_stat_free
// zmalloc.c
#define update_zmalloc_stat_alloc(__n) do { \
size_t _n = (__n); \
if (_n&(sizeof(long)-1)) _n += sizeof(long)-(_n&(sizeof(long)-1)); \
atomicIncr(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)); \
atomicDecr(used_memory,__n); \
} while(0)
这里用到了两个技巧,8字节对齐和宏定义中的do-while(0)。读者可能会发现一个问题,size_t _n = (__n); 把变量__n(双下划线)赋值给_n(单下划线),然后 if (_n&(sizeof(long)-1)) _n += sizeof(long)-(_n&(sizeof(long)-1)); 对_n(单下划线)做了8字节补齐,但是最后并没有用到_n(单下划线)这个变量,传给atomicIncr 和 atomicDecr函数的是补齐前的值__n(双下划线)。
这是Redis代码中的一个小问题,可能也算不上是bug,只是几行遗留下来的多余的代码,不会影响程序运行,也不会对性能造成任何的影响,因为聪明的现代编译器会删除这几行代码。
// zmalloc.c
#define update_zmalloc_stat_alloc(__n) atomicIncr(used_memory,(__n))
#define update_zmalloc_stat_free(__n) atomicDecr(used_memory,(__n))
之后初始化了静态变量和互斥锁,定义了发生OOM内存耗尽时的处理函数。
// zmalloc.c
static size_t used_memory = 0;
pthread_mutex_t used_memory_mutex = PTHREAD_MUTEX_INITIALIZER;
static void zmalloc_default_oom(size_t size) {
fprintf(stderr, "zmalloc: Out of memory trying to allocate %zu bytes\n",
size); // 打印错误
fflush(stderr); // 清空stderr缓冲区
abort(); // 终止程序
}
static void (*zmalloc_oom_handler)(size_t) = zmalloc_default_oom;
接下来就是负责内存管理相关的函数了,包括zmalloc,zcalloc,zrealloc和zfree。
// zmalloc.c
void *zmalloc(size_t size) {
// 实际分配的内存大小为size+PREFIX_SIZE
void *ptr = malloc(size+PREFIX_SIZE);
// 处理OOM
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
}
// 版本1 使用分配器时
void *zmalloc(size_t size) {
// 实际分配的内存大小为size
void *ptr = malloc(size+0);
// 处理OOM
if (!ptr) zmalloc_oom_handler(size);
update_zmalloc_stat_alloc(zmalloc_size(ptr));
return ptr;
}
// 版本2 不使用分配器时
void *zmalloc(size_t size) {
/*
实际分配的内存大小为size+PREFIX_SIZE,需要额外的字节记录内存块的大小,通常需要8字节
内存示意图:
-------------------------------------
| PREFIX_SIZE | size |
-------------------------------------
| |
ptr 返回的指针
*/
void *ptr = malloc(size+PREFIX_SIZE);
// 处理OOM
if (!ptr) zmalloc_oom_handler(size);
// 记录内存块大小
*((size_t*)ptr) = size;
update_zmalloc_stat_alloc(size+PREFIX_SIZE);
return (char*)ptr+PREFIX_SIZE;
}
这里使用了HAVE_MALLOC_SIZE宏来实现根据条件编译,所以zmalloc函数根据不同的编译参数会有两个版本。当使用内存分配器(je_malloc等)的时候会定义HAVE_MALLOC_SIZE,相关代码在config.h文件中。版本1中PREFIX_SIZE为0,分配完成后更新内存统计数据,直接返回指针。版本2中PREFIX_SIZE为sizeof(long long)或者sizeof(size_t),实际分配内存大小是size+PREFIX_SIZE,其中包含了记录内存块大小的数据。如果使用了jemalloc,这里的malloc函数在预处理阶段会被替换为je_malloc,malloc_size函数会被替换为je_malloc_usable_size。其他的zcalloc,zrealloc和zfree的逻辑都是类似的,在这里就不重复了。