当我第一次阅读了这个文件的源代码的时候。我笑了,忽然想起前几周阿里电话二面的时候,问到了自己定义内存管理函数并处理8字节对齐问题。

当时无言以对,在面试官无数次的提示下才答了出来,结果显而易见,挂掉了二面。而这份源代码中函数zmalloc()和zfree()的设计思路和实现原理,正是面试官想要的答案。

源代码结构

zmalloc.c文件的内容例如以下:

主要函数

  • zmalloc()
  • zfree()
  • zcalloc()
  • zrelloc()
  • zstrdup()

字长与字节对齐

        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个字节了。该段代码依旧能保证对应代码可用。

zmalloc

        辅助的函数:
  • malloc()
  • zmalloc_oom_handler【函数指针】
  • zmalloc_default_oom()【被上面的函数指针所指向】
  • update_zmalloc_stat_alloc()【宏函数】
  • update_zmalloc_stat_add()【宏函数】
zmalloc()和malloc()有同样的函数接口(參数。返回值)。 

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
}

參数size是我们须要分配的内存大小。实际上我们调用malloc实际分配的大小是size+PREFIX_SIZE。PREFIX_SIZE是一个条件编译的宏。不同的平台有不同的结果,在Linux中其值是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)循环看似毫无意义,实际上大有深意。

这部分内容不是本文讨论的重点,这里不再赘述。详细请看网上的这篇文章http://www.spongeliu.com/415.html

        由于 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的情况】。
#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()分配的空间。
辅助函数:
  • free()
  • update_zmalloc_free()【宏函数】
  • update_zmalloc_sub()【宏函数】
  • zmalloc_size()

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
realptr = (char*)ptr-PREFIX_SIZE;
oldsize = *((size_t*)realptr);
update_zmalloc_stat_free(oldsize+PREFIX_SIZE);
free(realptr);
#endif
}

重点关注#else后面的代码

realptr = (char *)ptr - PREFIX_SIZE;
表示的是ptr指针向前偏移8个字节的长度,即回退到最初malloc返回的地址,这里称为realptr。然后
 oldsize = *((size_t*)realptr);

先进行类型转换再取指针所指向的值。通过zmalloc()函数的分析,可知这里存储着我们最初须要分配的内存大小(zmalloc中的size)。这里赋值个oldsize

update_zmalloc_stat_free(oldsize+PREFIX_SIZE);

update_zmalloc_stat_free()也是一个宏函数,和zmalloc中update_zmalloc_stat_alloc()大致同样。唯一不同之处是前者在给变量used_memory减去分配的空间,而后者是加上该空间大小。
最后free(realptr)。清除空间

update_zmalloc_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减法操作。

#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()不会
辅助函数
  • calloc()
  • update_zmalloc_stat_alloc()【宏函数】
  • update_zmalloc_stat_add()【宏函数】

zcalloc()源代码

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
}

zcalloc()中没有calloc()的第一个函数nmemb。

由于它每次调用calloc(),其第一个參数都是1。

也就是说zcalloc()功能是每次分配 size+PREFIX_SIZE 的空间,并初始化。

其余代码的分析和zmalloc()同样,也就是说:
        zcalloc()和zmalloc()具有同样的编程接口,实现功能基本同样。唯一不同之处是zcalloc()会做初始化工作。而zmalloc()不会。

zrealloc

        zrealloc()和realloc()具有同样的编程接口:
void *realloc (void *ptr, size_t size);
void *zrealloc(void *ptr, size_t size);
        realloc()要完毕的功能是给首地址ptr的内存空间,又一次分配大小。假设失败了,则在其他位置新建一块大小为size字节的空间,将原先的数据拷贝到新的内存空间。并返回这段内存首地址【原内存会被系统自然释放】。
        zrealloc()要完毕的功能也相似。

辅助函数:
  • zmalloc()
  • zmalloc_size()
  • realloc()
  • zmalloc_oom_handler【函数指针】
  • update_zmalloc_stat_free()【宏函数】
  • update_zmalloc_stat_alloc()【宏函数】

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
}

经过前面关于zmalloc()和zfree()的源代码解读,相信您一定可以非常轻松地读懂zrealloc()的源代码,这里我就不赘述了。

zstrdup

从这个函数名中,非常easy发现它是string duplicate的缩写,即字符串复制。它的代码比較简单。先看一下声明:

char *zstrdup(const char *s);

功能描写叙述:复制字符串s的内容。到新的内存空间,构造新的字符串【堆区】。

并将这段新的字符串地址返回。

zstrdup源代码

char *zstrdup(const char *s) {
size_t l = strlen(s)+1;
char *p = zmalloc(l); memcpy(p,s,l);
return p;
}
  1. 首先,先获得字符串s的长度,新闻strlen()函数是不统计'\0'的,所以最后要加1。

  2. 然后调用zmalloc()来分配足够的空间。首地址为p。
  3. 调用memcpy来完毕复制。
  4. 然后返回p。

简介一下memcpy

memcpy

        这是标准C【ANSI C】中用于内存复制的函数。在头文件<string.h>中(gcc)。

