redis源码阅读——内存分配完全解析

875 阅读6分钟

简介

zmalloc.czmalloc.h主要功能就是对原有库里的内存分配函数进行封装,形成独立的一套内存管理函数。由于redis要求满足跨平台性,而每个平台又会有自己的内存管理函数,所以在这两个文件中,将会看到大量的#ifdef,根据系统的不同,使用不同的内存管理函数(例如jemalloc,tcmalloc,cmalloc),而封装接口都是一致的--zmalloc

通过学习这一块代码,收获如下:

  • 学习reids如何通过条件编译以及宏定义封装几个内存分配库
  • 有大量判断平台的条件编译代码,可以借鉴学习
  • 回顾一些内存分配的c语言代码,例如malloc、calloc、realloc
  • 在不同平台下如何获取进程RSS,以及物理内存大小

源码分析

下面这一段zmalloc.h中的代码是对tcmalloc、jemalloc、苹果系统、其他情况,共四种情况下的封装,通过不同的宏判断include不同的库:

// TCMALLOC
#if defined(USE_TCMALLOC)
#define ZMALLOC_LIB ("tcmalloc-" __xstr(TC_VERSION_MAJOR) "." __xstr(TC_VERSION_MINOR))
#include <google/tcmalloc.h>
#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

// JEMALLOC
#elif defined(USE_JEMALLOC)
#define ZMALLOC_LIB ("jemalloc-" __xstr(JEMALLOC_VERSION_MAJOR) "." __xstr(JEMALLOC_VERSION_MINOR) "." __xstr(JEMALLOC_VERSION_BUGFIX))
#include <jemalloc/jemalloc.h>
#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 <malloc/malloc.h>
#define HAVE_MALLOC_SIZE 1
#define zmalloc_size(p) malloc_size(p)
#endif

// 其他情况
#ifndef ZMALLOC_LIB
#define ZMALLOC_LIB "libc"
#if !defined(NO_MALLOC_USABLE_SIZE) && \
    (defined(__GLIBC__) || defined(__FreeBSD__) || \
     defined(USE_MALLOC_USABLE_SIZE))
#include <malloc.h>
#define HAVE_MALLOC_SIZE 1
#define zmalloc_size(p) malloc_usable_size(p) // zmalloc_size()获取内存长度
#endif
#endif

对一些宏做个解释:

  • ZMALLOC_LIB:最终使用的内存库,可以是tcmalloc-版本号jemalloc-版本号libc三个
  • HAVE_MALLOC_SIZE: 使用的内存分配库是否有自带的获取内存块大小的函数,jemalloc和tcmalloc都有
  • HAVE_DEFRAG: 是否支持内存碎片处理,如果redis使用的事支持内存碎片处理的JEMALLOC版本,这个宏就为1

关于内存碎片整理

zmalloc.h中,HAVE_DEFRAG这个宏跟内存碎片整理有关,相关代码如下:

#if defined(USE_JEMALLOC) && defined(JEMALLOC_FRAG_HINT)
#define HAVE_DEFRAG
#endif
...
#ifdef HAVE_DEFRAG
void zfree_no_tcache(void *ptr);
void *zmalloc_no_tcache(size_t size);
#endif

JEMALLOC_FRAG_HINT 变量的定义在Jemalloc的依赖文件 jemalloc_macros.h.in 中,用于标识当前版本Jemalloc支持碎片整理。标准的Jemalloc内存分配器中是不包含这个变量的,Redis使用的是经过修改的Jemalloc版本。

在满足使用Jemalloc并且所使用版本支持内存碎片整理时,会编译这两个函数:

  • void zfree_no_tcache(void *ptr);
  • void *zmalloc_no_tcache(size_t size);

这两个函数分别用于内存的分配和释放,在实现上区别于常规的分配和释放函数zmalloc/zfree()。以 zmalloc_no_tcache() 为例,内部通过调用je_mallocx()函数来分配内存;je_mallocx()会绕过线程缓存,直接分配内存块,这是在自动内存碎片整理时所要使用到的函数。

