Redis内存管理的基石zmallc.c源码解读

当我第一次阅读了这个文件的源码的时候,我笑了,忽然想起前几周阿里电话二面的时候,问到了自定义内存管理函数并处理8字节对齐问题。当时无言以对,在面试官无数次的提示下才答了出来,结果显而易见,挂掉了二面。而这份源码中函数zmalloc()和zfree()的设计思路和实现原理,正是面试官想要的答案。

源码结构

zmalloc.c文件的内容如下:

主要函数

字长与字节对齐

CPU一次性能读取数据的二进制位数称为字长,也就是我们通常所说的32位系统(字长4个字节)、64位系统(字长8个字节)的由来。所谓的8字节对齐,就是指变量的起始地址是8的倍数。比如程序运行时(CPU)在读取long型数据的时候,只需要一个总线周期,时间更短,如果不是8字节对齐的则需要两个总线周期才能读完数据。

本文中我提到的8字节对齐是针对64位系统而言的,如果是32位系统那么就是4字节对齐。实际上Redis源码中的字节对齐是软编码,而非硬编码。里面多用sizeof(long)或sizeof(size_t)来表示。size_t(long unsigned int)和long的长度是一样的,long的长度就是计算机的字长。这样在未来的系统中如果字长(long的大小)不是8个字节了,该段代码依然能保证相应代码可用。

zmalloc

辅助的函数:

zmalloc()和malloc()有相同的函数接口(参数,返回值)。

zmalloc()源码

void *zmalloc(size_t size) {void *ptr = malloc(size+PREFIX_SIZE);if (!ptr) zmalloc_oom_handler(size);#ifdef HAVE_MALLOC_SIZEupdate_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}

参数size是我们需要分配的内存大小。实际上我们调用malloc实际分配的大小是size+PREFIX_SIZE。PREFIX_SIZE是sizeof(size_t),所以我们多分配了一个字长(8个字节)的空间(后面代码可以看到多分配8个字节的目的是用于储存size的值)。

如果ptr指针为NULL(内存分配失败),调用zmalloc_oom_handler(size)。该函数实际上是一个函数指针指向函数zmalloc_default_oom,其主要功能就是打印错误信息并终止程序。

// oom是out of memory(内存不足)的意思static void zmalloc_default_oom(size_t size) {fprintf(stderr, "zmalloc: Out of memory trying to allocate %zu bytes\n",size);fflush(stderr);abort();}接下来是宏的条件编译,我们聚焦在#else的部分。

*((size_t*)ptr) = size;update_zmalloc_stat_alloc(size+PREFIX_SIZE);return (char*)ptr+PREFIX_SIZE;

第一行就是在已分配空间的第一个字长(前8个字节)处存储需要分配的字节大小(size)。

第二行调用了update_zmalloc_stat_alloc()【宏函数】,它的功能是更新全局变量used_memory(已分配内存的大小)的值(源码解读见下一节)。

第三行返回的(char *)ptr+PREFIX_SIZE。就是将已分配内存的起始地址向右偏移PREFIX_SIZE * sizeof(char)的长度(即8个字节),此时得到的新指针指向的内存空间的大小就等于size了。

接下来,分析一下update_zmalloc_stat_alloc的源码

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)循环看似毫无意义,实际上大有深意。这部分内容不是本文讨论的重点,这里不再赘述。具体请看网上的这篇文章。

因为 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函数真正要实现的功能并不是进行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的情况】。

#define update_zmalloc_stat_add(__n) do { \pthread_mutex_lock(&used_memory_mutex); \used_memory += (__n); \pthread_mutex_unlock(&used_memory_mutex); \} while(0) pthread_mutex_lock()和pthread_mutex_unlock()使用互斥锁(mutex)来实现线程同步,前者表示加锁,后者表示解锁,它们是POSIX定义的线程同步函数。当加锁以后它后面的代码在多线程同时执行这段代码的时候就只会执行一次,也就是实现了线程安全。

zfree

zfree()和free()有相同的编程接口,它负责清除zmalloc()分配的空间。

辅助函数:

zfree()源码

人生的路无需苛求。只要你迈步,路就在你的脚下延伸。

Redis内存管理的基石zmallc.c源码解读

相关文章:

你感兴趣的文章:

标签云: