当前位置: 首页 > 编程日记 > 正文

Redis源码解析——内存管理

在《Redis源码解析——源码工程结构》一文中,我们介绍了Redis可能会根据环境或用户指定选择不同的内存管理库。在linux系统中,Redis默认使用jemalloc库。当然用户可以指定使用tcmalloc或者libc的原生内存管理库。本文介绍的内容是在这些库的基础上,Redis封装的功能。(转载请指明出于breaksoftware的csdn博客)

统一函数名

首先Redis需要判断最终选择的内存管理库是否可以满足它的基础需求。比如Redis需要能够通过一个堆上分配的指针知晓其空间大小。但是并不是所有内存管理库的每个版本都有这个方法。于是对于不满足的就报错

#if defined(USE_TCMALLOC)
#define ZMALLOC_LIB ("tcmalloc-" __xstr(TC_VERSION_MAJOR) "." __xstr(TC_VERSION_MINOR))
#include <google/tcmalloc.h>
#if (TC_VERSION_MAJOR == 1 && TC_VERSION_MINOR >= 6) || (TC_VERSION_MAJOR > 1)
#define HAVE_MALLOC_SIZE 1
#define zmalloc_size(p) tc_malloc_size(p)
#else
#error "Newer version of tcmalloc required"
#endif#elif defined(USE_JEMALLOC)
#define ZMALLOC_LIB ("jemalloc-" __xstr(JEMALLOC_VERSION_MAJOR) "." __xstr(JEMALLOC_VERSION_MINOR) "." __xstr(JEMALLOC_VERSION_BUGFIX))
#include <jemalloc/jemalloc.h>
#if (JEMALLOC_VERSION_MAJOR == 2 && JEMALLOC_VERSION_MINOR >= 1) || (JEMALLOC_VERSION_MAJOR > 2)
#define HAVE_MALLOC_SIZE 1
#define zmalloc_size(p) je_malloc_usable_size(p)
#else
#error "Newer version of jemalloc required"
#endif#elif defined(__APPLE__)
#include <malloc/malloc.h>
#define HAVE_MALLOC_SIZE 1
#define zmalloc_size(p) malloc_size(p)
#endif#ifndef HAVE_MALLOC_SIZE
size_t zmalloc_size(void *ptr) {void *realptr = (char*)ptr-PREFIX_SIZE;size_t size = *((size_t*)realptr);/* Assume at least that all the allocations are padded at sizeof(long) by* the underlying allocator. */if (size&(sizeof(long)-1)) size += sizeof(long)-(size&(sizeof(long)-1));return size+PREFIX_SIZE;
}
#endif

上面这段代码除了判断内存库的支持能力,还顺带统一zmalloc_size方法的实现。其实需要统一的方法不止这一个。比如libc的malloc方法在jemalloc中叫做je_malloc,而在tcmalloc中叫tc_malloc。这些基础方法并不多,它们分别是单片内存分配的malloc方法、多片内存分配calloc方法、内存重分配的realloc方法和内存释放函数free。经过统一命令后,之后使用这些方法的地方就不用考虑基础库不同的问题了。

#if defined(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)
#elif defined(USE_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)
#endif

记录堆空间申请大小

Redis内存管理模块需要实时知道已经申请了多少空间,它通过一个全局变量保存:

static size_t used_memory = 0;

由于内存分配可能发生在各个线程中,所以对这个数据的管理要做到原子性。但是不同平台原子性操作的方法不同,有的甚至不支持原子操作,这个时候Redis就要统一它们的行为

pthread_mutex_t used_memory_mutex = PTHREAD_MUTEX_INITIALIZER;
……
#if defined(__ATOMIC_RELAXED)
#define update_zmalloc_stat_add(__n) __atomic_add_fetch(&used_memory, (__n), __ATOMIC_RELAXED)
#define update_zmalloc_stat_sub(__n) __atomic_sub_fetch(&used_memory, (__n), __ATOMIC_RELAXED)
#elif defined(HAVE_ATOMIC)
#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))
#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)#define update_zmalloc_stat_sub(__n) do { \pthread_mutex_lock(&used_memory_mutex); \used_memory -= (__n); \pthread_mutex_unlock(&used_memory_mutex); \
} while(0)#endif

一般来说,锁操作比原子操作慢。但是在不支持原子操作的系统上只能使用锁机制了。