声明例如以下:

void *memcpy(void *dest, const void *src, size_t n);

dest即目的地址。src是源地址。n是要复制的字节数。

Redis内存管理的基石zmallc.c源代码解读(一)的更多相关文章

  1. Redis 内存管理与事件处理

    1 Redis内存管理 Redis内存管理相关文件为zmalloc.c/zmalloc.h,其只是对C中内存管理函数做了简单的封装,屏蔽了底层平台的差异,并增加了内存使用情况统计的功能. void * ...

  2. Redis 内存管理 源码分析

    要想了解redis底层的内存管理是如何进行的,直接看源码绝对是一个很好的选择 下面是我添加了详细注释的源码,需要注意的是,为了便于源码分析,我把redis为了弥补平台差异的那部分代码删了,只需要知道有 ...

  3. Redis内存管理(一)

    Redis数据库的内存管理函数有关的文件为:zmalloc.h和zmalloc.c. Redis作者在编写内存管理模块时考虑到了查看系统内是否安装了TCMalloc或者Jemalloc模块,这两个是已 ...

  4. Redis内存管理(二)

    上一遍详细的写明了Redis为内存管理所做的初始化工作,这篇文章写具体的函数实现. 1.zmalloc_size,返回内存池大小函数,因为库不同,所以这个函数在内部有很多的宏定义,通过具体使用的库来确 ...

  5. TCMalloc优化MySQL、Nginx、Redis内存管理

    TCMalloc(Thread-Caching Malloc)与标准glibc库的malloc实现一样的功能,但是TCMalloc在效率和速度效率都比标准malloc高很多.TCMalloc是 goo ...

  6. redis内存管理

    Redis主要通过控制内存上线和回收策略来实现内存管理. 1. 设置内存上限 redis使用maxmemory参数限制最大可用内存.限制的目的主要有: 用户缓存场景,当超出内存上限maxmemory时 ...

  7. 详解 Redis 内存管理机制和实现

    Redis是一个基于内存的键值数据库,其内存管理是非常重要的.本文内存管理的内容包括:过期键的懒性删除和过期删除以及内存溢出控制策略. 最大内存限制 Redis使用 maxmemory 参数限制最大可 ...

  8. redis内存管理代码的目光

    zmalloc.h /* zmalloc - total amount of allocated memory aware version of malloc() * * Copyright (c) ...

  9. redis 内存管理与数据淘汰机制(转载)

    原文地址:http://www.jianshu.com/p/2f14bc570563?from=jiantop.com 最大内存设置 默认情况下,在32位OS中,Redis最大使用3GB的内存,在64 ...

随机推荐

  1. 【习题 7-9 UVA-1604】Cubic Eight-Puzzle

    [链接] 我是链接,点我呀:) [题意] 在这里输入题意 [题解] IDA* 保证这次移动的方格不和前一次重复. 然后加一个8数码的剪枝就行了. ->看看当前状态和目标状态有多少个地方是不一样的 ...

  2. 【Unity3D自学记录】鼠标移动三维物体

    创建一个脚本.例如以下: using UnityEngine; using System.Collections; public class OnMouse : MonoBehaviour { IEn ...

  3. 用jquery获取单选按钮选中的内容 和 获取select下拉列表选中的值

    1.<label><input name='reason' type='radio' value='您的评论内容涉嫌谣言' />您的评论内容涉嫌谣言</label> ...

  4. nuxt按需引入 element-UI、自定义主题色(终极按需引入)

    首先你要知道 nuxt.js怎么引入第三方插件 : 不多BB. 一.按需引入element-UI 第一步:安装 babel-plugin-component: npm install babel-pl ...

  5. 洛谷 P2790 ccj与zrz之积木问题

    P2790 ccj与zrz之积木问题 题目背景 ccj和zrz无聊到了玩起了搭积木...(本题选自uva101,翻译来自<算法竞赛入门经典2>) 题目描述 从左到右有n个木块,编号从0到n ...

  6. 2017.1-TOP5 Android开源库

    Colorful (Github) Colorful简单实用,通过这个开源库可以通过编码的方式来改变应用的主题,不再需要定义不同的style dependencies { compile 'com.g ...

  7. java调用C++的过程

    转自https://blog.csdn.net/yjhdxflqm/article/details/50503551 jni是java和C.C++通信的桥梁. java适合写上层的应用,C.C++适合 ...

  8. Java ThreadLocal Example(java中的ThreadLocal例子)

    Java ThreadLocal is used to create thread local variables. We know that all threads of an Object sha ...

  9. Eclipse "Could not create java virtual machine"的问题解决

    今天到了新的环境,需要重新搭建Android的开发环境,下载eclipse并安装了JDK1.6后,启动eclipse,发现出现了错误“Could not create Javavirtual mach ...

  10. 每日技术总结:Better-scroll应用于弹出层内容滚动

    一.Better-scroll在项目中的应用 Better-scroll这款滚动插件还是很好用的,通常不会有什么问题.但偶尔总会出点意外.今天再次使用better-scroll,记录一下这次顺利的过程 ...