redis源码

redis源码1---内存管理(zmalloc)

2020-04-24  本文已影响0人  QaoKi

打算学习一下redis源码,结果刚开始看sds就发现一个陌生的词汇,zmalloc,查看zmalloc的实现,发现是对malloc的封装,并且还引出了ptMalloc和tcMalloc等知识,关于malloc库和redis的其他内存管理的知识,后续查看了后再谈

字长与字节对齐

首先要了解一个操作系统的基础,字长和字节对齐,字长是指 CPU一次性能读取数据的二进制位数,也就是我们通常所说的32位系统(字长4个字节)、64位系统(字长8个字节)的由来。所谓的字节对齐,简单的介绍可以查看 https://blog.csdn.net/ldw662523/article/details/79623404,总之,当我们向系统申请内存的时候,系统会返回给我们 n倍个字长的字节数,比如我们申请10字节,64位系统下会返回给我们2*8个字节
long类型变量和指针类型变量占用一字长的字节数,在32为系统下,sizeof(char *) 和 sizeof(long) 是4,在64位下是8,本文接下来内容都用64位,也就是8字节对齐为主

主要变量和函数

全局变量

 //定义当前进程已使用的内存总量
static size_t used_memory = 0;  
//标识是否线程安全,值为1时线程安全
static int zmalloc_thread_safe = 0;  
 //如果是线程安全的,修改used_memory 时的互斥锁
pthread_mutex_t used_memory_mutex = PTHREAD_MUTEX_INITIALIZER; 
//zmalloc_oom_handler 为函数指针,当内存出错时调用,默认的调用函数是 zmalloc_default_oom
//oom  是out of memory
static void (*zmalloc_oom_handler)(size_t) = zmalloc_default_oom;

主要函数

void *zmalloc(size_t size);  //分配内存空间
void zcalloc(size_t size);  //分配内存并初始化为0
void zrealloc(void *ptr,size_t size);  //重新分配内存空间的大小
void zfree(void *ptr);  //释放zmalloc所分配的内存空间
char *zstrdup(const char *s);  //字符串复制
void zlibc_free(void *ptr);  //同free()

//linux的glibc下是sizeof(size_t),64位系统为8字节
//当zmalloc申请内存时,多分配PREFIX_SIZE 个字节
PREFIX_SIZE
//若使用tcmalloc、jemalloc或Mac系统则定义此宏,linux的glibc不定义
HAVE_MALLOC_SIZE

宏函数

//分配内存空间后更新used_memory的值
update_zamlloc_stat_alloc
//释放内存空间后更新used_memory的值
update_zamlloc_stat_free
//线程安全地used_memory增加操作
update_zamlloc_stat_add
//线程安全地used_memory减少操作
update_zamlloc_stat_sub

zmalloc

void *zmalloc(size_t size) {
    //size是要分配的内存大小,多分配了PREFIX_SIZE个字节,用于存放size的大小
    void *ptr = malloc(size + PREFIX_SIZE);

    //如果内存分配失败,调用函数指针zmalloc_oom_handler所指向的函数
    if (!ptr) zmalloc_oom_handler(size);
#ifdef HAVE_MALLOC_SIZE
    update_zmalloc_stat_alloc(zmalloc_size(ptr));
    return ptr;
#else
    //前sizeof(size_t) 个字节,存储要分配的内存大小
    *((size_t*)ptr) = size;
    update_zmalloc_stat_alloc(size+PREFIX_SIZE);
    //只返回给用户可用的内存空间
    return (char*)ptr+PREFIX_SIZE;
#endif
}

PREFIX_SIZE是一个条件编译的宏,不同的平台有不同的结果,在Linux中其值是sizeof(size_t),所以我们多分配了一个字长(8个字节)的空间,用于存放size的值

zmalloc_oom_handler指向内存错误处理函数,默认是指向函数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();
}

linux下不定义宏 HAVE_MALLOC_SIZE,所以走 #else的代码

//前sizeof(size_t) 个字节,存储要分配的内存大小
*((size_t*)ptr) = size;
update_zmalloc_stat_alloc(size+PREFIX_SIZE);
//只返回给用户可用的内存空间
return (char*)ptr+PREFIX_SIZE;

前sizeof(size_t),也就是PREFIX_SIZE个字节,存放所申请的内存大小size。我们先看一下

return (char*)ptr+PREFIX_SIZE;

将指针后移 PREFIX_SIZE 位,只给用户返回可用的内存空间,所以malloc出来的内存被分为了两部分,一部分用来存放size的值,一部分返回给用户。
然后调用了宏函数update_zmalloc_stat_alloc

update_zmalloc_stat_alloc

#define update_zmalloc_stat_alloc(__n) do { \
    size_t _n = (__n); \
    //判断 _n是否是8的倍数
    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)

其中

if (_n&(sizeof(long)-1)) _n += sizeof(long)-(_n&(sizeof(long)-1)); \

用位运算判断 _n 是否是8的倍数,如果不是,加上所需的数值,使之能被8整除,比如,申请20bytes, 20 & (sizeof (long) - 1) = 20 & 7 = 4,非零,不能整除,即缺 8 - 4 = 4 bytes,于是申请的20 bytes多申请4bytes,24 mod 8 = 0正好