但是作为一个基础库,它不能仅仅考虑到多线程的问题。比如用户系统上不支持原子操作,而用户也不希望拥有多线程安全特性(可能它只有一个线程在运行),那么上述接口在计算时就必须使用锁机制,这样对于性能有苛刻要求的场景是不能接受的。于是Redis暴露了一个方法用于让用户指定是否需要启用线程安全特性

static int zmalloc_thread_safe = 0;void zmalloc_enable_thread_safeness(void) {zmalloc_thread_safe = 1;
}

相应的,线程安全的方法update_zmalloc_stat_add和update_zmalloc_stat_free需要被封装,以满足不同模式:

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

之后我们在堆上分配释放空间时,就需要使用update_zmalloc_stat_alloc和update_zmalloc_stat_free方法实时更新堆空间申请的情况。而获取其值则需要下面的方法:

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);
#elsepthread_mutex_lock(&used_memory_mutex);um = used_memory;pthread_mutex_unlock(&used_memory_mutex);
#endif}else {um = used_memory;}return um;
}

内存分配和释放

之前我们讲过,Redis的内存分配库需要底层库支持通过堆上指针获取该空间大小的功能,但是一些低版本的内存管理库并不支持。针对这种场景Redis还是做了兼容,它设计的内存结构是Header+Body。在Header中保存了该堆空间Body的大小信息,而Body则用于返回给内存申请者。我们看下malloc的例子:

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
}

一开始时,zmalloc直接分配了一个比申请空间大的空间,这就意味着无论是否支持获取申请空间大小的内存库,它都一视同仁了——实际申请比用户要求大一点。

如果内存库支持,则通过zmalloc_size获取刚分配的空间大小,并累计到记录整个程序申请的堆空间大小上,然后返回申请了的地址。此时虽然用户申请的只是size的大小,但是实际给了size+PREFIX_SIZE的大小。

如果内存库不支持,则在申请的内存前sizeof(size_t)大小的空间里保存用户需要申请的空间大小size。累计到记录整个程序申请堆空间大小上的也是实际申请的大小。最后返回的是偏移了头大小的内存地址。此时用户拿到的空间就是自己要求申请的空间大小。

多片分配空间的zcalloc函数实现也是类似的,稍微有点区别的是重新分配空间的zrealloc方法,它需要在统计程序以申请堆空间大小的数据上减去以前该块的大小,再加上新申请的空间大小

void *zrealloc(void *ptr, size_t size) {
#ifndef HAVE_MALLOC_SIZEvoid *realptr;
#endifsize_t oldsize;void *newptr;if (ptr == NULL) return zmalloc(size);
#ifdef HAVE_MALLOC_SIZEoldsize = 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;
#elserealptr = (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
}

还有就是zfree函数的实现,它需要释放的空间起始地址要视库的支持能力决定。如果库不支持获取区块大小,则需要将传入的指针前移PREFIX_SIZE,然后释放该起始地址的空间。

void zfree(void *ptr) {
#ifndef HAVE_MALLOC_SIZEvoid *realptr;size_t oldsize;
#endifif (ptr == NULL) return;
#ifdef HAVE_MALLOC_SIZEupdate_zmalloc_stat_free(zmalloc_size(ptr));free(ptr);
#elserealptr = (char*)ptr-PREFIX_SIZE;oldsize = *((size_t*)realptr);update_zmalloc_stat_free(oldsize+PREFIX_SIZE);free(realptr);
#endif
}

最后我们看下Redis在内存分配时处理内存溢出的处理。它提供了一个接口,让用户处理内存溢出问题。当然它也有自己默认的处理逻辑:

static void (*zmalloc_oom_handler)(size_t) = zmalloc_default_oom;static void zmalloc_default_oom(size_t size) {fprintf(stderr, "zmalloc: Out of memory trying to allocate %zu bytes\n",size);fflush(stderr);abort();
}void zmalloc_set_oom_handler(void (*oom_handler)(size_t)) {zmalloc_oom_handler = oom_handler;
}

获取进程内存信息

Redis不仅在代码层面要统计已申请的堆空间,还要通过其他方法获取本进程中一些内存信息。比如它要通过zmalloc_get_rss方法获取当前进程的实际使用物理内存。这个也要按系统支持来区分实现,比如支持/proc/%pid%/stat的使用:

