码迷,mamicode.com
首页 > 其他好文 > 详细

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

时间:2015-04-01 17:38:39      阅读:254      评论:0      收藏:0      [点我收藏+]

标签:rss   内存页   procselfsmaps   private_dirty   reids   

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

开胃菜

zmalloc_enable_thread_safeness

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

zmalloc_used_memory

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

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

zmalloc_set_oom_handler

void zmalloc_set_oom_handler(void (*oom_handler)(size_t)) {
    zmalloc_oom_handler = oom_handler;
}
        该函数的功能是给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

#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
        这段代码和我在上一篇博文中介绍的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(内存页的数目)
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;
}
         函数开头:
    int page = sysconf(_SC_PAGESIZE);
        通过调用库函数sysconf()【大家可以man sysconf查看详细内容】来查询内存页的大小。
接下来:
    snprintf(filename,256,"/proc/%d/stat",getpid());
        getpid()就是获得当前进程的id,所以这个snprintf()的功能就是将当前进程所对应的stat文件的绝对路径名保存到字符数组filename中。【不得不称赞一下类Unix系统中“万物皆文件”的概念】
    if ((fd = open(filename,O_RDONLY)) == -1) return 0;
    if (read(fd,buf,4096) <= 0) {
        close(fd);
        return 0;
    }
        以只读模式打开 /proc/<pid>/stat 文件。然后从中读入4096个字符到字符数组buf中。如果失败就关闭文件描述符fd,并退出(个人感觉因错误退出,还是返回-1比较好吧)。
    p = buf;
    count = 23; /* RSS is the 24th field in /proc/<pid>/stat */
    while(p && count--) {
        p = strchr(p,' ');
        if (p) p++;
    }
        RSS在stat文件中的第24个字段位置,所以就是在第23个空格的后面。观察while循环,循环体中用到了字符串函数strchr(),这个函数在字符串p中查询空格字符,如果找到就把空格所在位置的字符指针返回并赋值给p,找不到会返回NULL指针。p++原因是因为,p当前指向的是空格,在执行自增操作之后就指向下一个字段的首地址了。如此循环23次,最终p就指向第24个字段的首地址了。
    if (!p) return 0;
    x = strchr(p,' ');
    if (!x) return 0;
    *x = '\0';
        因为循环结束也可能是p变成了空指针,所以判断一下p是不是空指针。接下来的的几部操作很好理解,就是将第24个字段之后的空格设置为‘\0‘,这样p就指向一个一般的C风格字符串了。
    rss = strtoll(p,NULL,10);
    rss *= page;
    return rss;
        这段代码又用到了一个字符串函数——strtoll():顾名思义就是string to long long的意思啦。它有三个参数,前面两个参数表示要转换的字符串的起始和终止位置(字符指针类型),NULL和‘\0‘是等价的。最后一个参数表示的是“进制”,这里就是10进制了。
        后面用rss和page相乘并返回,因为rss获得的实际上是内存页的页数,page保存的是每个内存页的大小(单位字节),相乘之后就表示RSS实际的内存大小了。

zmalloc_get_fragmentation_ratio

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

zmalloc_get_smap_bytes_by_field

#if defined(HAVE_PROC_SMAPS)
size_t zmalloc_get_smap_bytes_by_field(char *field) {
    char line[1024];
    size_t bytes = 0;
    FILE *fp = fopen("/proc/self/smaps","r");
    int flen = strlen(field);

    if (!fp) return 0;
    while(fgets(line,sizeof(line),fp) != NULL) {
        if (strncmp(line,field,flen) == 0) {
            char *p = strchr(line,'k');
            if (p) {
                *p = '\0';
                bytes += strtol(line+flen,NULL,10) * 1024;
            }
        }
    }
    fclose(fp);
    return bytes;
}
#else
size_t zmalloc_get_smap_bytes_by_field(char *field) {
    ((void) field);
    return 0;
}
#endif
一个条件编译的函数,我们当然要聚焦到#if defined的部分。
   FILE *fp = fopen("/proc/self/smaps","r");
        用标准C的fopen()以只读方式打开/proc/self/smaps文件。简单介绍一下该文件,前面我们已经说过/proc目录下有许多以进程id命名的目录,里面保存着每个进程的状态信息,而/proc/self目录的内容和它们是一样的,self/ 表示的是当前进程的状态目录。而smaps文件中记录着该进程的详细映像信息,该文件内部由多个结构相同的组成,看一下其中某一块的内容:
00400000-004ef000 r-xp 00000000 08:08 1305603                            /bin/bash
Size:                956 kB
Rss:                 728 kB
Pss:                 364 kB
Shared_Clean:        728 kB
Shared_Dirty:          0 kB
Private_Clean:         0 kB
Private_Dirty:         0 kB
Referenced:          728 kB
Anonymous:             0 kB
AnonHugePages:         0 kB
Swap:                  0 kB
KernelPageSize:        4 kB
MMUPageSize:           4 kB
Locked:                0 kB
VmFlags: rd ex mr mw me dw sd 
除去开头和结尾两行,其他的每一行都有一个字段和该字段的值(单位kb)组成【每个字段的具体含义,各位自行百度】。注意这只是smaps文件的一小部分。
    while(fgets(line,sizeof(line),fp) != NULL) {
        if (strncmp(line,field,flen) == 0) {
            char *p = strchr(line,'k');
            if (p) {
                *p = '\0';
                bytes += strtol(line+flen,NULL,10) * 1024;
            }
        }
    }
  • 利用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

size_t zmalloc_get_private_dirty(void) {
    return zmalloc_get_smap_bytes_by_field("Private_Dirty:");
}
        源代码很简单,该函数的本质就是在调用 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这几个字段含义的详细讨论,有位网友进行了深入地探究,并形成了博文,推荐阅读:
--------------------------------------------------------------------------------------------------------------------------------------------------------------

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

标签:rss   内存页   procselfsmaps   private_dirty   reids   

原文地址:http://blog.csdn.net/guodongxiaren/article/details/44783767

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!