Redis源码分析(1)内存管理

Redis在zmalloc.h和zmalloc.c实现了底层的内存管理,代码很简洁。
Redis的内存管理提供了以下几个函数:

1 2// 申请内存,封装malloc 3void *zmalloc(size_t size); 4// 申请内存并初始化为0,封装calloc 5void *zcalloc(size_t size); 6// 调整内存大小,封装realloc 7void *zrealloc(void *ptr, size_t size); 8// 释放内存,封装free 9void zfree(void *ptr); 10// 拷贝字符串 11char *zstrdup(const char *s); 12/***********************以下都是用于控制程序内存*******************************/ 13// 获取已使用的内存大小(猜测用于内存管理LRU等) 14size_t zmalloc_used_memory(void); 15// 启用线程安全 16void zmalloc_enable_thread_safeness(void); 17// 设置处理out-of-memory的函数 18void zmalloc_set_oom_handler(void (*oom_handler)(size_t)); 19// 获取碎片化比例,rss:是常驻内存集(Resident Set Size),表示该进程分配的内存大小 20float zmalloc_get_fragmentation_ratio(size_t rss); 21// 获取程序占用的内存(常驻内存集Resident Set Size),除了程序分配的内存,还包括进程运行本身需要的内存、内存碎片等,但是不包括虚拟内存 22size_t zmalloc_get_rss(void); 23 24size_t zmalloc_get_private_dirty(void); 25size_t zmalloc_get_smap_bytes_by_field(char *field); 26void zlibc_free(void *ptr); 27WIN32_ONLY(void zmalloc_free_used_memory_mutex(void);) 28 29// 获取指定指针申请的内存空间大小 30size_t zmalloc_size(void *ptr); 31 32

要分析redis的内存管理,先看内存是如何申请的:

1// 申请一段内存空间 2void *zmalloc(size_t size) { 3 // 申请内存,多申请了PREFIX_SIZE,PREFIX_SIZE大小是sizeof(size_t) 4 void *ptr = malloc(size+PREFIX_SIZE); 5 6 // 判断申请是否成功,当内存不足时将申请失败 7 if (!ptr) zmalloc_oom_handler(size); 8#ifdef HAVE_MALLOC_SIZE 9 // 更新当前内存统计数 10 update_zmalloc_stat_alloc(zmalloc_size(ptr)); 11 return ptr; 12#else 13 // 将长度写入头部,前PREFIX_SIZE个byte用于存储申请的内存大小 14 *((size_t*)ptr) = size; 15 // 更新内存使用量统计,添加使用内存量计数 16 update_zmalloc_stat_alloc(size+PREFIX_SIZE); 17 // 返回内存数据区起始地址 18 return (char*)ptr+PREFIX_SIZE; 19#endif 20} 21 22

redis使用zmalloc申请的内存结构如下:


|  数据区长度 |       数据区     |

↑         ↑
实际头地址    返回的指针地址
在这里插入图片描述
redis通过**update_zmalloc_stat_alloc、update_zmalloc_stat_free、**这个函数调整当前内存使用量的统计:

1#define update_zmalloc_stat_add(__n) do { \ 2 pthread_mutex_lock(&used_memory_mutex); \ 3 used_memory += (__n); \ 4 pthread_mutex_unlock(&used_memory_mutex); \ 5} while(0) 6 7#define update_zmalloc_stat_sub(__n) do { \ 8 pthread_mutex_lock(&used_memory_mutex); \ 9 used_memory -= (__n); \ 10 pthread_mutex_unlock(&used_memory_mutex); \ 11} while(0) 12 13#define update_zmalloc_stat_alloc(__n) do { \ 14 size_t _n = (__n); \ 15 if (_n&(sizeof(PORT_LONG)-1)) _n += sizeof(PORT_LONG)-(_n&(sizeof(PORT_LONG)-1)); \ 16 if (zmalloc_thread_safe) { \ 17 update_zmalloc_stat_add(_n); \ 18 } else { \ 19 used_memory += _n; \ 20 } \ 21} while(0) 22 23#define update_zmalloc_stat_free(__n) do { \ 24 size_t _n = (__n); \ 25 if (_n&(sizeof(PORT_LONG)-1)) _n += sizeof(PORT_LONG)-(_n&(sizeof(PORT_LONG)-1)); \ 26 if (zmalloc_thread_safe) { \ 27 update_zmalloc_stat_sub(_n); \ 28 } else { \ 29 used_memory -= _n; \ 30 } \ 31} while(0) 32 33static size_t used_memory = 0; 34 35

这个有个要点,计算机是如何分配内存的?例如我们用malloc(1)申请一个byte的内存,会占用多大的空间?

在64位系统,如果申请内存为1~24字节,系统内存消耗32字节,当申请25字节的内存时,系统内存消耗48字节。而对于32位系统,申请内存为1~12字节时,系统内存消耗为16字节,当申请内存为13字节时,系统内存消耗为24字节。(这段在linux下适用,是实际占用的内存,可用malloc_usable_size测试,但与下文的无关)

redis给了一个计算申请内存大小的公式:

1#define PORT_LONG size_t 2// 获取使用的内存大小,将n调整为sizeof(PORT_LONG)的整数倍 3int get_memory_size(size_t _n) 4{ 5 if (_n & (sizeof(PORT_LONG) - 1)) 6 _n += sizeof(PORT_LONG) - (_n & (sizeof(PORT_LONG) - 1)); 7 return _n; 8} 9 10 11

redis是如何获取申请的内存大小的:

1size_t zmalloc_used_memory(void) { 2 size_t um; 3 4 if (zmalloc_thread_safe) { 5#if defined(__ATOMIC_RELAXED) || defined(HAVE_ATOMIC) 6 um = update_zmalloc_stat_add(0); 7#else 8 pthread_mutex_lock(&used_memory_mutex); 9 um = used_memory; 10 pthread_mutex_unlock(&used_memory_mutex); 11#endif 12 } 13 else { 14 um = used_memory; 15 } 16 17 return um; 18} 19 20

used_memory是静态变量,用于保存申请的内存大小,每次内存调整都会修改其值。

redis通过封装malloc、zalloc、realloc、free实现了自己的内存管理,主要是定义了自己的内存结构并可以统计内存使用量。

代码交流 2021