Redis内存管理的基石zmallc.c源码解读(二)

本文深入探讨了Redis内存管理中的关键函数,包括线程安全设置、内存使用情况查询、内存不足处理策略及内存碎片率计算等内容。通过对核心代码的解析,帮助读者理解Redis如何高效管理内存。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

转载自:https://blog.youkuaiyun.com/guodongxiaren/article/details/44783767

上一篇博文中,我介绍了zmalloc.c文件中几个常用的函数,接下来给大家介绍一下该文件中的其他函数,其实本文中的很多函数要比上一篇文章中的函数要更有趣的,并且涉及到很多操作系统的知识。前面几个函数比较简单,一笔带过,后面几个是学习的重点。

开胃菜

zmalloc_enable_thread_safeness

[cpp]  view plain  copy
  1. void zmalloc_enable_thread_safeness(void) {  
  2.     zmalloc_thread_safe = 1;  
  3. }  
        zmalloc_thread_safe是一个全局静态变量(static int)。它是操作是否是线程安全的标识。1 表示线程安全,0 表示非线程安全。

zmalloc_used_memory

[cpp]  view plain  copy
  1. size_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. }  
        该函数要完成的操作就是返回变量used_memory(已用内存)的值,所以它的功能是查询系统当前为Redis分配的内存大小。本身代码量不大,但是涉及到了线程安全模式下的查询操作。实现线程同步用到了互斥锁(mutex)。关于互斥锁的内容在上一篇文章中已经简要介绍过了。总之要记住的是加锁(pthread_mutex_lock)和解锁(pthread_mutex_unlock)。在加了互斥锁之后,就能保证之后的代码同时只能被一个线程所执行。

zmalloc_set_oom_handler

[cpp]  view plain  copy
  1. void zmalloc_set_oom_handler(void (*oom_handler)(size_t)) {  
  2.     zmalloc_oom_handler = oom_handler;  
  3. }  
        该函数的功能是给zmalloc_oom_handler赋值。zmalloc_oom_handler是一个函数指针,表示在内存不足(out of memory,缩写oom)的时候所采取的操作,它的类型是void (*) (size_t)。所以zmalloc_set_oom_handler函数的参数也是void (*) (size_t)类型,调用的时候就是传递一个该类型的函数名就可以了。
        不过zmalloc_oom_handler在声明的时候初始化了默认值——zmalloc_default_oom()。同样在上一篇博文中也有过介绍。

zmalloc_size

[cpp]  view plain  copy
  1. #ifndef HAVE_MALLOC_SIZE  
  2. size_t zmalloc_size(void *ptr) {  
  3.     void *realptr = (char*)ptr-PREFIX_SIZE;  
  4.     size_t size = *((size_t*)realptr);  
  5.     /* Assume at least that all the allocations are padded at sizeof(long) by 
  6.      * the underlying allocator. */  
  7.     if (size&(sizeof(long)-1)) size += sizeof(long)-(size&(sizeof(long)-1));  
  8.     return size+PREFIX_SIZE;  
  9. }  
  10. #endif  
        这段代码和我在上一篇博文中介绍的zfree()函数中的内容颇为相似。大家可以去阅读那一篇 博文。这里再概括一下,zmalloc(size)在分配内存的时候会多申请sizeof(size_t)个字节大小的内存【64位系统中是8字节】,即调用malloc(size+8),所以一共 申请分配size+8个字节,zmalloc(size)会在已分配内存的首地址开始的8字节中存储size的值,实际上因为内存对齐,malloc(size+8)分配的内存可能会比size+8要多一些,目的是凑成8的倍数,所以实际分配的内存大小是size+8+X【(size+8+X)%8==0 (0<=X<=7)】。然后内存指针会向右偏移8个字节的长度。zfree()就是zmalloc()的一个逆操作,而zmalloc_size()的目的就是计算出size+8+X的总大小。
