Redis源码剖析--内存分配

Posted ZeeCoder

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Redis源码剖析--内存分配相关的知识,希望对你有一定的参考价值。

请持续关注我的个人博客:https://zcheng.ren

深受侯捷老师的《STL源码剖析》一书的影响,在该书中开篇就对STL的空间配置器进行了一个详尽的介绍。以应用的角度而言,空间配置器是最不需要介绍的,它总是隐藏在一切组件的背后;可是,就源码分析而言,空间配置是最为关键的,也是分析源码之路的第一步!Redis在内存分配方面,仅仅是对系统的malloc/free做了一层简单的封装,然后加上了异常处理功能和内存统计功能。其实现主要在zmalloc.c和zmalloc.h文件中。

功能函数总览

在zmalloc.h中,定义了Redis内存分配的主要功能函数,这些函数基本上实现了Redis内存申请,释放和统计等功能,其函数声明如下:

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);  // 调用zfree释放内存空间
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); // 获取RSS信息(Resident Set Size)
size_t zmalloc_get_private_dirty(void); // 获得实际内存大小
size_t zmalloc_get_smap_bytes_by_field(char *field); // 获取/proc/self/smaps字段的字节数
size_t zmalloc_get_memory_size(void); // 获取物理内存大小
void zlibc_free(void *ptr); // 原始系统free释放方法

另外,我们还要注意到zmalloc.c中的几个变量和概念,

static size_t used_memory = 0;  // 已使用内存的大小
static int zmalloc_thread_safe = 0; // 线程安全模式状态
pthread_mutex_t used_memory_mutex = PTHREAD_MUTEX_INITIALIZER; // 为此服务器

接下来,我分几个章节来一一剖析zmalloc.c中的函数实现。

内存管理函数

内存申请函数zmalloc

Redis的内存申请函数zmalloc本质就是调用了系统的malloc函数,然后对其进行了适当的封装,加上了异常处理函数和内存统计。其源代码如下:

void *zmalloc(size_t size) 
    // 调用malloc函数进行内存申请
    // 多申请的PREFIX_SIZE大小的内存用于记录该段内存的大小
    void *ptr = malloc(size+PREFIX_SIZE);

    // 如果ptr为NULL,则调用异常处理函数
    if (!ptr) zmalloc_oom_handler(size);
    // 以下是内存统计
    *((size_t*)ptr) = size;
    update_zmalloc_stat_alloc(size+PREFIX_SIZE); // 更新used_memory的值
    return (char*)ptr+PREFIX_SIZE;

上述代码中的PREFIX_SIZE解释:由于malloc函数申请的内存不会标识内存块的大小,而我们需要统计内存大小,所以需要在多申请PREFIX_SIZE大小的内存,用于存放该大小。

其中,异常处理函数如下:

static void zmalloc_default_oom(size_t size) 
    fprintf(stderr, "zmalloc: Out of memory trying to allocate %zu bytes\\n", // 打印输出日志
        size);
    fflush(stderr);
    abort(); // 中断退出

更新used_memory值得函数以宏定义给出,其代码和注释如下:

#define update_zmalloc_stat_alloc(__n) do  \\
    size_t _n = (__n); \\
    if (_n&(sizeof(long)-1)) _n += sizeof(long)-(_n&(sizeof(long)-1)); \\   // 将_n调整为sizeof(long)的整数倍
    if (zmalloc_thread_safe)  \\ // 如果启用了线程安全模式
        update_zmalloc_stat_add(_n); \\   // 调用原子操作加(+)来更新已用内存
     else  \\
        used_memory += _n; \\   // 不考虑线程安全,则直接更新已用内存
     \\
 while(0)

在上述函数中,又用到了原子加操作,其代码和注释如下:

// __atomic_add_fetch是C++11特性中提供的原子加操作
#if defined(__ATOMIC_RELAXED)
#define update_zmalloc_stat_add(__n) __atomic_add_fetch(&used_memory, (__n), __ATOMIC_RELAXED)
// 如果不支持C++11,则调用GCC提供的原子加操作
#elif defined(HAVE_ATOMIC)
#define update_zmalloc_stat_add(__n) __sync_add_and_fetch(&used_memory, (__n))
// 如果上述都没有,则只能采用加锁操作
#else
#define update_zmalloc_stat_add(__n) do  \\
    pthread_mutex_lock(&used_memory_mutex); \\
    used_memory += (__n); \\
    pthread_mutex_unlock(&used_memory_mutex); \\
 while(0)

