Redis 源码分析(一)内存池-zmalloc
- 概述
- 内存管理
- zmalloc 内存分配结构图
- malloc库
- 重要宏定义
- 全局变量
- update_zmalloc_stat_add(__n) 增加记录使用的内存大小
- update_zmalloc_stat_alloc 增加已经分配的内存大小
- update_zmalloc_stat_add(__n) 减小记录使用内存大小
- update_zmalloc_stat_free 减小已经分配的内存大小
- OOM内存溢出处理
- zmalloc 分配内存
- zcalloc 分配内存空间,并且初始化
- zrealloc 重新开辟空间的大小
- zmalloc_size 当前分配的空间大小
- zfree 内存回收
- zmalloc_used_memory 获取已经使用的内存
- zmalloc_get_rss 获取内存碎片大小
- zmalloc_get_fragmentation_ratio 获取内存碎片率
- zmalloc_get_private_dirty 获取私有的被占有的脏内存
概述
大家好,我是一名萌新,在文章分享有不正确的、语言比较晦涩的地方,欢迎大家明确提出来。
本人经历:过去一年在某高校的Java方向的科研团队工作学习,今年在武汉做图像算法方向的工作。自己比较喜欢玩特性数据库比如Elasticsearch、Redis。对数据结构方面极其敏感,就从今天开始立一个flag,一个一个分析。
内存管理
zmalloc 内存分配结构图
malloc库
malloc是C语言的标准库函数,但是Redis并未使用标准malloc。在Redis源码src\Makefile中间有一段源码是这样写的: ifeq ($(uname_S),Linux) MALLOC = jemalloc,从次可以发现,Redis默认使用了 <jemalloc/jemalloc.h> 第三方内存池框架,它的官方描述它可以避免碎片和可扩展的高性能并发支持。除了此套框架外,同时它给出了其他两套内存分配回收方案。其中一套解决方案是google的 <google/tcmalloc.h> ,它是基于C++实现的,性能上比其他的框架都要好,但是与C语言存在一定的兼容性。另外一套就是标准库的 <malloc/malloc.h>,最不佳的选择,因为malloc进行的动态内存分配和嵌入式系统中使用到堆区的内存分配会产生内存碎片。
重要宏定义
HAVE_MALLOC_SIZE这个宏定义是会判断当前的操作系统是否有 malloc_size() 函数,也就是判断当前指针malloc开辟空间后指向空间的大小。PREFIX_SIZE 作为新开辟空间的头部信息,这块内存空间主要是存放size_t类型的开辟空间的大小信息。在Redis高版本中,如果是Mac平台,PREFIX_SIZE的大小就是有默认值固定的。
全局变量
已经使用的内存
static size_t used_memory = 0;
判断当前是否线程安全
static int zmalloc_thread_safe = 0;
有两种方法创建互斥锁,静态方式和动态方式。
POSIX定义了一个宏PTHREAD_MUTEX_INITIALIZER来静态初始化互斥锁。在LinuxThreads实现中,pthread_mutex_t是一个结构,而PTHREAD_MUTEX_INITIALIZER则是一个结构常量。
- PTHREAD_MUTEX_NORMAL : 不提供死锁检测
- PTHREAD_MUTEX_ERRORCHECK :提供错误检查,尝试重新锁定的互斥锁已经由该线程锁定 或者
解除锁定的互斥锁不是由该线程锁定或者未锁定 返回错误 - PTHREAD_MUTEX_RECURSIVE :锁定计数,锁住 +1 ,解除 -1 ,0可以获取
- PTHREAD_MUTEX_DEFAULT : 以递归方式锁定,尝试解除对它的锁定、解除锁定尚未锁定的互斥锁,则会产生不确定的行为
POSIX这个名称是由理查德·斯托曼(RMS)应IEEE的要求而提议的一个易于记忆的名称。它基本上是Portable Operating System Interface(可移植操作系统接口)的缩写,而X则表明其对Unix API的传承。
pthread_mutex_t used_memory_mutex = PTHREAD_MUTEX_INITIALIZER;
update_zmalloc_stat_add(__n) 增加记录使用的内存大小
Linux和其它代码库里的宏都用do/while(0)来包围执行逻辑,因为它能确保宏的行为总是相同的,而不管在调用代码中使用了多少分号和大括号。_n可以是任意类型。
POSIX定义的线程同步函数, 调用本地方法的互斥锁pthread_mutex(&used_memory_mutex),为了保证全局变量used_memory 的增加内存是原子性操作。
#define update_zmalloc_stat_add(__n) do { \
pthread_mutex_lock(&used_memory_mutex); \
used_memory += (__n); \
pthread_mutex_unlock(&used_memory_mutex); \
} while(0)
update_zmalloc_stat_alloc 增加已经分配的内存大小
在64为的Linux环境下,第二行位运算代码可以替换为 if (_n&7==0) -n+=8-(-n&7)。这段代码判断内存空间的大小是不是8的倍数。malloc()本身能够保证所分配的内存是8字节对齐的:如果你要分配的内存不是8的倍数,那么malloc就会多分配一点,来凑成8的倍数。所以update_zmalloc_stat_alloc函数(或者说zmalloc()相对malloc()而言)真正要实现的功能并不是进行8字节对齐(malloc已经保证了),它的真正目的是使变量used_memory精确的维护实际已分配内存的大小,如果64位系统种 内存大小不是8的倍数,就加上相应的偏移量使之变成8的倍数。第三行if分支是为了判断是否使用线程安全,zmalloc_thread_safe默认值为1,是启用线程安全函数的。
#define update_zmalloc_stat_alloc(__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_add(_n); \
} else { \
used_memory += _n; \
} \
} while(0)
update_zmalloc_stat_add(__n) 减小记录使用内存大小
#define update_zmalloc_stat_sub(__n) do { \
pthread_mutex_lock(&used_memory_mutex); \
used_memory -= (__n); \
pthread_mutex_unlock(&used_memory_mutex); \
} while(0)
update_zmalloc_stat_free 减小已经分配的内存大小
update_zmalloc_sub与zmalloc()中的update_zmalloc_add相对应,但功能相反,提供线程安全地used_memory减法操作。
#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)
OOM内存溢出处理
OOM溢出的异常处理使用的是函数的指针,把zmalloc_default_oom指向了名称为zmalloc_oom_handler的函数指针。优秀的松耦合设计,方便扩展时自定义处理错误。
static void (*zmalloc_oom_handler)(size_t) = zmalloc_default_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();
}
// 设置OOM内存溢出后的方法
void zmalloc_set_oom_handler(void (*oom_handler)(size_t)) {
zmalloc_oom_handler = oom_handler;
}
zmalloc 分配内存
首先调用函数malloc分配size+PREFIX_SIZE(Linux 64位等于8个字节),这个malloc函数也是通过 #define 定义的,使用tc_mlloc或者je_malloc,以及其他的tc_、je开头的第三方函数都会替换为C标准库风格的函数。 如果操作系统有malloc_size()函数,救调用Redis封装的zmalloc_size()函数计算出size_t的大小,再去修改全局的used_memory 的大小。如果没有,需要把ptr指针的头部放置此次分配的内存块的大小,接着修改全局used_memory 的大小,最后返回prt的首指针。
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
}
zcalloc 分配内存空间,并且初始化
与zmalloc函数实现相似,calloc()会对分配的空间做初始化工作(初始化为0),而malloc()不会
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
}
zrealloc 重新开辟空间的大小
realloc()要完成的功能是给首地址ptr的内存空间,重新分配大小。如果失败了,则在其它位置新建一块大小为size字节的空间,将原先的数据复制到新的内存空间,并返回这段内存首地址【原内存会被系统自然释放】。zrealloc()要完成的功能也类似。
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
// 1、减去头部8字节的信息
realptr = (char*)ptr-PREFIX_SIZE;
// 2、获取原来的大小
oldsize = *((size_t*)realptr);
// 3、调用 realloc 函数重新分配空间
newptr = realloc(realptr,size+PREFIX_SIZE);
// 4、如果为空 out of memory
if (!newptr) zmalloc_oom_handler(size);
// 5、重新分配空间的newptr空间
*((size_t*)newptr) = size;
// 6、use_memory减去旧空间
update_zmalloc_stat_free(oldsize);
// 7、use_memory加上新的空间
update_zmalloc_stat_alloc(size);
// 8 、返回新开辟的空间 newptr + 向右偏移位8指针
return (char*)newptr+PREFIX_SIZE;
#endif
}
zmalloc_size 当前分配的空间大小
malloc 基础库操作系统本身不提供此 malloc_size() 函数,引入了第三方的malloc库后为每个分配的指针,增加带有信息的指针头(分配的64位 8首字节;32位4字节)。判断新开辟的空间是不是8(64位)的倍数,如果不是就会进行内存对其。
#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
zfree 内存回收
Redis在回收内存时,与开辟内存函数zmalloc一样,会判断操作系统是否有malloc_size函数,如果有调用zmalloc_size函数获取当前指针ptr的大小,调用宏定义函数free经行回收,否者需要自行在指针的头部获取长度信息来进行回收。
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
// 1 、指针ptr 64位下 向前偏移8个字节的长度
realptr = (char*)ptr-PREFIX_SIZE;
// 2 、得到最初需要分配的内存大小
oldsize = *((size_t*)realptr);
//3 、在线程安全的情况下,减去use_memory 总消耗的内存量
update_zmalloc_stat_free(oldsize+PREFIX_SIZE);
// 4、清除空间
free(realptr);
#endif
}
zmalloc_used_memory 获取已经使用的内存
HAVE_ATOMIC 判断是否拥有原子函数库,__sync_add_and_fetch是原子操作函数:对于多线程对全局变量进行自加1 ,返回没有自增1之前的值,使用此函数,就不用怕线程锁,锁住资源。pthread_mutex_lock、pthread_mutex_unlock是linux的互斥锁,调用全局变量 used_memory_mutex来锁定或者解锁当前资源。
size_t zmalloc_used_memory(void) {
size_t um;
if (zmalloc_thread_safe) {
#ifdef HAVE_ATOMIC
um = __sync_add_and_fetch(&used_memory, 0);
#else
// linux 互斥锁 锁定当前的 used_memory_mutex
pthread_mutex_lock(&used_memory_mutex);
um = used_memory;
// linux 互斥锁 解锁当前的 used_memory_mutex
pthread_mutex_unlock(&used_memory_mutex);
#endif
}
else {
um = used_memory;
}
return um;
}
zmalloc_get_rss 获取内存碎片大小
获取内存碎片大小主要是调用sysconf()系统库函数,查看在/proc//stat文件下的内存消耗
size_t zmalloc_get_rss(void) {
int page = sysconf(_SC_PAGESIZE);//调用库函数sysconf()【大家可以man sysconf查看详细内容】来查询内存页的大小
size_t rss;
char buf[4096];
char filename[256];
int fd, count;
char *p, *x;
//是在当前进程的 /proc/<pid>/stat (<pid>指代当前进程实际id)文件中进行检索
// 把检索出的绝对路径保存到filename中
snprintf(filename,256,"/proc/%d/stat",getpid());
if ((fd = open(filename,O_RDONLY)) == -1) return 0;// 以只读模式打开 /proc/<pid>/stat 文件,然后从中读入4096个字符到字符数组buf中
if (read(fd,buf,4096) <= 0) {
close(fd);
return 0;
}
close(fd);
p = buf;
// 该文件的第24个字段是RSS的信息,它的单位是pages(内存页的数目)
count = 23; /* RSS is the 24th field in /proc/<pid>/stat */
while(p && count--) {
p = strchr(p,' ');
if (p) p++; //p++原因是因为,p当前指向的是空格,在执行自增操作之后就指向下一个字段的首地址
}
if (!p) return 0; // 判断是否是空指针
x = strchr(p,' '); // 查找空格在p指针数组中首次出现的地址
if (!x) return 0;// 如果为NULL就是查询失败
*x = '\0'; //把最后一个字符设置为 c字符串风格的 ’\0‘
rss = strtoll(p,NULL,10); //string 转 为10进制的long long类型
rss *= page; //rss page相乘并返回,rss获得的实际上是内存页的页数,page保存的是每个内存页的大小(单位字节),相乘之后就表示RSS实际的内存大小了
return rss;
}
zmalloc_get_fragmentation_ratio 获取内存碎片率
获取内存碎片率,用专业术语来形容:RSS与所分配总内存空间的比值。内存碎片分为内部碎片和外部碎片。内部碎片是已经被分配出去(能明确指出属于哪个进程)却不能被利用的内存空间,直到进程释放掉,才能被系统利用。外部碎片是还没有被分配出去(不属于任何进程),但由于太小了无法分配给申请内存空间的新进程的内存空闲区域。
获取内存碎片率公式为:zmalloc_get_rss()/ used_memory
float zmalloc_get_fragmentation_ratio(size_t rss) {
return (float)rss/zmalloc_used_memory();
}
zmalloc_get_private_dirty 获取私有的被占有的脏内存
如果操作系统有smaps文件,完成的操作就是扫描 /proc/self/smaps文件,统计其中所有 Private_Dirty字段的和。如果没有返回内存碎片为0。
Rss=Shared_Clean+Shared_Dirty+Private_Clean+Private_Dirty
- Shared_Clean:多进程共享的内存,且其内容未被任意进程修改
- Shared_Dirty:多进程共享的内存,但其内容被某个进程修改
- Private_Clean:某个进程独享的内存,且其内容没有修改
- Private_Dirty:某个进程独享的内存,但其内容被该进程修改
#if defined(HAVE_PROC_SMAPS)
size_t zmalloc_get_private_dirty(void) {
char line[1024];
size_t pd = 0;
FILE *fp = fopen("/proc/self/smaps","r");
if (!fp) return 0;
while(fgets(line,sizeof(line),fp) != NULL) {
if (strncmp(line,"Private_Dirty:",14) == 0) {
char *p = strchr(line,'k');
if (p) {
*p = '\0';
pd += strtol(line+14,NULL,10) * 1024;
}
}
}
fclose(fp);
return pd;
}
#else
size_t zmalloc_get_private_dirty(void) {
return 0;
}
#endif