--------------------------------------------------------------------------------------------------------------------------------------------------------------
        这个函数是一个条件编译的函数,通过阅读zmalloc.h文件,我们可以得知zmalloc_size()依据不同的平台,具有不同的宏定义,因为在某些平台上提供查询已分配内存实际大小的函数,可以直接 #define zmalloc_size(p)
  1. tc_malloc_size(p)               【tcmalloc】
  2. je_malloc_usable_size(p)【jemalloc】 
  3. malloc_size(p)                 【Mac系统】
当这三个平台都不存在的时候,就自定义,也就是上面的源码。
--------------------------------------------------------------------------------------------------------------------------------------------------------------

大餐

zmalloc_get_rss

        获取RSS的大小,这个RSS可不是我们在网络上常常看到的RSS,而是指的Resident Set Size,表示当前进程实际所驻留在内存中的空间大小,即不包括被交换(swap)出去的空间。
        了解一点操作系统的知识,就会知道我们所申请的内存空间不会全部常驻内存,系统会把其中一部分暂时不用的部分从内存中置换到swap区(装Linux系统的时候我们都知道有一个交换空间)。
        该函数大致的操作就是在当前进程的  /proc/<pid>/stat 【<pid>表示当前进程id】文件中进行检索。该文件的第24个字段是RSS的信息,它的单位是pages(内存页的数目)
[cpp]  view plain  copy
  1. size_t zmalloc_get_rss(void) {  
  2.     int page = sysconf(_SC_PAGESIZE);  
  3.     size_t rss;  
  4.     char buf[4096];  
  5.     char filename[256];  
  6.     int fd, count;  
  7.     char *p, *x;  
  8.   
  9.     snprintf(filename,256,"/proc/%d/stat",getpid());  
  10.     if ((fd = open(filename,O_RDONLY)) == -1) return 0;  
  11.     if (read(fd,buf,4096) <= 0) {  
  12.         close(fd);  
  13.         return 0;  
  14.     }  
  15.     close(fd);  
  16.   
  17.     p = buf;  
  18.     count = 23; /* RSS is the 24th field in /proc/<pid>/stat */  
  19.     while(p && count--) {  
  20.         p = strchr(p,' ');  
  21.         if (p) p++;  
  22.     }  
  23.     if (!p) return 0;  
  24.     x = strchr(p,' ');  
  25.     if (!x) return 0;  
  26.     *x = '\0';  
  27.   
  28.     rss = strtoll(p,NULL,10);  
  29.     rss *= page;  
  30.     return rss;  
  31. }  
         函数开头:
[cpp]  view plain  copy
  1. int page = sysconf(_SC_PAGESIZE);  
        通过调用库函数sysconf()【大家可以man sysconf查看详细内容】来查询内存页的大小。
接下来:
[cpp]  view plain  copy
  1. snprintf(filename,256,"/proc/%d/stat",getpid());  
        getpid()就是获得当前进程的id,所以这个snprintf()的功能就是将当前进程所对应的stat文件的绝对路径名保存到字符数组filename中。【不得不称赞一下类Unix系统中“万物皆文件”的概念】
[cpp]  view plain  copy
  1. if ((fd = open(filename,O_RDONLY)) == -1) return 0;  
  2. if (read(fd,buf,4096) <= 0) {  
  3.     close(fd);  
  4.     return 0;  
  5. }  
        以只读模式打开 /proc/<pid>/stat 文件。然后从中读入4096个字符到字符数组buf中。如果失败就关闭文件描述符fd,并退出(个人感觉因错误退出,还是返回-1比较好吧)。
[cpp]  view plain  copy
  1. p = buf;  
  2. count = 23; /* RSS is the 24th field in /proc/<pid>/stat */  
  3. while(p && count--) {  
  4.     p = strchr(p,' ');  
  5.     if (p) p++;  
  6. }  
        RSS在stat文件中的第24个字段位置,所以就是在第23个空格的后面。观察while循环,循环体中用到了字符串函数strchr(),这个函数在字符串p中查询空格字符,如果找到就把空格所在位置的字符指针返回并赋值给p,找不到会返回NULL指针。p++原因是因为,p当前指向的是空格,在执行自增操作之后就指向下一个字段的首地址了。如此循环23次,最终p就指向第24个字段的首地址了。
