当前位置 博文首页 > 英雄哪里出来:Redis底层详解(三) 内存管理

    英雄哪里出来:Redis底层详解(三) 内存管理

    作者:[db:作者] 时间:2021-09-03 21:59

    一、内存分配概述

    ? ? ? ? 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,然后再返回。

    cs