redis6.2源码解析-内存管理

1,153 阅读13分钟

redis 的内存管理主要由zmalloc.h和zmalloc.c来实现的, 他主要的作用是提供统一的内存管理方法, 屏蔽底层不同系统不同分配器的差异.

1 C语言相关基础

  • 宏定义中#s 表示将s符号变成字符串
  • 宏定义中 a##b 用于链接两个符号变成一个
  • *(&a)=2  相当于给指针a的变量赋值为2, * 不止可以读取指针变量的值, 还可以赋值
  • sdtlib.h/abort(void) 中止程序执行,直接从调用的地方跳出
  • fprintf() 发送格式化输出到流 stream 中
  • fflush() 刷新输出流

2 分配器的介绍

  • tcmalloc: 由google用于优化C++多线程就应用而开发
  • jemalloc: 一个通用的malloc(3)实现, 着重于减少内存碎片和提高并发性能
  • 苹果系统自带的 malloc
  • 其他系统或glibc
  • 标准的libc

3 关于内存对齐

这里我引用别人文章的一段话来介绍一下内存对齐的概念

  CPU一次性能读取数据的二进制位数称为字长,也就是我们通常所说的32位系统(字长4个字节)、64位系统(字长8个字节)的由来。  所谓的8字节对齐,就是指变量的起始地址是8的倍数。  比如程序运行时(CPU)在读取long型数据的时候,只需要一个总线周期,时间更短,  如果不是8字节对齐的则需要两个总线周期才能读完数据。  本文中我提到的8字节对齐是针对64位系统而言的,如果是32位系统那么就是4字节对齐。  实际上Redis源码中的字节对齐是软编码,而非硬编码。  里面多用sizeof(long)或sizeof(size_t)来表示。  size_t(gcc中其值为long unsigned int)和long的长度是一样的,  long的长度就是计算机的字长。  这样在未来的系统中如果字长(long的大小)不是8个字节了,该段代码依然能保证相应代码可用。

redis3.0的时候, 在内存统计和自定义实现malloc_size时都会进行手动内存对齐, redis 6.0 的版本只在进行统计的内存时, 会尝试进行手动内存对齐的, 但是redis 6.2后, 把所有手动内存对齐的代码删除了, 这里还没找到原因. 为什么越后面的版本越不需要手动内存对齐?

4 内存模型

image.png 首部: PREFIX_SIZE 目标分配内存大小: 也就是入参的SIZE 填充字节数(对齐)是由malloc自动完成的

image.png

redis 分配的内存主要由三部分组成, 分别是首部PREFIX_SIZE, 目标内存SIZE, 和对齐填充字节, 底层malloc返回的是 realptr 指针, redis 上层使用的指向数据的指针ptr.

5 zmalloc.h源码解析

zmalloc.h的主要逻辑是声明一些通用的方法, 并且根据底层不同分配器的实现, 申明相关宏定义参数, 如: HAVE_MALLOC_SIZE, HAVE_DEFRAG redis6.2可选的分配器有 tcmalloc/jemalloc/apple malloc/存在 malloc_usable_size方法的 libc/原生的libc, 按我看的理解, 最终都不满足的话, 最后应该用ANSI libc 来处理, 并且需要手动实现zmalloc_size()方法和zmalloc_usable_size()方法.


//一般C文件都是在声明一个文件标识, 用于避免文件重复引用
#ifndef __ZMALLOC_H
#define __ZMALLOC_H

/* Double expansion needed for stringification of macro values. */
#define __xstr(s) __str(s)
//将s变成字符串
#define __str(s) #s
//分别判断使用tcmalloc库/jemalloc库/苹果库哪个作为底层的malloc函数调用
#if defined(USE_TCMALLOC)
//拼接 ZMALLOC_LIB 字符串
#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)
//定义 HAVE_MALLOC_SIZE
#define HAVE_MALLOC_SIZE 1
//定义获取指针对应的内存大小
#define zmalloc_size(p) tc_malloc_size(p)
#else
#error "Newer version of tcmalloc required"
#endif

#elif defined(USE_JEMALLOC)
//拼接ZMALLOC_LIB字符串
#define ZMALLOC_LIB ("jemalloc-" __xstr(JEMALLOC_VERSION_MAJOR) "." __xstr(JEMALLOC_VERSION_MINOR) "." __xstr(JEMALLOC_VERSION_BUGFIX))
//引 jemalloc 的库
#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