[cpp]  view plain  copy
  1. if (!p) return 0;  
  2. x = strchr(p,' ');  
  3. if (!x) return 0;  
  4. *x = '\0';  
        因为循环结束也可能是p变成了空指针,所以判断一下p是不是空指针。接下来的的几部操作很好理解,就是将第24个字段之后的空格设置为'\0',这样p就指向一个一般的C风格字符串了。
[cpp]  view plain  copy
  1. rss = strtoll(p,NULL,10);  
  2. rss *= page;  
  3. return rss;  
        这段代码又用到了一个字符串函数——strtoll():顾名思义就是string to long long的意思啦。它有三个参数,前面两个参数表示要转换的字符串的起始和终止位置(字符指针类型),NULL和'\0'是等价的。最后一个参数表示的是“进制”,这里就是10进制了。
        后面用rss和page相乘并返回,因为rss获得的实际上是内存页的页数,page保存的是每个内存页的大小(单位字节),相乘之后就表示RSS实际的内存大小了。

zmalloc_get_fragmentation_ratio

[cpp]  view plain  copy
  1. /* Fragmentation = RSS / allocated-bytes */  
  2. float zmalloc_get_fragmentation_ratio(size_t rss) {  
  3.     return (float)rss/zmalloc_used_memory();  
  4. }  
        这个函数是查询内存碎片率(fragmentation ratio),即RSS和所分配总内存空间的比值。需要用zmalloc_get_rss()获得RSS的值,再以RSS的值作为参数传递进来。
------------------------------------------------------------------------------------------------------------------------- -------------------------------------
内存碎片分为:内部碎片和外部碎片
  • 内部碎片:是已经被分配出去(能明确指出属于哪个进程)却不能被利用的内存空间,直到进程释放掉,才能被系统利用;
  • 外部碎片:是还没有被分配出去(不属于任何进程),但由于太小了无法分配给申请内存空间的新进程的内存空闲区域。
------------------------------------------------------------------------------------------------------------------------- -------------------------------------
zmalloc_get_fragmentation_ratio()要获得的显然是内部碎片率。

zmalloc_get_smap_bytes_by_field

[cpp]  view plain  copy
  1. #if defined(HAVE_PROC_SMAPS)  
  2. size_t zmalloc_get_smap_bytes_by_field(char *field) {  
  3.     char line[1024];  
  4.     size_t bytes = 0;  
  5.     FILE *fp = fopen("/proc/self/smaps","r");  
  6.     int flen = strlen(field);  
  7.   
  8.     if (!fp) return 0;  
  9.     while(fgets(line,sizeof(line),fp) != NULL) {  
  10.         if (strncmp(line,field,flen) == 0) {  
  11.             char *p = strchr(line,'k');  
  12.             if (p) {  
  13.                 *p = '\0';  
  14.                 bytes += strtol(line+flen,NULL,10) * 1024;  
  15.             }  
  16.         }  
  17.     }  
  18.     fclose(fp);  
  19.     return bytes;  
  20. }  
  21. #else  
  22. size_t zmalloc_get_smap_bytes_by_field(char *field) {  
  23.     ((void) field);  
  24.     return 0;  
  25. }  
  26. #endif  
一个条件编译的函数,我们当然要聚焦到#if defined的部分。
[cpp]  view plain  copy
  1. FILE *fp = fopen("/proc/self/smaps","r");  
        用标准C的fopen()以只读方式打开/proc/self/smaps文件。简单介绍一下该文件,前面我们已经说过/proc目录下有许多以进程id命名的目录,里面保存着每个进程的状态信息,而/proc/self目录的内容和它们是一样的,self/ 表示的是当前进程的状态目录。而smaps文件中记录着该进程的详细映像信息,该文件内部由多个结构相同的 组成,看一下其中 某一块的内容:
[plain]  view plain  copy
  1. 00400000-004ef000 r-xp 00000000 08:08 1305603                            /bin/bash  
  2. Size:                956 kB  
  3. Rss:                 728 kB  
  4. Pss:                 364 kB  
  5. Shared_Clean:        728 kB  
  6. Shared_Dirty:          0 kB  
  7. Private_Clean:         0 kB  
  8. Private_Dirty:         0 kB  
  9. Referenced:          728 kB  
  10. Anonymous:             0 kB  
  11. AnonHugePages:         0 kB  
  12. Swap:                  0 kB  
  13. KernelPageSize:        4 kB  
  14. MMUPageSize:           4 kB  
  15. Locked:                0 kB  
  16. VmFlags: rd ex mr mw me dw sd   
除去开头和结尾两行,其他的每一行都有一个字段和该字段的值(单位kb)组成【每个字段的具体含义,各位自行百度】。注意这只是smaps文件的一小部分。
[cpp]  view plain  copy
  1. while(fgets(line,sizeof(line),fp) != NULL) {  
  2.     if (strncmp(line,field,flen) == 0) {  
  3.         char *p = strchr(line,'k');  
  4.         if (p) {  
  5.             *p = '\0';  
  6.             bytes += strtol(line+flen,NULL,10) * 1024;  
  7.         }  
  8.     }  
  9. }  
  • 利用fgets()逐行读取/proc/self/smaps文件内容
  • 然后strchr()将p指针定义到字符k的位置
  • 然后将p置为'\0',截断形成普通的C风格字符串
  • line指向的该行的首字符,line+flen(要查询的字段的长度)所指向的位置就是字段名后面的空格处了,不必清除空格,strtol()无视空格可以将字符串转换成int类型
  • strol()转换的结果再乘以1024,这是因为smaps里面的大小是kB表示的,我们要返回的是B(字节byte)表示
------------------------------------------------------------------------------------------------------------------------- -------------------------------------
实际上 /proc/self目录是一个符号链接,指向/proc/目录下以当前id命名的目录。我们可以进入该目录下敲几个命令测试一下。
root@X:/proc/self# pwd -P
/proc/4152
root@X:/proc/self# ps aux|grep [4]152
root      4152  0.0  0.0  25444  2176 pts/0    S    09:06   0:00 bash
------------------------------------------------------------------------------------------------------------------------- -------------------------------------

zmalloc_get_private_dirty

[cpp]  view plain  copy
  1. size_t zmalloc_get_private_dirty(void) {  
  2.     return zmalloc_get_smap_bytes_by_field("Private_Dirty:");  
  3. }  
        源代码很简单,该函数的本质就是在调用 zmalloc_get_smap_bytes_by_field("Private_Dirty:");其完成的操作就是扫描 /proc/self/smaps文件,统计其中所有 Private_Dirty字段的和。那么这个Private_Dirty是个什么意思呢?
        大家继续观察一下,我在上面贴出的 /proc/self/smaps文件的结构,它有很多结构相同的部分组成。其中有几个字段有如下的关系:

Rss=Shared_Clean+Shared_Dirty+Private_Clean+Private_Dirty
        其中:
  • Shared_Clean:多进程共享的内存,且其内容未被任意进程修改 
  • Shared_Dirty:多进程共享的内存,但其内容被某个进程修改 
  • Private_Clean:某个进程独享的内存,且其内容没有修改 
  • Private_Dirty:某个进程独享的内存,但其内容被该进程修改
    其实所谓的共享的内存,一般指的就是Unix系统中的共享库(.so文件)的使用,共享库又叫动态库(含义同Windows下的.dll文件),它只有在程序运行时才被装入内存。这时共享库中的代码和数据可能会被多个进程所调用,于是就会产生共享(Shared)与私有(Private)、干净(Clean)与脏(Dirty)的区别了。此外该处所说的共享的内存除了包括共享库以外,还包括System V的IPC机制之一的共享内存段(shared memory)
------------------------------------------------------------------------------------------------------------------------- -------------------------------------
关于smaps文件中Shared_Clean、Shared_Dirty、Private_Clean、Private_Dirty这几个字段含义的详细讨论,有位网友进行了深入地探究,并形成了博文,推荐阅读:
------------------------------------------------------------------------------------------------------------------------- -------------------------------------
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值