内存申请函数zcalloc

与malloc一样,zcalloc调用的是系统给的calloc()来申请内存。

void *zcalloc(size_t size) 
    void *ptr = calloc(1, size+PREFIX_SIZE);
    // 异常处理函数
    if (!ptr) zmalloc_oom_handler(size);
    // 内存统计函数
    *((size_t*)ptr) = size;
    update_zmalloc_stat_alloc(size+PREFIX_SIZE);
    return (char*)ptr+PREFIX_SIZE;

内存调整函数zrecalloc

Redis定义的zrecalloc用于调整已申请内存的大小,其本质也是直接调用系统函数recalloc()

void *zrealloc(void *ptr, size_t size) 
    size_t oldsize;
    void *newptr;
    // 为空直接退出
    if (ptr == NULL) return zmalloc(size);
    // 找到内存真正的起始位置
    realptr = (char*)ptr-PREFIX_SIZE;
    oldsize = *((size_t*)realptr);
    // 调用recalloc函数
    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;

内存释放函数

与内存申请函数调用malloc一样,内存释放也是调用系统的free()函数来实现内存释放

void zfree(void *ptr) 
    if (ptr == NULL) return;  // 为空直接返回
    realptr = (char*)ptr-PREFIX_SIZE; // 找到该段内存真正的起始位置
    oldsize = *((size_t*)realptr);
    update_zmalloc_stat_free(oldsize+PREFIX_SIZE);// 更新use_memory函数
    free(realptr); // 调用系统的内存释放函数

其中,内存状态统计函数的代码实现如下:

#define update_zmalloc_stat_free(__n) do  \\
    size_t _n = (__n); \\ 
    if (_n&(sizeof(long)-1)) _n += sizeof(long)-(_n&(sizeof(long)-1)); \\  // 将内存大小调整为sizeof(long)的整数倍
    if (zmalloc_thread_safe)  \\  // 如果开启了线程安全模式
        update_zmalloc_stat_sub(_n); \\ // 更新use_memory值(与上述的update_zmalloc_stat_add这里就不赘述了)
     else  \\
        used_memory -= _n; \\ // 没有线程安全则直接减
     \\
 while(0)

讲到这里,Redis基本的内存处理函数已经分析完毕了。另外,Redis还提供了一下辅助函数,我这里简要的分析一下。

辅助函数

字符串复制方法

暂时不清楚这个函数为什么要放在这里。

char *zstrdup(const char *s) 
    size_t l = strlen(s)+1; 
    char *p = zmalloc(l); // 开辟一段新内存

    memcpy(p,s,l); // 调用字符串复制函数
    return p;

设置异常处理函数

Redis允许自行设定异常处理函数,也提供了如下的函数。

void zmalloc_set_oom_handler(void (*oom_handler)(size_t)) 
    zmalloc_oom_handler = oom_handler; // 绑定自定义的异常处理函数

开启线程安全

void zmalloc_enable_thread_safeness(void) 
    zmalloc_thread_safe = 1;  // 此参数用来控制是否开启线程安全

获取已使用内存

size_t zmalloc_used_memory(void) 
    size_t um;
    // 如果开启了线程安全模式
    if (zmalloc_thread_safe) 
#if defined(__ATOMIC_RELAXED) || defined(HAVE_ATOMIC)
        um = update_zmalloc_stat_add(0);
#else
        pthread_mutex_lock(&used_memory_mutex);
        um = used_memory;
        pthread_mutex_unlock(&used_memory_mutex);
#endif
    
    else 
        um = used_memory; // 未开启则直接使用used_memory
    

    return um;

到此,Redis的内存管理函数已分析完。
end…

以上是关于Redis源码剖析--内存分配的主要内容,如果未能解决你的问题,请参考以下文章

STL源码剖析

vector源码2(参考STL源码--侯捷)

剖析c语言动态内存管理

智能指针之atuo_ptr源码剖析

vector源码(参考STL源码--侯捷)-----空间分配导致迭代器失效

[侯捷 C++内存管理] 标准分配器实现