其实malloc出来的内存,已经是8的倍数,系统自动帮我们完成字节对齐,我们之所以还要判断一次,是为了保证used_memory的值是实际分配的内存大小。比如用户申请20字节,malloc以后,系统分配了24字节,但是我们并不知道分配了24字节,所以我们要像系统一样,去做一次字节对齐,这样能够知道系统具体分配了多少

接下来判断是否是线程安全的,zmalloc_thread_safe默认为0,也就是走else下面,直接给used_memory 加上值。如果是线程安全的,调用宏函数 update_zmalloc_stat_add

update_zmalloc_stat_add

#define update_zmalloc_stat_add(__n) do { \
    pthread_mutex_lock(&used_memory_mutex); \
    used_memory += (__n); \
    pthread_mutex_unlock(&used_memory_mutex); \
} while(0)

其实就是调用了互斥锁来保证增加used_memory 值的时候是线程安全的,注意到一点是宏函数外层都用 do while(0) 包裹着,其实并不是额外加了什么功能,只是为了保持宏函数实现的时候语意和我们所写的代码一致,防止大括号以及分号的干扰,详见 https://blog.csdn.net/Move_now/article/details/73480195

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
    //将指针前移 PREFIX_SIZE 位
    realptr = (char*)ptr-PREFIX_SIZE;
    //取出当zmalloc时返回给用户的可用的内存空间大小
    oldsize = *((size_t*)realptr);
    update_zmalloc_stat_free(oldsize+PREFIX_SIZE);
    free(realptr);
#endif
}

我们还是关注没有定义 HAVE_MALLOC_SIZE 的部分,定义指针realptr,让其指向最初malloc返回的地址,用oldsize 保存用户所申请的内存大小
update_zmalloc_stat_free 和 update_zmalloc_stat_alloc 一样是宏函数,不同之处是前者在给变量used_memory减去分配的空间,而后者是加上该空间大小

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)); \
  if (zmalloc_thread_safe) { \
      update_zmalloc_stat_sub(_n); \
  } else { \
      used_memory -= _n; \
  } \
} while(0)

update_zmalloc_sub与zmalloc()中的update_zmalloc_add相对应,但功能相反,提供线程安全地used_memory减法操作

update_zmalloc_stat_sub

#define update_zmalloc_stat_sub(__n) do { \
   pthread_mutex_lock(&used_memory_mutex); \
   used_memory -= (__n); \
   pthread_mutex_unlock(&used_memory_mutex); \
} while(0)

zcalloc

zcalloc()的实现基于calloc(),但是两者编程接口不同

void *calloc(size_t nmemb, size_t size);
void *zcalloc(size_t size);

calloc()的功能是也是分配内存空间,与malloc()的不同之处有两点:
1.它分配的空间大小是 size * nmemb。比如calloc(10,sizoef(char)); // 分配10个字节
2.calloc()会对分配的空间做初始化工作(初始化为0),而malloc()不会

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
}

zaclloc和zmalloc原理是一样的,只不过申请内存时malloc换成了calloc,和zmalloc相比就是申请的内存被初始化了

zrealloc

zrealloc()和realloc()具有相同的编程接口:

void *realloc (void *ptr, size_t size);
void *zrealloc(void *ptr, size_t size);

realloc()要完成的功能是给首地址ptr的内存空间,重新分配大小。如果失败了,则在其它位置新建一块大小为size字节的空间,将原先的数据复制到新的内存空间,并返回这段内存首地址【原内存会被系统自然释放】。 zrealloc()要完成的功能也类似。

void *zrealloc(void *ptr, size_t size) {
#ifndef HAVE_MALLOC_SIZE
  void *realptr;
#endif
  size_t oldsize;
  void *newptr;

  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);
  update_zmalloc_stat_alloc(size);
  return (char*)newptr+PREFIX_SIZE;
#endif
}

zstrdup

复制字符串s的内容,申请新的内存空间存储字符串。并将这段新的字符串地址返回

char *zstrdup(const char *s) {
  //strlen()函数是不统计'\0'的,所以最后要加1
  size_t l = strlen(s)+1;
  char *p = zmalloc(l);
   //调用memcpy来完成复制
  memcpy(p,s,l);
  return p;
}

zmalloc_size

这个函数是为glibc定制的,只有用这个库时,才能使用这个函数

#ifndef HAVE_MALLOC_SIZE
  size_t zmalloc_size(void *ptr) {
  void *realptr = (char*)ptr-PREFIX_SIZE;
  size_t size = *((size_t*)realptr);

  if (size&(sizeof(long)-1)) size += sizeof(long)-(size&(sizeof(long)-1));
  return size+PREFIX_SIZE;
}
#endif

得到zmalloc像系统申请的真实的内存大小

参考资料:https://blog.csdn.net/guodongxiaren/article/details/44747719
https://blog.csdn.net/u012842205/article/details/50392119

上一篇下一篇

猜你喜欢

热点阅读