一、内存分配概述
redis 的内存分配,实质上是对 tcmalloc / jemalloc 的封装。内存分配本质就是给定需要分配的大小,以字节为单位,然后返回一个指向一段分配好的连续的内存空间的首指针。
通过这个首指针,我们需要知道它的连续空间的大小,才能进行内存统计,某些低版本的 tcmalloc / jemalloc 不支持通过给定指针获取它申请的内存块的大小,如果能够通过接口获得这个大小,那么我们就定义宏 HAVE_MALLOC_SIZE 为 1,并且定义 zmalloc_size 为相应的接口函数。实现在 zmalloc.h 中:
这段代码的核心是宏定义 zmalloc_size,如果是用 jemalloc,那么它就是 je_malloc_usable_size;如果是用 tcmalloc,那么它就是 tc_malloc_size;如果是在 mac 上编译,那么就是 malloc_size 。
从上面的宏定义可以看出:版本号小于1.6的 tcmalloc 以及 版本号小于2.1的 jemalloc, HAVE_MALLOC_SIZE 均为未定义(本文的末尾,会给出 HAVE_MALLOC_SIZE 未定义的情况下 zmalloc_size 的实现方式)。
二、内存管理模型
如果 HAVE_MALLOC_SIZE 未定义,那么就代表在申请内存空间的时候,需要额外申请一块空间来记录这个需要申请的空间的实际字节数(方便申请和释放的时候做内存统计),这个 “额外空间” 被放在申请空间的前面,它的字节数被定义为 PREFIX_SIZE,定义在 zmalloc.c 中:
__sun、__sparc、__sparc__的含义知不知道都无所谓,主要是对平台的判断。当 PREFIX_SIZE 不为0的时候,内存模型如下图所示:
三、内存分配
接下来看下内存分配的几个常用函数的宏替换,同样定义在 zmalloc.c 中:
经典的内存分配函数主要有3个:malloc(size)、calloc(count, size)、realloc(ptr, size)。
malloc(size):分配 size 个字节的内存空间,返回值为分配到的连续内存的首地址。分配的数据不做初始化。
calloc(count, size):分配 count * size 个字节的内存空间,返回值为分配到的连续内存的首地址。并且对分配后的数据进行初始化。
realloc(ptr, size) 从 ptr 地址开始重新分配 size 个字节的空间,可以比原本 ptr 指向的连续空间的长度小或者大。
1、zmalloc
接下来看下 redis 是如何对这几个内存分配函数进行封装的,首先是 zmalloc (size_t size),实现在 zmalloc.c 中: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
} 代码比较简短,其中 malloc 用来分配内存,长度为 size+PREFIX_SIZE,返回指针 ptr。如果得到的 ptr 为空,则表明内存分配失败,一般是内存溢出了,直接调用内存溢出处理函数 zmalloc_oom_handler 进行处理 (oom 即 out of memory),zmalloc_oom_handler 是个函数指针,有个默认处理函数 zmalloc_default_oom,当然也可以通过 zmalloc_set_oom_handler (void (*oom_handler)(size_t)) 对默认处理函数进行替换。
如果 HAVE_MALLOC_SIZE 未定义,则在 ptr 指向的位置的首地址上将 size 记录下来,并且实际返回的地址是偏移了 PREFIX_SIZE 个字节的,即 (char*)ptr+PREFIX_SIZE 。因为对用户来说,这个 size 是它不需要关心的,它只关心申请到的内存。
然后我们发现这里做了一步操作,就是 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)); \ /* 1 */
if (zmalloc_thread_safe) { \ /* 2 */
update_zmalloc_stat_add(_n); \ /* 3 */
} else { \
used_memory += _n; \ /* 4 */
} \
} while(0) 1、这段代码比较有意思,首先我们看 sizeof(long),在32位机器下它的值是4,64位机器下值为8。也就是无论如何都是2的幂,那么 (_n&(sizeof(long)-1)) 的含义就是 (_n % sizeof(long) != 0),这句话的意思就是将 _n 向上补齐为 sizeof(long) 的倍数。因为 malloc 在分配内存的时候已经做了内存对齐(一定是 sizeof(long) 的倍数),所以补齐后的 _n 才是真正的申请出来的内存大小。
2、zmalloc_thread_safe 标记是否线程安全,通过 zmalloc_enable_thread_safeness(void) 函数来开启。
3、update_zmalloc_stat_add 是个宏定义,即线程安全版的 used_memory += _n。
4、used_memory 是一个静态变量,用于记录一共分配了多少个字节的内存。
2、zcalloc
zcalloc (size_t size) 的实现和 zmalloc (size_t size) 类似,malloc() 和 calloc() 的主要区别是前者不能初始化所分配的内存空间,而后者能。如果由 malloc() 函数分配的内存空间原来没有被使用过,则其中的每一位可能都是0;反之,如果这部分内存曾经被分配过,则其中可能遗留有各种各样的数据。zcalloc (size_t size) 的实现如下:void *zcalloc(size_t size) {
void *ptr = calloc(1, 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 (size_t size) 实现一模一样,不再累述。
3、zrealloc
接下来讲下 zrealloc(void *ptr, size_t size),重分配函数实现如下:void *zrealloc(void *ptr, size_t size) {
#ifndef HAVE_MALLOC_SIZE
void *realptr;
#endif
size_t oldsize;
void *newptr;
if (ptr == NULL) return zmalloc(size);
#ifdef HAVE_MALLOC_SIZE
oldsize = zmalloc_size(ptr);
newptr = realloc(ptr,size);
if (!newptr) zmalloc_oom_handler(size);
update_zmalloc_stat_free(oldsize);
update_zmalloc_stat_alloc(zmalloc_size(newptr));
return newptr;
#else
realptr = (char*)ptr-PREFIX_SIZE; /* 1 */
oldsize = *((size_t*)realptr); /* 2 */
newptr = realloc(realptr,size+PREFIX_SIZE); /* 3 */
if (!newptr) zmalloc_oom_handler(size); /* 4 */
*((size_t*)newptr) = size; /* 5 */
update_zmalloc_stat_free(oldsize); /* 6 */
update_zmalloc_stat_alloc(size);
return (char*)newptr+PREFIX_SIZE;
#endif
} HAVE_MALLOC_SIZE 在定义和未定义的情况下分别处理,这里只介绍 HAVE_MALLOC_SIZE 未定义的情况(定义的情况相对较简单):
1、将当前指针 ptr 向前偏移 PREFIX_SIZE 个字节,得到真正内存分配的起始地址 realptr;
2、取 realptr 位置上的值作为该连续内存块的大小,并且记录在 oldsize 中;
3、realloc 在 realptr 的位置分配 size+PREFIX_SIZE 的空间,返回 newptr。size 的值有可能比 oldsize 大或小,newptr 和 ptr 的值可能相同也可能不同,这个完全取决于 realloc 的实现。
4、如若内存分配失败,调用 out of memory 进行处理。
5、将 size 记录在 newptr 指向的位置上。
6、update_zmalloc_stat_free 的作用和 update_zmalloc_stat_alloc 正好相反,都是操作 use_memory 这个静态变量的。free 是减, alloc 是加。
update_zmalloc_stat_free 的实现参考 update_zmalloc_stat_alloc,如下:#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) 四、内存释放
1、zfree
有内存的分配,自然就有释放,内存释放的实现 zfree(void *ptr),定义在 zmalloc.c 中: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
} 在 HAVE_MALLOC_SIZE 未定义的情况下,实际分配的内存空间的指针位置需要向前偏移 PREFIX_SIZE 个字节,并且指针首地址内存的就是这次分配的内存空间的大小。调用 update_zmalloc_stat_free 更新 used_memory 的值后就可以调用 free(void *ptr) 进行内存释放了。
五、其它
1、zmalloc_size
到现在为止,我们已经基本了解了 redis 的内存管理的实现。再回到文章开头,只有当 HAVE_MALLOC_SIZE 被定义的情况下,才能获取到 zmalloc_size,那么如果系统没有提供 zmalloc_size 函数的实现,我们要如何获取当前指针的实际内存空间呢?
在 HAVE_MALLOC_SIZE 未定义的情况下,zmalloc_size 实现如下:#ifndef HAVE_MALLOC_SIZE
size_t zmalloc_size(void *ptr) {
void *realptr = (char*)ptr-PREFIX_SIZE;
size_t size = *((size_t*)realptr);
if (size&(sizeof(long)-1)) size += sizeof(long)-(size&(sizeof(long)-1));
return size+PREFIX_SIZE;
}
#endif 首先 ptr 指针向前偏移 PREFIX_SIZE 个字节获取到实际申请内存空间的起始地址 realptr, 从而得到这次分配的大小 size,再将 size 向上转成 sizeof(long) 的倍数。这样实际消耗内存大小就是 size + PREFIX_SIZE 了。
2、used_memory
最后,静态变量 used_memory,我们希望在外部获取使用的内存大小,直接如下做法是有欠考虑的:size_t zmalloc_used_memory(void) {
return used_memory;
} 原因是 used_memory 属于共享资源,而 return 不是一个原子操作,我们需要考虑多线程的情况,正确实现如下:size_t zmalloc_used_memory(void) {
size_t um;
if (zmalloc_thread_safe) {
#if defined(__ATOMIC_RELAXED) || defined(HAVE_ATOMIC)
um = update_zmalloc_stat_add(0);
#else
pthread_mutex_lock(&used_memory_mutex);
um = used_memory;
pthread_mutex_unlock(&used_memory_mutex);
#endif
}
else {
um = used_memory;
}
return um;
} update_zmalloc_stat_add 之前提到过的 “原子加” 操作,pthread_mutex_lock 则是互斥锁,在 线程安全标记 zmalloc_thread_safe 为 1 的时候,需要进行原子操作将 used_memory 的值赋值给局部变量 um,然后再返回。
|