redis 7年前

从零学习redis(15)--- 源码阅读之内存分配

作者头像 刘宇帅
3756 0

redis 中内存管理没有直接使用 C 语言提供的方法而是做了一个可管理已分配内存大小及添加了自己分配策略的实现,下面从 SDS 开始去了解 redis 的内存管理实现及策略。

SDS 内存管理函数定义如下:

src/sdsalloc.h

#include "zmalloc.h"
#define s_malloc zmalloc
#define s_realloc zrealloc
#define s_free zfree

s_malloc 内存申请、s_realloc 内存重新分配、s_free 内存释放,这里定义分别是 zmalloc.h 里三个函数的别名,而 zmalloc zrealloc zfree 的定义 在 zmalloc.h 中,它们会根据不同的编译参数来决定其具体使用的内存管理库。

src/sdsalloc.h


#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 ZMALLOC_LIB
#define ZMALLOC_LIB "libc"
#ifdef __GLIBC__
#include <malloc.h>
#define HAVE_MALLOC_SIZE 1
#define zmalloc_size(p) malloc_usable_size(p)
#endif
#endif

上面的的预处理是 Redis 会根据平台或者依赖的库来决定内存分配方案,包括以下几种:

  • google/tcmalloc
  • jemalloc/jemalloc
  • malloc/malloc
  • libc

libc 是 C 语言默认的内存管理库,但是由于 libc 内存碎片率较高,其他三个库在这方面做了优化,另外 redis 在 linux 系统下默认使用 jemalloc 库,因为经验证 jemalloc 比 libc 的碎片率要低很多。

Redis 封装的内存管理函数如下:

void *zmalloc(size_t size); // 申请内存空间
void *zcalloc(size_t size); // 申请并初始化内存
void *zrealloc(void *ptr, size_t size); // 重新申请内存空间 
void zfree(void *ptr); // 释放内存
char *zstrdup(const char *s); // 复制字符串
size_t zmalloc_used_memory(void); // 获得程序申请内存的大小(即通过封装的内存管理函数)
void zmalloc_set_oom_handler(void (*oom_handler)(size_t)); // 设置内存不足申请失败时的回调函数
size_t zmalloc_get_rss(void); // 获得程序实际的内存占用(全部内存占用)
int zmalloc_get_allocator_info(size_t *allocated, size_t *active, size_t *resident); // 获得分配器的信息,使用jemalloc 时可获得jemalloc的分配信息
size_t zmalloc_get_private_dirty(long pid); //获得Rss中已改写的私有页面页面大小
size_t zmalloc_get_smap_bytes_by_field(char *field, long pid);//获得/proc/pid/smaps中某一个field的字节大小
size_t zmalloc_get_memory_size(void); // 获得物理内存的大小
void zlibc_free(void *ptr); // libc free 的封装

这里主要看下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
}

这段函数里会根据HAVE_MALLOC_SIZE是否定义走两条分支,这个常量是否标识是否记录分配内存大小,除了 libc 其他三个库都提供了获得内存占用的函数(上面 sdsalloc.h 代码中 zmalloc_size 对应的别名函数)而 libc 需要自己实现,Redis 针对 libc 代码实现如下:

src/zmalloc.c

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;
}

根据上面代码我们可以看到zmalloc_size的实现也比较简单,就是在申请内存的时候在头部多申请一块 PREFIX_SIZE 的空间用来保存。

作者头像

刘宇帅

非著名程序员,全栈开发工程师,长期专注系统开发与架构设计。

提示

功能待开通!


暂无评论~

相关文章

从零学习redis(8)--- 过期及过期策略

redis 的 string 类型是支持过期设置的,默认是永不过期的。 Redis 设置过期 redis 中设置设置 key 过期有3中方式 第一种在设置值的时候指定过期时间 Set 命令格式 SET key value [EX seconds] [PX milliseconds] [NX|XX] EX second :设置键的过期时间为 second 秒。 SET key value EX second 效果等同于 SETEX key second value 。 PX millisecond :设置键的过期时间为 millisecond 毫秒。 3. SET key value PX m

从零学习redis(6)--- 多机部署之集群模式

Redis 作为高效的缓存数据库,单机也能够表现出很好的性能。但是随着数据的增加、并发的增加,单机模式的 Redis 会受到单机性能、容量、稳定性的限制,即使 Redis 提供了稳定的持久化方案,但是单机服务器始终是部署在单个机器上,如果运行机器本身的服务器出现问题我们很难在短时间恢复服务。所以 Redis 提供了三种多机模式: 主从复制模式 哨兵模式 集群模式 redis集群模式 哨兵模式解决了主节点挂掉的问题,但是没有解决从节点挂掉的问题,而 redis 集群模式可以有效的解决这个问题。redis集群中的数据是和槽(slot)挂钩的,一共定义了16384个槽,所有的数据使用一致性哈希分

从零学习redis(5)--- 多机部署之哨兵模式

Redis 作为高效的缓存数据库,单机也能够表现出很好的性能。但是随着数据的增加、并发的增加,单机模式的 Redis 会受到单机性能、容量、稳定性的限制,即使 Redis 提供了稳定的持久化方案,但是单机服务器始终是部署在单个机器上,如果运行机器本身的服务器出现问题我们很难在短时间恢复服务。所以 Redis 提供了三种多机模式: 主从复制模式 哨兵模式 集群模式 哨兵模式 主从复制模式解决了数据备份和单机可能存在的性能问题,但是也引入了新的问题,一主多从,在使用过程中,如果主服务器挂掉那么整个 Redis 服务的写就挂掉了,如果一个从服务器挂掉那么调用方就无法正确的读数据了,只能通过修改调

从零学习redis(12)--- 大量导入数据

有时候我们需要在短时间内往 redis 里插入大量的数据,我们如果单条单条的出入会浪费很多时间和服务器资源,我们可以使用管道来实现快速的插入。 netcat 我们把需要插入 redis 的数据创建为如下的一个命令集文件 set key1 val1 set key2 val2 ... set key3 val3 我们使用如下命令执行数据导入 &gt; $ cat /tmp/test|nc localhost 6379 +OK +OK +OK pipe mode 使用 netcat 并不是一个可靠地方式,因为用netcat进行大规模插入时不能检查错误。从Redis 2.6开始redis-cli支

从零学习redis(7)--- 单线程和性能

在面试过程中只要涉及到 Redis,一定会问到 Redis是多进程还是多线程?为什么单线程还可以有这么高的性能? Redis为什么是单线程的 Redis 是基于内存的操作,CPU 不是 Redis 的瓶颈,Redis 的瓶颈最有可能是机器的内存的大小和网络的带宽,而且单线程的性能已经非常高了,就没有必要使用多线程了,所以 Redis 是单进程单线程的。 提示: 如果我们运行的服务器是多核服务器,为了充分利用多核优势我们可以在单台服务器起多个 Redis 服务,或者架设 主从复制、哨兵模式、集群模式等多机方案。 Redis 服务运行时只是处理客户端请求是单进程单线程的,但是服务运行时会有其他进