//mac的库
#elif defined(__APPLE__)
#include <malloc/malloc.h>
//是否存在获取已分配内存大小的方法
#define HAVE_MALLOC_SIZE 1
//获取指针对象分配内存的大小, 为什么需要这个方法呢, zmalloc_size 会返回不包括内存大小头(PREFIX_SIZE)的内存大小
#define zmalloc_size(p) malloc_size(p)
#endif

/* On native libc implementations, we should still do our best to provide a
 * HAVE_MALLOC_SIZE capability. This can be set explicitly as well:
 *
 * NO_MALLOC_USABLE_SIZE disables it on all platforms, even if they are
 *      known to support it.
 * USE_MALLOC_USABLE_SIZE forces use of malloc_usable_size() regardless
 *      of platform.
 */
//没有声明内存分配的库
#ifndef ZMALLOC_LIB
//定义ZMALLOC_LIB为"libc"
#define ZMALLOC_LIB "libc"
//是 glibc 或者 freeBSD系统 或 如果存在 malloc_usable_size() 方法
//malloc_usable_size 函数中传入一个指针,返回指针指向的空间实际占用的大小,
//这个返回的大小,可能会比使用malloc申请的要大,由于系统的内存对齐或者最小分配限制
#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)
#endif
#endif

/* We can enable the Redis defrag capabilities only if we are using Jemalloc
 * and the version used is our special version modified for Redis having
 * the ability to return per-allocation fragmentation hints. */
#if defined(USE_JEMALLOC) && defined(JEMALLOC_FRAG_HINT)
//定义是否支持内存碎片整理
#define HAVE_DEFRAG
#endif

//申请大小为size的内存空间, 不进行初始化, 有可能有脏数据
void *zmalloc(size_t size);
//以块的形式申请内存, 默认是1块, 对应 calloc, 并初始化为0
void *zcalloc(size_t size);
//重新调用已申请的内存大小为size
void *zrealloc(void *ptr, size_t size);
//尝试用 malloc 申请内存
void *ztrymalloc(size_t size);
//尝试用 calloc 申请内存
void *ztrycalloc(size_t size);
void *ztryrealloc(void *ptr, size_t size);
//释放内存
void zfree(void *ptr);
void *zmalloc_usable(size_t size, size_t *usable);
void *zcalloc_usable(size_t size, size_t *usable);
void *zrealloc_usable(void *ptr, size_t size, size_t *usable);
void *ztrymalloc_usable(size_t size, size_t *usable);
void *ztrycalloc_usable(size_t size, size_t *usable);
void *ztryrealloc_usable(void *ptr, size_t size, size_t *usable);
void zfree_usable(void *ptr, size_t *usable);
//字符串复制
char *zstrdup(const char *s);
//获取redis已经使用(分配)的内存大小
size_t zmalloc_used_memory(void);
//自定义内存溢出时回调函数
void zmalloc_set_oom_handler(void (*oom_handler)(size_t));
//获取RSS(常驻内存集)大小
size_t zmalloc_get_rss(void);
int zmalloc_get_allocator_info(size_t *allocated, size_t *active, size_t *resident);
void set_jemalloc_bg_thread(int enable);
int jemalloc_purge();
//获取进程私有的内容已经发生更改的内存大小
size_t zmalloc_get_private_dirty(long pid);
size_t zmalloc_get_smap_bytes_by_field(char *field, long pid);
//获取物理内存大小
size_t zmalloc_get_memory_size(void);
//直接调用系统free函数释放已分配的内存
void zlibc_free(void *ptr);

//如果开启内存碎片整理
#ifdef HAVE_DEFRAG
void zfree_no_tcache(void *ptr);
void *zmalloc_no_tcache(size_t size);
#endif

//没有获取已分配内存大小的方法, 则声明两个函数, 给 zmalloc.c 进行手动实现, 这里有点像java的抽象方法
#ifndef HAVE_MALLOC_SIZE
size_t zmalloc_size(void *ptr);
size_t zmalloc_usable_size(void *ptr);
#else
//将 zmalloc_size 方法重定义为 zmalloc_usable_size, 用于获取指针对象大小
#define zmalloc_usable_size(p) zmalloc_size(p)
#endif

