Redis源码阅读之zmalloc内存分配

本文内容绝大部分来自:https://blog.csdn.net/guodongxiaren/article/details/44747719,果冻虾仁,中间根据自己的知识盲点补充了一些资料,具体可查看参考资料。

zmalloc是redis自己实现的内存分配,是对linux中malloc,free,relloc这3个函数的一个封装。

一. zmalloc定义的函数

void *zmalloc(size_t size);/* 调用zmalloc申请size个大小的空间 */
void *zcalloc(size_t size);/* 调用系统函数calloc函数申请空间 */
void *zrealloc(void *ptr, size_t size);/* 原内存重新调整空间为size的大小 */
void zfree(void *ptr);/* 释放空间方法,并更新used_memory的值 */
char *zstrdup(const char *s);/* 字符串复制方法 */
size_t zmalloc_used_memory(void);/* 获取当前已经占用的内存大小 */
void zmalloc_enable_thread_safeness(void);/* 是否设置线程安全模式 */
void zmalloc_set_oom_handler(void (*oom_handler)(size_t)); /* 可自定义设置内存溢出的处理方法 */
float zmalloc_get_fragmentation_ratio(size_t rss);/* 所给大小与已使用内存大小之比 */
size_t zmalloc_get_rss(void);
size_t zmalloc_get_private_dirty(void);/* 获取私有的脏数据大小 */
void zlibc_free(void *ptr);/* 原始系统free释放方法 */
其中最重要的函数是zmalloc()、zfree()、zcalloc()、zrelloc()、zstrdup()。

在介绍函数之前,首先说一说#if,#if define,#if defined.

#if的使用说明
#if的后面接的是表达式
#if (MAX==10)||(MAX==20) code... #endif
它的作用是:如果(MAX==10)||(MAX==20)成立,那么编译器就会把其中的#if 与 #endif之间的代码编译进去>(注意:是编译进去,不是执行!!)
#if defined的使用
#if后面接的是一个宏。
#if defined (x) ...code... #endif
这个#if defined它不管里面的“x”的逻辑是“真”还是“假”它只管这个程序的前面的宏定义里面有没有定义“x”这个>宏,如果定义了x这个宏,那么,编译器会编译中间的…code…否则不直接忽视中间的…code…代码。
另外 #if defined(x)也可以取反,也就用 #if !defined(x)
#ifdef的使用
#ifdef的使用和#if defined()的用法一致
#ifndef又和#if !defined()的用法一致。

最后强调两点:

  • 第一:这几个宏定义只是决定代码块是否被编译!
  • 第二:别忘了#endif

下面开始详细介绍具体函数

1. zmalloc

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
}

zmalloc的函数很简短,size是我们需要分配的内存大小,但是在调用zmalloc的时候,实际上还会多分配一个PREFIX_SIZE。PREFIX_SIZE是以个条件编译的宏,根据不同的操作系统有不同的大小,这个具体可以查看参考资料[4]。在Linux系统中PREFIX_SIZE = sizeof(size_t)。如果malloc失败,就进入zmalloc_oom_handler函数,输出OOM的报错信息并终止程序。后面代码可以看到多分配一个PREFIX_SIZE的目的是用于储存size的值。

接下来是宏的条件编译,

  • 第一行就是在已分配空间的第一个字长(前8个字节)处存储需要分配的字节大小(size)。
  • 第二行调用了update_zmalloc_stat_alloc()【宏函数】,它的功能是更新全局变量used_memory(已分配内存的大小)的值(源码解读见下一节)。
  • 第三行返回的(char *)ptr+PREFIX_SIZE。就是将已分配内存的起始地址向右偏移PREFIX_SIZE * sizeof(char)的长度(即8个字节),此时得到的新指针指向的内存空间的大小就等于size了。

接下来分析 update_zmalloc_stat_alloc

#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)

这个宏函数最外圈有一个do{...}while(0)循环看似毫无意义,实际上大有深意,具体作用可以搜索宏定义时的do while,主要作用是保证代码安全性。
因为 sizeof(long) = 8 【64位系统中】,所以上面的第一个if语句,可以等价于以下代码:

  • if(_n&7) _n += 8 - (_n&7);
    
  • 这段代码就是判断分配的内存空间的大小是不是8的倍数。如果内存大小不是8的倍数,就加上相应的偏移量使之变成8的倍数。_n&7 在功能上等价于 _n%8,不过位操作的效率显然更高。
  • malloc()本身能够保证所分配的内存是8字节对齐的:如果你要分配的内存不是8的倍数,那么malloc就会多分配一点,来凑成8的倍数。所以update_zmalloc_stat_alloc函数(或者说zmalloc()相对malloc()而言)真正要实现的功能并不是进行8字节对齐(malloc已经保证了),它的真正目的是使变量used_memory精确的维护实际已分配内存的大小.
  • 第2个if的条件是一个整型变量zmalloc_thread_safe。顾名思义,它的值表示操作是否是线程安全的,如果不是线程安全的(else),就给变量used_memory加上n。used_memory是zmalloc.c文件中定义的全局静态变量,表示已分配内存的大小。如果是内存安全的就使用update_zmalloc_stat_add来给used_memory加上n。
  • update_zmalloc_stat_add也是一个宏函数(Redis效率之高,速度之快,这些宏函数可谓功不可没)。它也是一个条件编译的宏,依据不同的宏有不同的定义,这里我们来看一下#else后面的定义的源码【zmalloc.c有多处条件编译的宏,为了把精力都集中在内存管理的实现算法上,这里我只关注Linux平台下使用glibc的malloc的情况】。

参考资料

[1] 《redis设计与实现》, 黄建宏
[2] https://blog.csdn.net/Androidlushangderen/article/details/40659331, Android路上的人, 2014-10-31
[3] https://www.cnblogs.com/wuchanming/p/4057630.html, Jessica要努力了, 2014-10-28
[4] https://blog.csdn.net/ylo523/article/details/38756047 他并不是原作者,就不贴名字了......
[5] https://blog.csdn.net/guodongxiaren/article/details/44747719, 果冻虾仁, 2015-3-31

推荐阅读更多精彩内容