size_t zmalloc_get_rss(void) {int page = sysconf(_SC_PAGESIZE);size_t rss;char buf[4096];char filename[256];int fd, count;char *p, *x;snprintf(filename,256,"/proc/%d/stat",getpid());if ((fd = open(filename,O_RDONLY)) == -1) return 0;if (read(fd,buf,4096) <= 0) {close(fd);return 0;}close(fd);p = buf;count = 23; /* RSS is the 24th field in /proc/<pid>/stat */while(p && count--) {p = strchr(p,' ');if (p) p++;}if (!p) return 0;x = strchr(p,' ');if (!x) return 0;*x = '\0';rss = strtoll(p,NULL,10);rss *= page;return rss;
}

如果支持使用task_for_pid方法的则使用:

size_t zmalloc_get_rss(void) {task_t task = MACH_PORT_NULL;struct task_basic_info t_info;mach_msg_type_number_t t_info_count = TASK_BASIC_INFO_COUNT;if (task_for_pid(current_task(), getpid(), &task) != KERN_SUCCESS)return 0;task_info(task, TASK_BASIC_INFO, (task_info_t)&t_info, &t_info_count);return t_info.resident_size;
}

获取完物理内存数据后,可以通过和累计的分配内存大小相除,算出内存使用效率:

float zmalloc_get_fragmentation_ratio(size_t rss) {return (float)rss/zmalloc_used_memory();
}

Redis源码说明上指出上述获取RSS信息的方法是不高效的。可以通过RedisEstimateRSS()方法高效获取。
        除了上面这些方法,Redis还有获取已被修改的私有页面大小函数zmalloc_get_private_dirty以及获取物理内存((RAM))大小的zmalloc_get_memory_size方法。这些方法都是些系统性方法,我就不在这儿做说明了。

相关文章:

poj_2479 动态规划

题目大意 给定一列数&#xff0c;从中选择两个不相交的连续子段&#xff0c;求这两个连续子段和的最大值。 题目分析 典型的M子段和的问题&#xff0c;使用动态规划的方法来解决。 f[i][j] 表示将A[1...i] 划分为j个不相交连续子串&#xff0c;且A[j]属于第i个子串&#xff0c;…

Redis源码解析——字典结构

C语言中有标准的字典库&#xff0c;我们可以通过pair(key,value)的形式存储数据。但是C语言中没有这种的库&#xff0c;于是就需要自己实现。本文讲解的就是Redis源码中的字典库的实现方法。&#xff08;转载请指明出于breaksoftware的csdn博客&#xff09; 一般情况下&#xf…

十步,教你把Python运行速度提升 30%

作者 | Martin Heinz译者 | 陆离编辑 | Jane出品 | AI科技大本营&#xff08;ID&#xff1a;rgznai100&#xff09;【导读】一直以来&#xff0c;诟病 Python语言的人经常说&#xff0c;他们不想使用的一个原因是 Python 的速度太慢了。不管使用哪一种编程语言&#xff0c;程序…

字符串转换成NSDate类型的 为nil解决方法