#ifdef REDIS_TEST
int zmalloc_test(int argc, char **argv);
#endif

#endif /* __ZMALLOC_H */

由上述方法声明可以我们可以猜到, redis内存分配方法分两种

  • 直接申请, 申请不了则报内存溢出, 如: zmalloc(), zcalloc(), zrealloc()
  • 尝试申请, 申请不了则返回NULL, 如: ztrymalloc(), ztrycalloc(), ztryrealloc()

6 zmalloc.c 的源码解析

zmalloc.c主要是对zmalloc.h的函数声明的实现, zmalloc.h有点像java的接口类.

redis分配内存时, 会根据宏定义变量 HAVE_MALLOC_SIZE 是否存在来处理 PREFIX_SIZE 的值, 如果 HAVE_MALLOC_SIZE 存在, 则可以通过 zmalloc_size() 函数来获取分配内存的大小, 也就是底层的分配器已经维护好指针已分配内存的大小, 那么PREFIX_SIZE就会设置成0, 如果HAVE_MALLOC_SIZE不存在, 则分配内存的时候需要加上一个 PREFIX_SIZE 的大小, 并且将申请的size写到内存首部的位置, 最后返回内存首部的偏移地址作用指针给上层使用.

6.1 维护 PREFIX_SIZE 变量的代码

//如果有定义 HAVE_MALLOC_SIZE 变量
#ifdef HAVE_MALLOC_SIZE
//PREFIX_SIZE 用于保存指针对象的内存长度, 第三方内存分配器已经存了内存长度, 所以为 0
#define PREFIX_SIZE (0)
//定义 ASSERT_NO_SIZE_OVERFLOW 为空方法
#define ASSERT_NO_SIZE_OVERFLOW(sz)
#else
//没有 HAVE_MALLOC_SIZE, 则定义保存内存大小的字节
#if defined(__sun) || defined(__sparc) || defined(__sparc__)
#define PREFIX_SIZE (sizeof(long long))
#else
#define PREFIX_SIZE (sizeof(size_t))
#endif
//定义ASSERT_NO_SIZE_OVERFLOW方法实, sz表示申请内存的大小, 断言 申请内存量+内存大小PREFIX_SIZE 不会溢出
#define ASSERT_NO_SIZE_OVERFLOW(sz) assert((sz) + PREFIX_SIZE > (sz))
#endif

6.2 内存分配malloc方法

zmalloc 和 zcalloc 的代码流程基本一样, 只是他们的底层调用不一样, zmalloc 调用的是 malloc 方法, zcalloc 底层调用的是 calloc 方法. tips : 理论上, 如果看懂了内存分配方法的流程, 其他内存分配的方法都很容易看懂的

//分配指定大小的内存, 没有分配成功, 则调用oom处理器
/* Allocate memory or panic */
void *zmalloc(size_t size) {
    void *ptr = ztrymalloc_usable(size, NULL);
    if (!ptr) zmalloc_oom_handler(size);
    return ptr;
}

//内存溢出的函数指针
static void (*zmalloc_oom_handler)(size_t) = zmalloc_default_oom;

//内存分配默认的OOM错误处理, 打印错误日志, 并且退出程序
static void zmalloc_default_oom(size_t size) {
    //打印内存异常日志
    fprintf(stderr, "zmalloc: Out of memory trying to allocate %zu bytes\n",
        size);
    fflush(stderr);
    //退出程序
    abort();
}

//尝试分配内存, 分配不了则返回 NULL
/* Try allocating memory, and return NULL if failed.
 * '*usable' is set to the usable size if non NULL. */
