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 内存模型
首部: PREFIX_SIZE
目标分配内存大小: 也就是入参的SIZE
填充字节数(对齐)是由malloc自动完成的
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