什么是线程缓存?

在jemalloc中,每个线程有各自的缓存,分配内存的时候,从各自的缓存中分配内存就能避免频繁加锁操作全局内存块中拿内存,提高效率

关于获取内存块大小

看这一段代码:

#ifndef HAVE_MALLOC_SIZE
size_t zmalloc_size(void *ptr); // zmalloc_size()获取内存长度,不使用第三方库就要自己定义
size_t zmalloc_usable_size(void *ptr);// return zmalloc_size(ptr)-PREFIX_SIZE; 获取减去前缀的size
#else
#define zmalloc_usable_size(p) zmalloc_size(p)
#endif

HAVE_MALLOC_SIZE宏表示使用的库有获取指针指向内存块大小的函数:

  • Jemaloocje_malloc_usable_size(p)
  • tcmalloctc_malloc_size(p)
  • 苹果系统有malloc_size(p)
  • linux下有malloc_usable_size(p) 如果强制关闭使用上述库,也就是打开宏NO_MALLOC_USABLE_SIZE的话,HAVE_MALLOC_SIZE就为0,需要编译这两个函数
  • size_t zmalloc_size(void *ptr) :获取内存长度(带前缀)
  • size_t zmalloc_usable_size(void *ptr):获取内存长度(不带前缀)

如果不使用第三方内存库,每次malloc出来的内存有一个前缀来标识内存长度(使用第三方库泽没有)。注意zmalloc()函数返回的指针是下图中ptr位置

image.png

基础函数封装

#if defined(USE_TCMALLOC)
#define malloc(size) tc_malloc(size)
#define calloc(count,size) tc_calloc(count,size)
#define realloc(ptr,size) tc_realloc(ptr,size)
#define free(ptr) tc_free(ptr)
#elif defined(USE_JEMALLOC)
#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)
#endif


可以看到,如果使用了第三方库,直接写宏定义用对应库提供的函数替换掉基础函数,这里对这些函数做一个介绍:

  • malloc(size): 普通分配内存
  • free(ptr): 普通释放内存
  • calloc(count,size): 分配count*size的内存空间,往往用于数组,calloc 会设置分配的内存为零
  • realloc(ptr,size): 调整ptr指向的内存大小为size
  • mallocx(size,flags): 上文提过,jemalloc指定,包装为je_mallocx
  • dallocx(ptr,flags):上文提过,jemalloc指定,包装为je_dallocx

记录内存大小

#define update_zmalloc_stat_alloc(__n) atomicIncr(used_memory,(__n))
#define update_zmalloc_stat_free(__n) atomicDecr(used_memory,(__n))
static redisAtomic size_t used_memory = 0;

redis用一个静态原子变量used_memory来记录已使用的内存大小。update_zmalloc_stat_alloc(n)update_zmalloc_stat_free(n)式用来增减used_memory的宏。每次malloc和free的相关操作后都要调用一下

至于原子变量的实现,后面写一篇文章单独说

函数/接口详解

介绍几个主要的函数和接口

ztrymalloc_usable(size_t size, size_t *usable)

分配内存,如果失败返回NULL,成功返回分配内存的指针,并把usable设为size大小

void *ztrymalloc_usable(size_t size, size_t *usable) {
    ASSERT_NO_SIZE_OVERFLOW(size);
    void *ptr = malloc(MALLOC_MIN_SIZE(size)+PREFIX_SIZE);

    if (!ptr) return NULL;
#ifdef HAVE_MALLOC_SIZE
    size = zmalloc_size(ptr); // 调用malloc_size(),获取实际分配的大小
    update_zmalloc_stat_alloc(size); //更新已使用内存大小,加上size
    if (usable) *usable = size;
    return ptr;
#else
    *((size_t*)ptr) = size;
    update_zmalloc_stat_alloc(size+PREFIX_SIZE);
    if (usable) *usable = size;
    return (char*)ptr+PREFIX_SIZE; //指针偏移
#endif
}