方法一 通过下列函数来解决 但是得到的日期会改变 修改方法fix- (NSDate *)timeForString:(NSString *)string {NSMutableString *timeString [[NSMutableString alloc] initWithString:string]; [timeString setString:[timeString stringByReplacingOccurrence…

Redis源码解析——字典基本操作

有了《Redis源码解析——字典结构》的基础&#xff0c;我们便可以对dict的实现进行展开分析。&#xff08;转载请指明出于breaksoftware的csdn博客&#xff09; 创建字典 一般字典创建时&#xff0c;都是没有数据的&#xff0c;但是字典类型需要确定&#xff0c;所以我们看到R…

[转]控制 Cookie 的作用范围

默认时&#xff0c;网站的所有 Cookies 都一起被存储在客户端&#xff0c;并且所有 Cookies 连同网站的任何请求一起被发送到服务器。换句话说&#xff0c;网站中的每个页面都能够为网站获取所有的 Cookies。但是&#xff0c;你能够通过两个方式来设置 Cookies 的作用范围&…

强化学习70年演进:从精确动态规划到基于模型

作者 | Nathan Lambert译者 | 泓礼编辑 | 夕颜出品 | AI科技大本营&#xff08;ID: rgznai100&#xff09;【导读】这是一份帮你了解强化学习算法本质的资源&#xff0c;无需浏览大量文档&#xff0c;没有一条公式&#xff0c;非常适合学生和研究人员阅读。作为强化学习研究人员…

Android ActionBar相关

1.Android 5.0 删除ActionBar下面的阴影 于Android 5.0假设你发现的ActionBar下面出现了阴影&#xff0c;例如&#xff0c;下面的设置&#xff0c;以消除阴影&#xff1a; getActionBar().setElevation(0); Android 5.0之前能够用以下代码消除阴影&#xff1a; <item name&q…

Redis源码解析——字典遍历

之前两篇博文讲解了字典库的基础&#xff0c;本文将讲解其遍历操作。之所以将遍历操作独立成一文来讲&#xff0c;是因为其中的内容和之前的基本操作还是有区别的。特别是高级遍历一节介绍的内容&#xff0c;充满了精妙设计的算法智慧。&#xff08;转载请指明出于breaksoftwar…

开发者在行动!中国防疫开源项目登上GitHub TOP榜

整理 | 唐小引出品 | CSDN&#xff08;ID&#xff1a;CSDNnews&#xff09;【导读】用开发者们的方式支援这场没有硝烟的战争&#xff01;截止北京时间 1 月 28 日下午 15:47&#xff0c;全国确诊新型冠状病毒的数字已经到达了 4586 例&#xff0c;疑似高达 6973 例&#xff0c…

像童话一样学习OSPF原理

可以把整个网络&#xff08;一个自治系统AS&#xff09;看成一个王国&#xff0c;这个王国可以分成几个 区(area)&#xff0c;现在我们来看看区域内的某一个人(你所在的机器root)是怎样得到一张 世界地图(routing table)的。   首先&#xff0c;你得跟你周围的人&#xff08;…

队列——PowerShell版

继续读啊哈磊《啊哈&#xff01;算法》感悟系列——队列 地铁售票处排队&#xff0c;先来的人先到队首先买完先走&#xff0c;后来的人排在队尾等候后买完后走。 想买票&#xff0c;必须排在队尾&#xff1b;买完票&#xff0c;只能从队首离开。 这种先进先出&#xff08;First…

Redis源码解析——双向链表

相对于之前介绍的字典和SDS字符串库&#xff0c;Redis的双向链表库则是非常标准的、教科书般简单的库。但是作为Redis源码的一部分&#xff0c;我决定还是要讲一讲的。&#xff08;转载请指明出于breaksoftware的csdn博客&#xff09; 基本结构 首先我们看链表元素的结构。因为…

12月第三周安全要闻回顾:浏览器安全不容忽视,SSL弱点影响网站安全

本周&#xff08;081215至081221&#xff09;安全方面的新闻众多&#xff0c;主要集中在***与威胁趋势方面。浏览器安全方向波澜起伏&#xff0c;微软推出了针对上周公开的IE7新漏洞的紧急安全补丁&#xff0c;但目前互联网上针对该漏洞的大规模***仍在继续&#xff0c;******的…

GPT2文本生成有问题?这里有些潜在解决思路

作者 | Leo Gao译者 | 凯隐编辑 | 夕颜出品 | AI科技大本营&#xff08;ID: rgznai100&#xff09; 【导读】在过去的一年中&#xff0c;人们对文本生成模型的兴趣重新燃起&#xff0c;这在很大程度上要归功于GPT2&#xff0c;它主要展示了使用更大模型、更大数据和更大计算量的…

HTML5学习之二:HTML5中的表单2

&#xff08;本内容部分节选自《HTML 5从入门到精通》) 对表单的验证 ———————————————————————————————————————————————————————— •1、required属性 required属性主要目的是确保表单控件中的值已填写。在提交时&…

Redis源码解析——有序整数集

有序整数集是Redis源码中一个以大尾&#xff08;big endian&#xff09;形式存储&#xff0c;由小到大排列且无重复的整型集合。它存储的类型包括16位、32位和64位的整型数。在介绍这个库的实现前&#xff0c;我们还需要先熟悉下大小尾内存存储机制。&#xff08;转载请指明出于…

GitHub标星1.2w+,Chrome最天秀的插件都在这里

作者 | Rocky0429来源 | Python空间&#xff08;ID: Devtogether&#xff09;大家好&#xff0c;我是 Rocky0429&#xff0c;一个沉迷 Chrome 不能自拔的蒟蒻...作为一个在远古时代用过什么 IE、360、猎豹等浏览器的资深器哥&#xff0c;当我第一次了解 Chrome 的时候&#xff…

基础篇 第四节 项目进度计划编辑 之 任务关联性设定

1.任务关联性的类型 ◎完成 —— 开始 FS ◎开始 —— 开始 SS ◎开始 —— 完成 SF 完成 —— 完成 FF 2.设定任务关联性 三种方法&#xff1a; ◎在条形图中直接拖拽 ◎在“前置任务”列中编辑 ◎在“任务信息”中的“前置任务”选项卡中编辑 3.设定“延隔时间” 延隔时间小于…

开坑,写点Polymer 1.0 教程第3篇——组件注册与创建

之前一篇算是带大家大致领略了一下Polymer的风采。这篇我们稍微深入一丢丢&#xff0c;讲下组件的注册和创建。 创建自定义组件的几种方式 这里我们使用Polymer函数注册了一个自定义组件"my-element" // register an element Polymer({is: my-element,created: funct…

Redis源码解析——Zipmap

本文介绍的是Redis中Zipmap的原理和实现。&#xff08;转载请指明出于breaksoftware的csdn博客&#xff09; 基础结构 Zipmap是为了实现保存Pair(String,String)数据的结构&#xff0c;该结构包含一个头信息、一系列字符串对&#xff08;之后把一个“字符串对”称为一个“元素…

IIS7入门之旅:(3)CGI application和FastCGI application的区别

前言&#xff1a; 一如既往地&#xff0c;IIS支持通过提供pluggable module来提供对第3方script的支持,例如php等。在IIS7中&#xff0c;对于CGI的支持有了一个新的变化&#xff0c;就是同时提供了2种CGI支持模块&#xff0c;分别为&#xff1a;CGIModule (cgi.dll)和FastCGIMo…

抗击疫情!阿里云为加速新药疫苗研发提供免费AI算力

1月29日&#xff0c;阿里云正式宣布&#xff1a;疫情期间&#xff0c;向全球公共科研机构免费开放一切AI算力&#xff0c;以加速本次新型肺炎新药和疫苗研发。 目前&#xff0c;中国疾控中心已成功分离病毒&#xff0c;疫苗研发和药物筛选仍在争分夺秒地进行。新药和疫苗研发期…

SpriteBuilder中如何平均拉伸精灵帧动画的距离

首先要在Timeline中选中所有的精灵帧&#xff0c;可以通过如下2种的任意一种办法达成&#xff1a; 1按下Shift键的同时鼠标单击它们 2鼠标在Timeline空白区拖拽直到拉出的矩形包围住所有精灵帧方块后放开鼠标。 你可以通过查看Timeline中精灵帧方块上是否有阴影来辨识是否选中…

C++拾趣——类构造函数的隐式转换

之前看过一些批判C的文章&#xff0c;大致意思是它包含了太多的“奇技淫巧”&#xff0c;并不是一门好的语言。我对这个“奇技淫巧”的描述颇感兴趣&#xff0c;因为按照批判者的说法&#xff0c;C的一些特性恰巧可以让一些炫耀技术的同学有了炫耀的资本——毕竟路人皆知的东西…

数十名工程师作战5天,阿里达摩院连夜研发智能疫情机器人

作者 | Just出品 | AI科技大本营&#xff08;ID:rgznai100&#xff09;新型肺炎疫情防控战在各大互联网科技公司拉响&#xff0c;阿里、百度等公司陆续对外提供相应技术和产品。当前&#xff0c;疫情当前防控一线人员紧缺&#xff0c;多地政务热线迎来大波问询市民&#xff0c;…

路由器互联端口处于不同网段的路由方法和原理

如下图&#xff1a;两台cisco路由器相连接的两个端口不在同一个网络&#xff0c;如何实现两台路由器的互联&#xff1f;初看似乎绝对不可能&#xff0c;但是这是可行的&#xff0c;而且我已经把这个变成了现实。这里讲述配置的方法&#xff0c;以及解释原理。这个就要讲到路由原…

上网行为管理产品选型简单考量

信息化浪潮汹涌向前&#xff0c;人们的生活、工作、学习越来越离不开IT&#xff0c;离不开网络。 对于很多组织来讲&#xff0c;网络就意味着效率、甚至生产力&#xff0c;协同办公、决策、科研、资金划拨等等都对网络有了前所未有的依赖。网络应用日益复杂、多变、动态特征发展…

码农技术炒股之路——配置管理器、日志管理器

配置管理器和日志管理器是项目中最为独立的模块。我们可以很方便将其剥离出来供其他Python工程使用。文件的重点将是介绍Python单例和logging模块的使用。&#xff08;转载请指明出于breaksoftware的csdn博客&#xff09; 配置管理器 在《码农技术炒股之路——架构和设计》中我…