评论

收藏

[NoSQL] Redis底层详解(三) 内存管理

数据库 数据库 发布于:2021-07-08 12:07 | 阅读数:364 | 评论:0

   DSC0000.png
一、内存分配概述
  redis 的内存分配,实质上是对 tcmalloc / jemalloc 的封装。内存分配本质就是给定需要分配的大小,以字节为单位,然后返回一个指向一段分配好的连续的内存空间的首指针。
        通过这个首指针,我们需要知道它的连续空间的大小,才能进行内存统计,某些低版本的 tcmalloc / jemalloc 不支持通过给定指针获取它申请的内存块的大小,如果能够通过接口获得这个大小,那么我们就定义宏 HAVE_MALLOC_SIZE 为 1,并且定义 zmalloc_size 为相应的接口函数。实现在 zmalloc.h 中:
DSC0001.png

  这段代码的核心是宏定义 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 中:
DSC0002.png

  __sun、__sparc、__sparc__的含义知不知道都无所谓,主要是对平台的判断。当 PREFIX_SIZE 不为0的时候,内存模型如下图所示:
DSC0003.png

三、内存分配
  接下来看下内存分配的几个常用函数的宏替换,同样定义在 zmalloc.c 中:
DSC0004.png

  经典的内存分配函数主要有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,然后再返回。

  
关注下面的标签,发现更多相似文章