阅读Redis6.0源码之内存管理篇

Redis的内存管理是由zmalloc.h和zmalloc.c实现的,编译时会先判断是否使用tcmalloc(google开发的内存分配器,全称Thread-Caching Malloc,即线程缓存的malloc,实现了高效的多线程内存管理)和jemalloc(一种内存分配器,与其它内存分配器相比,它最大的优势在于多线程情况下的高性能以及内存碎片的减少),然后判断是否为Mac系统,如果上面三者都无法使用的话才会使用标准库libc。默认情况下Redis6.0使用的是jemalloc,关于jemalloc, 这个博文讲的很好。

zmalloc.h重要代码

图片说明
选择的内存管理的方式不同,相应的zmalloc_size(p)这个宏(用来获得实际从堆中分配的内存字节数)也有不同的定义。

zmalloc.c的重要代码

  • 首先需要注意的是根据选择的内存管理器,会将malloc、free等函数"重写"为管理器的实现版本。比如Redis默认使用的是jemalloc,就会执行接下来这一段宏定义:

    #define malloc(size) je_malloc(size)
    #define calloc(count,size) je_calloc(count,size)
    #define realloc(ptr,size) je_realloc(ptr,size)
    #define free(ptr) je_free(ptr)
    #define mallocx(size,flags) je_mallocx(size,flags)
    #define dallocx(ptr,flags) je_dallocx(ptr,flags)
  • 从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
    }

    PREFIX_SIZE的大小如果定义了HAVE_MALLOC_SIZE为0,否则大小为sizeof(size_t)或sizeof(long long)。当内存分配失败时调用zmalloc_oom_handler函数,默认实现就是打印错误信息并终止程序,和我们平常对内存分配失败的处理方式类似。
    重头戏是接下来的几行代码,如果定义了HAVE_MALLOC_SIZE(注意在Redis3.x版本里如果使用的是libc,是不会定义HAVE_MALLOC_SIZE的!zmalloc_size也需要在zmalloc.c中定义。而在6.0版本里如果使用libc是会定义HAVE_MALLOC_SIZE的,并且也提供了malloc_usable_size函数作为zmalloc_size的实现!估计这也是标准库不断进化的结果吧),执行updata_zmalloc_stat_alloc用来更新Redis已使用的内存字节数(used_memory),传入的参数是实际从堆中分配的内存字节数(使用之前提到的zmalloc_size)。
    updata_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)); \
     atomicIncr(used_memory,__n); \
    } while(0)

    因为在64位Linux系统中,sizeof(long)=8,所以if语句可以简化为: if(_n&7) _n += 8 - _n&7;
    这段代码主要是判断分配的内存空间大小是否为8的倍数(使用位运算而不是_n%8,效率更高),不是的话就加上相应的偏移量使之变成8的倍数。因为malloc分配的内存都是8字节对齐的,所以如果你要分配的内存不是8的倍数,malloc就会多分配一点来凑成8的倍数,这样使得used_memory能够正确的维护已分配内存的大小。这里使用的atomicIncr是在atomicvar.h中定义的,确保变量的增、减操作是线程安全的,实现原理为如果系统提供了原子(atomic)或同步(sync)的机制,使用之,否则就使用互斥锁。
    没有定义HAVE_MALLOC_SIZE的情况,简单的说就是多分配PREFIX_SIZE的空间,并在已分配的内存头部存放上层申请的内存大小(即参数size),再进行内存对齐。在我看来,这一段代码其实已经没有太多的用武之地了,因为即使使用的是libc库现在也已经定义了HAVE_MALLOC_SIZE。
    zcalloc和zrealloc的实现比较简单,代码如下:

    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
    }
    void *zrealloc(void *ptr, size_t size) {
    #ifndef HAVE_MALLOC_SIZE
     void *realptr;
    #endif
     size_t oldsize;
     void *newptr;
    
     if (size == 0 && ptr != NULL) {
         zfree(ptr);
         return NULL;
     }
     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+PREFIX_SIZE);
     update_zmalloc_stat_alloc(size+PREFIX_SIZE);
     return (char*)newptr+PREFIX_SIZE;
    #endif
    }
  • 再看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
    }

    对于定义了HAVE_MALLOC_SIZE,调用update_zmalloc_stat_free,也是用来更新已分配的内存字节数的,实现为:

    #define update_zmalloc_stat_free(__n) do { \
     size_t _n = (__n); \
     if (_n&(sizeof(long)-1)) _n += sizeof(long)-(_n&(sizeof(long)-1)); \
     atomicDecr(used_memory,__n); \
    } while(0)

    其实也是8字节对齐后,再调用atomicDecr线程安全的减少已使用的内存字节数。

  • 关于内存分配和释放的内容大概就是这些,但在zmalloc.c中还实现了一些其它比较有用的函数。
    复制字符串函数zstrdup

    char *zstrdup(const char *s) {
     size_t l = strlen(s)+1;
     char *p = zmalloc(l);
    
     memcpy(p,s,l);
     return p;
    }

    获得已使用内存字节数zmalloc_used_memory

    size_t zmalloc_used_memory(void) {
     size_t um;
     atomicGet(used_memory,um);
     return um;
    }

    设置内存分配失败处理函数zmalloc_set_oom_handler

    void zmalloc_set_oom_handler(void (*oom_handler)(size_t)) {
     zmalloc_oom_handler = oom_handler;
    }

