Redis 的内存管理模块:zmalloc

1. 功能特性

1.1 支持多平台

通过宏的方式来区分系统,针对不同的系统使用不同的内存管理方式,支持 linux、Solaris、Apple。

1.2 支持多种内存管理方式

支持 tcmalloc、jemalloc 和 ptmalloc,apple 的 malloc 等。

关于 tcmalloc 的宏如下:

1
2
3
4
5
6
7
8
9
10
11
12
#ifdef USE_TCMALLOC
#define ZMALLOC_LIB ("tcmalloc-" __xstr(TC_VERSION_MAJOR) "." __xstr(TC_VERSION_MINOR))
#include <gperftools/tcmalloc.h>
#if (TC_VERSION_MAJOR == 2 && TC_VERSION_MINOR >= 7) || (TC_VERSION_MAJOR > 2)
#define HAVE_MALLOC_SIZE 1
#define zmalloc_size(ptr) tc_malloc_size(ptr)
#else
#error "New version tcmalloc is required"
#endif
#else
#define ZMALLOC_LIB "libc"
#endif

这几行代码来判断 tcmalloc 的版本是否低于 2.7,并对获取 malloc_size 的函数进行了映射。
为了方便在各种内存管理方式之间切换,使用宏对各内存管理函数进行了统一映射,例如 tcmalloc:

1
2
3
4
5
6
#ifdef USE_TCMALLOC
#define malloc(size) tc_malloc(size)
#define calloc(count, size) tc_calloc(count, size)
#define realloc(ptr, size) tc_realloc(ptr, size)
#define free(ptr) tc_free(ptr)
#endif

编译时通过指定 USE_TCMALLOC 宏可切换为 tcmalloc,不指定默认使用 glibc 的 ptmalloc。

1.3 支持原子操作

通过宏 HAVE_ATOMIC 可指定 redis 使用原子操作,在多线程下可以减少锁竞争,这样比通过线程锁的方式要更安全更快速更方便。
全局静态变量 used_memory 表示了 redis 已经使用的内存,在申请内存后,对该变量进行同步操作,使用原子操作可以很方便的同步:

1
2
#define update_zmalloc_stat_add(__n) __sync_add_and_fetch(&used_memory, (__n))
#define update_zmalloc_stat_sub(__n) __sync_sub_and_fetch(&used_memory, (__n))

如果使用线程锁的方式,代码如下:

1
2
3
4
5
6
7
8
9
10
11
#define update_zmalloc_stat_add(__n) do { \
    pthread_mutex_lock(&used_memory_mutex); \
    used_memory += (__n); \
    pthread_mutex_unlock(&used_memory_mutex); \
while(0)

#define update_zmalloc_stat_sub(__n) do { \
    pthread_mutex_lock(&used_memory_mutex); \
    used_memory -= (__n); \
    pthread_mutex_unlock(&used_memory_mutex); \
while(0)

使用宏定义的方式可以,减少堆栈等资源的消耗。

1.4 支持内存对齐

内存分配的最小单位为通用寄存器的大小,使用 sizeof(long) 可以代表通用寄存器的大小。在内存申请的时候,如果不做对齐,可能导致多线程中读取到错误数据。
内存申请和释放的相关代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#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)

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

内存对齐相关代码:

1
if (_n&(sizeof(long)-1)) _n += sizeof(long) - (_n&(sizeof(long)-1));

1.5 更方便的获取申请后实际内存大小

对于 tcmalloc 等第三方内存管理库,支持获取实际申请的内存大小,例如 tcmalloc 的函数 tc_malloc_size() 可以获取实际分配的内存大小。
对于标准 C 来讲是不支持这点的。但是在没有第三方库的情况下怎么办?可以通过从内存开始遍历,碰到异常代表到了结尾,通过捕获异常得到内存长度,但显然这并不是很可靠,而且时间复杂度为 O(n)。
在 redis 里利用单独分配一块区域专门用来存储内存长度,这样获取内存大小直接读取该区域的数值,时间复杂度为 O(1)。

申请内存的代码如下:

1
2
3
4
5
6
7
8
9
10
11
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;
#endif
    *((size_t *)ptr) = size;
    update_zmalloc_stat_alloc(size+PREFIX_SIZE);
    return (char *)ptr+PREFIX_SIZE;
}

如果没有使用第三方库,会单独开辟一块区域存储长度,PREFIX_SIZE在使用第三方库的情况下为 0,否则为 szieof(size_t)
zmalloc_size 函数的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
size_t zmalloc_size(void *ptr) {
#ifndef HAVE_MALLOC_SIZE
    void *realptr;
#endif
    size_t size;
    if (!ptr) return 0;
#ifdef HAVE_MALLOC_SIZE
    return zmalloc_size(ptr);
#else
    realptr = (char *)ptr-PREFIX_SIZE;
    size = *((size_t *)realptr);
    return size+PREFIX_SIZE;
#endif
}

2. 编写

对 redis 中 zmalloc 进行简略,只实现了基本的内存分配释放、tcmalloc、原子操作的支持,代码见附件。
目录结构:

1
2
3
4
5
redis - study /
├── config.h
├── test.c  
├── zmalloc.c      
└── zmalloc.h

3. 编译

gcc 编译,命令:

1
gcc -O0 -ltcmalloc -D USE_TCMALLOC=YES -otest test.c zmalloc.c zmalloc.h config.h

使用 -D 添加宏,这里测试的是 tcmalloc,原子操作开关在 config.h 中。
生成后的文件为test

4. 测试


测试 tcmalloc 的时候,需要安装 gperftools 工具包,项目地址:https://github.com/gperftools/gperftools,安装方法参考另外一篇【tcmalloc 的使用】。

5. 附件

链接:百度网盘
提取码:7hw9

6. 总结





root@kali ~# cat 重要声明
本博客所有原创文章,作者皆保留权利。转载必须包含本声明,保持文本完整,并以超链接形式注明出处【Techliu】。查看和编写文章评论都需翻墙,为了更方便地获取文章信息,可订阅RSS,如果您还没有一款喜爱的阅读器,不妨试试Inoreader.
root@kali ~# Thankyou!