首先调用malloc()分配大小为size+PREFIX_SIZE的内存,然后进入条件编译,如果没有第三方库的帮助,内存前面有记录内存块大小的前缀,返回的指针需要向后偏移PREFIX_SIZE,但是总共使用内存要加上PREFIX_SIZE

ztryrealloc_usable(void *ptr, size_t size, size_t *usable)

为整指针ptr指向内存块的大小为size, 如果成功,*usable = size

函数中的条件编译和上面一样是区分是否使用第三方库,没有使用的话返回指针时要考虑内存偏移,记录内存时需要考虑内存前缀大小

void *ztryrealloc_usable(void *ptr, size_t size, size_t *usable) {
    ASSERT_NO_SIZE_OVERFLOW(size);
#ifndef HAVE_MALLOC_SIZE
    void *realptr;
#endif
    size_t oldsize;
    void *newptr;

    /* not allocating anything, just redirect to free. */
    if (size == 0 && ptr != NULL) {
        zfree(ptr);
        if (usable) *usable = 0;
        return NULL;
    }
    /* Not freeing anything, just redirect to malloc. */
    if (ptr == NULL)
        return ztrymalloc_usable(size, usable);

#ifdef HAVE_MALLOC_SIZE
    oldsize = zmalloc_size(ptr);
    newptr = realloc(ptr,size);
    if (newptr == NULL) {
        if (usable) *usable = 0;
        return NULL;
    }

    update_zmalloc_stat_free(oldsize);
    size = zmalloc_size(newptr);
    update_zmalloc_stat_alloc(size);
    if (usable) *usable = size;
    return newptr;
#else
    realptr = (char*)ptr-PREFIX_SIZE;
    oldsize = *((size_t*)realptr);
    newptr = realloc(realptr,size+PREFIX_SIZE);
    if (newptr == NULL) {
        if (usable) *usable = 0;
        return NULL;
    }

    *((size_t*)newptr) = size;
    update_zmalloc_stat_free(oldsize);
    update_zmalloc_stat_alloc(size);
    if (usable) *usable = size;
    return (char*)newptr+PREFIX_SIZE;
#endif
}

zmalloc_get_rss(void) 获取RSS

该函数用来获取RSS

RSS是Resident Set Size(常驻内存大小)的缩写,用于表示进程使用了多少内存(RAM中的物理内存),RSS不包含已经被换出的内存。RSS包含了它所链接的动态库并且被加载到物理内存中的内存。RSS还包含栈内存和堆内存。

这里有许多条件编译,针对每个平台,获取RSS的方法不一样,例如:

  • linux平台,通过读文件proc/pid/stat获取页数,并乘以页面大小_SC_PAGESIZE
  • 苹果系统,通过task_info()函数 还有一些其他平台,就不再说了

zmalloc_get_memory_size(void)

该函数返回系统物理内存的大小(单位:字节)

未来满足跨平台,这里也有一堆条件编译,对于Linux,直接读页数以及页大小, 相乘即可:

size_t zmalloc_get_memory_size(void) {
    return (size_t)sysconf(_SC_PHYS_PAGES) * (size_t)sysconf(_SC_PAGESIZE);
}

sysconf 函数用来获取系统执行的配置信息。例如页大小、最大页数、cpu个数、打开句柄的最大个数等等。

#include <unistd.h>

long sysconf(int name);

常用的name参数有:

  • _SC_CHILD_MAX:每个user可同时运行的最大进程数
  • _SC_HOST_NAME_MAX:hostname最大长度,需小于_POSIX_HOST_NAME_MAX (255)
  • _SC_OPEN_MAX:一个进程可同时打开的文件最大数
  • _SC_PAGESIZE:一个page的大小,单位byte
  • _SC_PHYS_PAGES:物理内存总page数
  • _SC_AVPHYS_PAGES:当前可获得的物理内存page数
  • _SC_NPROCESSORS_CONF:配置的处理器个数
  • _SC_NPROCESSORS_ONLN:当前可获得的处理器个数
  • _SC_CLK_TCK:每秒对应的时钟tick数