总结

Redis可以使用4种内存分配器(tcmalloc、jemalloc、Mac、libc),默认使用jemalloc。
zmalloc、zfree除了申请、释放空间外,会将申请、释放的内存进行8字节对齐,再线程安全的更新已使用的内存字节数(used_memory)。

全部评论

相关推荐

点赞 评论 收藏
分享
在笔试的大西瓜很矫健:校招数分不用想了,这经历和学历都不够用,大厂更别想,初筛都过不了,说点不好听的小厂数分都进不去(小厂也是假数分),要两个对口实习+3个项目(或者3+2),而且要有含金量才能补一点你的学历劣势。 建议刷实习,社招找数分,校招看运气,能入行业就行,可以运营转数分
点赞 评论 收藏
分享
02-07 12:06
已编辑
华侨大学 测试开发
最近看到很多 92 的,甚至是硕士,开始往测开赛道卷,说实话有点看不懂。先把话说清楚,大厂里的测开,绝大多数时间干的还是测试的活,只是写点自动化脚本、维护测试平台、接接流水线,真正像开发一样做系统、做架构、做核心平台的测开少得可怜,基本都集中在核心提效组,而且人很少,外面进去的大概率轮不到你,我想真正干过人都清楚。很多人被洗脑了,以为测开也是开,和后端差不多,只是更简单、更轻松、还高薪。现实情况是,测开和开发的职业路径完全不一样。开发的核心是业务和系统能力,测开的核心是稳定性和覆盖率,前者是往上走,后者天花板非常明显。你可以见到很多开发转测开,但你很少见到干了几年测开还能顺利转回开发的。更现实一点说,92 的高学历如果拿来做测开,大部分时间就是在做重复性很强的杂活,这种工作对个人能力的放大效应非常弱。三年下来,你和一个双非的,甚至本科的测开差距不会太大,但你和同龄的后端、平台开发差距会非常明显。这不是努不努力的问题,是赛道问题。所谓测开简单高薪,本质上是把极少数核心测开的上限,当成了整个岗位的常态来宣传。那些工资高、技术强的测开,本身就是开发水平,只是挂了个测开的名。普通人进去,99% 做的都是项目兜底型工作,而不是你想象中的平台开发。测开不是不能做,但它绝对不是开发的平替,也不是性价比最优解。如果你是真的不想做开发,追求稳定,那测开没问题。但如果你只是觉得测开比后端容易,还能进大厂,那我劝你冷静一点,这只是在用短期安全感换长期天花板。有92的学历,如果你连测开这些重复性工作都能心甘情愿接受,那你把时间精力用在真正的开发、系统、业务深度上,回报大概率比卷测开要高得多。想清楚再下场,别被岗位名和话术带偏了,就算去个前端客户端也是随便占坑的,测开是一个坑位很少赛道,反而大面积学历下放,不用想也能知道会是什么结果,我想各位在JAVA那里已经看到了
小浪_Coding:工作只是谋生的手段 而不是相互比较和歧视
点赞 评论 收藏
分享
评论
2
1
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务