void *ztrymalloc_usable(size_t size, size_t *usable) {
    //判断size是否溢出
    ASSERT_NO_SIZE_OVERFLOW(size);
    //分配内存
    void *ptr = malloc(MALLOC_MIN_SIZE(size)+PREFIX_SIZE);

    //没分配到, 则返回 NULL
    if (!ptr) return NULL;
// 有获取分配内存大小的方法
#ifdef HAVE_MALLOC_SIZE
    //获取指针分配内存的大小
    size = zmalloc_size(ptr);
    //更新内存统计
    update_zmalloc_stat_alloc(size);
    //如果有指定 usable 指针, 则设置
    if (usable) *usable = size;
    //返回分配的指针
    return ptr;
#else
    //保存数据所需分配内存的实际大小, 这里有点秀, int a = 1; *(&a)=2; 相当于给a赋值为2
    //这里相当于设置 PREFIX_SIZE 这段位置为 size
    *((size_t*)ptr) = size;
    //更新统计数据
    update_zmalloc_stat_alloc(size+PREFIX_SIZE);
    //设置 usable
    if (usable) *usable = size;
    //计算出真正的指针, 也就是跳过PREFIX_SIZE大小后的内存首地址
    return (char*)ptr+PREFIX_SIZE;
#endif
}

//增加内存统计, 原子增加
#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;


  • 内存分配zmalloc()方法首先调用ztrymalloc_usable()方法进行内存分配, 如果分配成功则返回指针, 否则返回NULL. zmalloc()函数默认调用ztrymalloc_usable()进行内存分配, 如果分配成功则返回内存指针, 否则返回NULL.
  • 如果分配失败, 返回的指针为NULL, 则表示内存溢出, 默认的内存溢出处理函数是先打印错误日志, 再中断程序.
  • ztrymalloc_usable() 方法主要作用是计算实际要分配的内存(PREFIX_SIZE + SIZE)进行分配, 然后增加内存统计. 这里会根据是否声明 HAVE_MALLOC_SIZE 变量来决定是否调用 zmalloc_size() 方法来获取内存大小, 如果有声明 HAVE_MALLOC_SIZE, 则直接将更新内存统计并且将可用内存大小返回, 如果没声明 HAVE_MALLOC_SIZE , 则要手工设置 PREFIX_SIZE 的值, 再更新内存统计数据, 最后返回真正的对象指针.

6.3 内存重分配 zrealloc 方法

//内存重分配方法, 分配不成功则报内存溢出
/* Reallocate memory and zero it or panic */
void *zrealloc(void *ptr, size_t size) {
    //调用 ztryrealloc_usable() 方法进行重分配
    ptr = ztryrealloc_usable(ptr, size, NULL);
    //如果指针不存在且要分配的内存大于0, 则报内存溢出
    if (!ptr && size != 0) zmalloc_oom_handler(size);
    //返回重分配后的指针
    return ptr;
}

//尝试重分配内存
/* Try reallocating memory, and return NULL if failed.
 * '*usable' is set to the usable size if non NULL. */
void *ztryrealloc_usable(void *ptr, size_t size, size_t *usable) {
    //断言size + prefix_size 不溢出
    ASSERT_NO_SIZE_OVERFLOW(size);
//如果存在获取内存大小的方法
#ifndef HAVE_MALLOC_SIZE
    //旧的指针的原始指针(包括PREFIX_SIZE)
    void *realptr;
#endif
    //旧指针的内存大小
    size_t oldsize;
    //新的指针
    void *newptr;

    //如果指针不为空且要分配的内存长度为0, 则相当于释放内存
    /* not allocating anything, just redirect to free. */
    if (size == 0 && ptr != NULL) {
        //释放内存
        zfree(ptr);
        //设置可使用内存为0, 并且返回 NULL
        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);
    //没有分配到, 直接返回 NULL
    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;
    //获取 PREFIX_SIZE 的值, 也就是内存的大小
    oldsize = *((size_t*)realptr);
    //重新分配
    newptr = realloc(realptr,size+PREFIX_SIZE);
    //没有分配成功, 则返回 NULL
    if (newptr == NULL) {
        if (usable) *usable = 0;
        return NULL;
    }

    //设置 PREFIX_SIZE 的值
    *((size_t*)newptr) = size;
    //注意: 重分配内存时, 更新内存统计是没有操作PREFIX_SIZE的, 因为PREFIX_SIZE是没有变化的, 第一次内存分配时已经将PREFIX_SIZE纳入了
    //所以, 这里只要重新申请的size就行了
    //减少旧内存的统计
    update_zmalloc_stat_free(oldsize);
    //更新新的内存
    update_zmalloc_stat_alloc(size);
    //设置可用内存大小
    if (usable) *usable = size;
    //计算出可用的指针
    return (char*)newptr+PREFIX_SIZE;
#endif
}
  • zrealloc()方法通过调用ztryrealloc_usable()方法进行重分配内存, 如果返回的指针为空且要分配的内存不为0, 则报内存溢出
  • ztryrealloc_usable()方法的作用是尝试重分配内存并且返回分配后的可用内存. 这方法首先处理两种极端情况, 一是当要分配的内存size为0且原来内存指针不为空, 则相当于释放内存, 二是当原指针为空, 则直接根据size分配内存. 也就是根据参数不同, zrealloc()方法可以当作zmalloc()和zfree()使用.
  • 极端情况处理完后, 就开始处理正常的内存重分配.
  • 正常的内存重分配也分两种情况处理, 存在获取已分配内存大小的方法时, 则获取原来的已分配的内存大小用于减少内存统计, 然后重新分配内存, 重新统计已分配的内存. 如果不存在获取已分配内存大小的方法, 则需要手工计算出原已分配内存的大小, 然后重新分配内存并且设置新的内存大小到PREFIX_SIZE中, 最后先减少旧内存统计再添加新的内存统计, 返回对象可用的指针.

