打算学习一下redis源码,结果刚开始看sds就发现一个陌生的词汇,zmalloc,查看zmalloc的实现,发现是对malloc的封装,并且还引出了ptMalloc和tcMalloc等知识,关于malloc库和redis的其他内存管理的知识,后续查看了后再谈
字长与字节对齐
首先要了解一个操作系统的基础,字长和字节对齐,字长是指 CPU一次性能读取数据的二进制位数,也就是我们通常所说的32位系统(字长4个字节)、64位系统(字长8个字节)的由来。所谓的字节对齐,简单的介绍可以查看 https://blog.csdn.net/ldw662523/article/details/79623404,总之,当我们向系统申请内存的时候,系统会返回给我们 n倍个字长的字节数,比如我们申请10字节,64位系统下会返回给我们2*8个字节
long类型变量和指针类型变量占用一字长的字节数,在32为系统下,sizeof(char *) 和 sizeof(long) 是4,在64位下是8,本文接下来内容都用64位,也就是8字节对齐为主
主要变量和函数
全局变量
//定义当前进程已使用的内存总量
static size_t used_memory = 0;
//标识是否线程安全,值为1时线程安全
static int zmalloc_thread_safe = 0;
//如果是线程安全的,修改used_memory 时的互斥锁
pthread_mutex_t used_memory_mutex = PTHREAD_MUTEX_INITIALIZER;
//zmalloc_oom_handler 为函数指针,当内存出错时调用,默认的调用函数是 zmalloc_default_oom
//oom 是out of memory
static void (*zmalloc_oom_handler)(size_t) = zmalloc_default_oom;
主要函数
void *zmalloc(size_t size); //分配内存空间
void zcalloc(size_t size); //分配内存并初始化为0
void zrealloc(void *ptr,size_t size); //重新分配内存空间的大小
void zfree(void *ptr); //释放zmalloc所分配的内存空间
char *zstrdup(const char *s); //字符串复制
void zlibc_free(void *ptr); //同free()
宏
//linux的glibc下是sizeof(size_t),64位系统为8字节
//当zmalloc申请内存时,多分配PREFIX_SIZE 个字节
PREFIX_SIZE
//若使用tcmalloc、jemalloc或Mac系统则定义此宏,linux的glibc不定义
HAVE_MALLOC_SIZE
宏函数
//分配内存空间后更新used_memory的值
update_zamlloc_stat_alloc
//释放内存空间后更新used_memory的值
update_zamlloc_stat_free
//线程安全地used_memory增加操作
update_zamlloc_stat_add
//线程安全地used_memory减少操作
update_zamlloc_stat_sub
zmalloc
void *zmalloc(size_t size) {
//size是要分配的内存大小,多分配了PREFIX_SIZE个字节,用于存放size的大小
void *ptr = malloc(size + PREFIX_SIZE);
//如果内存分配失败,调用函数指针zmalloc_oom_handler所指向的函数
if (!ptr) zmalloc_oom_handler(size);
#ifdef HAVE_MALLOC_SIZE
update_zmalloc_stat_alloc(zmalloc_size(ptr));
return ptr;
#else
//前sizeof(size_t) 个字节,存储要分配的内存大小
*((size_t*)ptr) = size;
update_zmalloc_stat_alloc(size+PREFIX_SIZE);
//只返回给用户可用的内存空间
return (char*)ptr+PREFIX_SIZE;
#endif
}
PREFIX_SIZE是一个条件编译的宏,不同的平台有不同的结果,在Linux中其值是sizeof(size_t),所以我们多分配了一个字长(8个字节)的空间,用于存放size的值
zmalloc_oom_handler指向内存错误处理函数,默认是指向函数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();
}
linux下不定义宏 HAVE_MALLOC_SIZE,所以走 #else的代码
//前sizeof(size_t) 个字节,存储要分配的内存大小
*((size_t*)ptr) = size;
update_zmalloc_stat_alloc(size+PREFIX_SIZE);
//只返回给用户可用的内存空间
return (char*)ptr+PREFIX_SIZE;
前sizeof(size_t),也就是PREFIX_SIZE个字节,存放所申请的内存大小size。我们先看一下
return (char*)ptr+PREFIX_SIZE;
将指针后移 PREFIX_SIZE 位,只给用户返回可用的内存空间,所以malloc出来的内存被分为了两部分,一部分用来存放size的值,一部分返回给用户。
然后调用了宏函数update_zmalloc_stat_alloc
update_zmalloc_stat_alloc
#define update_zmalloc_stat_alloc(__n) do { \
size_t _n = (__n); \
//判断 _n是否是8的倍数
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)
其中
if (_n&(sizeof(long)-1)) _n += sizeof(long)-(_n&(sizeof(long)-1)); \
用位运算判断 _n 是否是8的倍数,如果不是,加上所需的数值,使之能被8整除,比如,申请20bytes, 20 & (sizeof (long) - 1) = 20 & 7 = 4,非零,不能整除,即缺 8 - 4 = 4 bytes,于是申请的20 bytes多申请4bytes,24 mod 8 = 0正好
其实malloc出来的内存,已经是8的倍数,系统自动帮我们完成字节对齐,我们之所以还要判断一次,是为了保证used_memory的值是实际分配的内存大小。比如用户申请20字节,malloc以后,系统分配了24字节,但是我们并不知道分配了24字节,所以我们要像系统一样,去做一次字节对齐,这样能够知道系统具体分配了多少
接下来判断是否是线程安全的,zmalloc_thread_safe默认为0,也就是走else下面,直接给used_memory 加上值。如果是线程安全的,调用宏函数 update_zmalloc_stat_add
update_zmalloc_stat_add
#define update_zmalloc_stat_add(__n) do { \
pthread_mutex_lock(&used_memory_mutex); \
used_memory += (__n); \
pthread_mutex_unlock(&used_memory_mutex); \
} while(0)
其实就是调用了互斥锁来保证增加used_memory 值的时候是线程安全的,注意到一点是宏函数外层都用 do while(0) 包裹着,其实并不是额外加了什么功能,只是为了保持宏函数实现的时候语意和我们所写的代码一致,防止大括号以及分号的干扰,详见 https://blog.csdn.net/Move_now/article/details/73480195
zfree
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
//将指针前移 PREFIX_SIZE 位
realptr = (char*)ptr-PREFIX_SIZE;
//取出当zmalloc时返回给用户的可用的内存空间大小
oldsize = *((size_t*)realptr);
update_zmalloc_stat_free(oldsize+PREFIX_SIZE);
free(realptr);
#endif
}
我们还是关注没有定义 HAVE_MALLOC_SIZE 的部分,定义指针realptr,让其指向最初malloc返回的地址,用oldsize 保存用户所申请的内存大小
update_zmalloc_stat_free 和 update_zmalloc_stat_alloc 一样是宏函数,不同之处是前者在给变量used_memory减去分配的空间,而后者是加上该空间大小
update_zmalloc_stat_free
#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)
update_zmalloc_sub与zmalloc()中的update_zmalloc_add相对应,但功能相反,提供线程安全地used_memory减法操作
update_zmalloc_stat_sub
#define update_zmalloc_stat_sub(__n) do { \
pthread_mutex_lock(&used_memory_mutex); \
used_memory -= (__n); \
pthread_mutex_unlock(&used_memory_mutex); \
} while(0)
zcalloc
zcalloc()的实现基于calloc(),但是两者编程接口不同
void *calloc(size_t nmemb, size_t size);
void *zcalloc(size_t size);
calloc()的功能是也是分配内存空间,与malloc()的不同之处有两点:
1.它分配的空间大小是 size * nmemb。比如calloc(10,sizoef(char)); // 分配10个字节
2.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
}
zaclloc和zmalloc原理是一样的,只不过申请内存时malloc换成了calloc,和zmalloc相比就是申请的内存被初始化了
zrealloc
zrealloc()和realloc()具有相同的编程接口:
void *realloc (void *ptr, size_t size);
void *zrealloc(void *ptr, size_t size);
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
realptr = (char*)ptr-PREFIX_SIZE;
//原来分配的内存大小
oldsize = *((size_t*)realptr);
//新申请的内存大小
newptr = realloc(realptr,size+PREFIX_SIZE);
if (!newptr) zmalloc_oom_handler(size);
*((size_t*)newptr) = size;
update_zmalloc_stat_free(oldsize);
update_zmalloc_stat_alloc(size);
return (char*)newptr+PREFIX_SIZE;
#endif
}
zstrdup
复制字符串s的内容,申请新的内存空间存储字符串。并将这段新的字符串地址返回
char *zstrdup(const char *s) {
//strlen()函数是不统计'\0'的,所以最后要加1
size_t l = strlen(s)+1;
char *p = zmalloc(l);
//调用memcpy来完成复制
memcpy(p,s,l);
return p;
}
zmalloc_size
这个函数是为glibc定制的,只有用这个库时,才能使用这个函数
#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
得到zmalloc像系统申请的真实的内存大小
参考资料:https://blog.csdn.net/guodongxiaren/article/details/44747719
https://blog.csdn.net/u012842205/article/details/50392119