6.4 内存释放

内存释放有两个方法, 分别是zfree()和zfree_usable().


//释放指针内存
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);
    //减少内存统计, 包括 PREFIX_SIZE
    update_zmalloc_stat_free(oldsize+PREFIX_SIZE);
    //释放指针
    free(realptr);
#endif
}

//跟zfree相同, *usable表示释放内存的大小
/* Similar to zfree, '*usable' is set to the usable size being freed. */
void zfree_usable(void *ptr, size_t *usable) {
#ifndef HAVE_MALLOC_SIZE
    //原始指针
    void *realptr;
    //旧内存
    size_t oldsize;
#endif

    //如果指针为空, 则直接返回
    if (ptr == NULL) return;
//存在获取内存大小的方法
#ifdef HAVE_MALLOC_SIZE
//获取内存大小, 并且减小内存统计
    update_zmalloc_stat_free(*usable = zmalloc_size(ptr));
    //释放指针
    free(ptr);
#else
    //计算出原始指针
    realptr = (char*)ptr-PREFIX_SIZE;
    //获取内存大小
    *usable = oldsize = *((size_t*)realptr);
    //减少内存统计
    update_zmalloc_stat_free(oldsize+PREFIX_SIZE);
    //释放内存
    free(realptr);
#endif
}
  • zfree() 方法也是根据是否存在获取已分配内存大小方法来做不同的处理, 存在的话, 则直接获取旧内存大小用于减少内存统计, 然后直接释放指针, 不存在的话, 需要手动解析PREFIX_SIZE的值来减少内存统计, 然后根据对象指针和PREFIX_SIZE来还原原始指针, 再进行释放.
  • zfree_usable()方法和zfree()差不多, 只是会将可用的内存大小返回.

6.5 手动实现获取内存大小的方法

这两个方法, 估计是给上层使用的

//如果没有获取内存分配大小的方法
#ifndef HAVE_MALLOC_SIZE
//获取指针真实分配内存
size_t zmalloc_size(void *ptr) {
    void *realptr = (char*)ptr-PREFIX_SIZE;
    size_t size = *((size_t*)realptr);
    return size+PREFIX_SIZE;
}
//获取可用内存
size_t zmalloc_usable_size(void *ptr) {
    return zmalloc_size(ptr)-PREFIX_SIZE;
}
#endif

6.6 其他方法

//复制字符串
char *zstrdup(const char *s) {
    //获取字符串的长度, 字符串长度 + 字符串结束符(1)
    size_t l = strlen(s)+1;
    //分配内存
    char *p = zmalloc(l);

    //将字符串s的内容拷贝到指针p中
    memcpy(p,s,l);
    return p;
}

//获取已分配的内存
size_t zmalloc_used_memory(void) {
    size_t um;
    //将 used_memory 原子获取并写入到 um 中
    atomicGet(used_memory,um);
    return um;
}

//设置内存溢出处理器
void zmalloc_set_oom_handler(void (*oom_handler)(size_t)) {
    zmalloc_oom_handler = oom_handler;
}

微信文章链接: mp.weixin.qq.com/s/M9oI6wF8w…

求关注一波公众号: wolfleong