专栏首页陈福荣的专栏十问 Linux 虚拟内存管理 ( 二 )
原创

十问 Linux 虚拟内存管理 ( 二 )

接上篇:十问 Linux 虚拟内存管理 ( 一 )

五. free 的内存真的释放了吗(还给 OS ) ?

前面所有例子都有一个很严重的问题,就是分配的内存都没有释放,即导致内存泄露。原则上所有 malloc/new 分配的内存,都需 free/delete 来释放。但是, free 了的内存真的释放了吗?

要说清楚这个问题,可通过下面例子来说明。

  1. 初始状态:如图 (1) 所示,系统已分配 ABCD 四块内存,其中 ABD 在堆内分配, C 使用 mmap 分配。为简单起见,图中忽略了如共享库等文件映射区域的地址空间。
  2. E=malloc(100k) :分配 100k 内存,小于 128k ,从堆内分配,堆内剩余空间不足,扩展堆顶 (brk) 指针。
  3. free(A) :释放 A 的内存,在 glibc 中,仅仅是标记为可用,形成一个内存空洞 ( 碎片 ) ,并没有真正释放。如果此时需要分配 40k 以内的空间,可重用此空间,剩余空间形成新的小碎片。
  1. free(C) : C 空间大于 128K ,使用 mmap 分配,如果释放 C ,会调用 munmap 系统调用来释放,并会真正释放该空间,还给 OS ,如图 (4) 所示。
  2. free(D) :与释放 A 类似,释放 D 同样会导致一个空洞,获得空闲空间,但并不会还给 OS 。此时,空闲总空间为 100K ,但由于虚拟地址不连续,无法合并,空闲空间无法满足大于 60k 的分配请求。
  3. free(E) :释放 E ,由于与 D 连续,两者将进行合并,得到 160k 连续空闲空间。同时 E 是最靠近堆顶的空间, glibc 的 free 实现中,只要堆顶附近释放总空间(包括合并的空间)超过 128k ,即会调用 sbrk(-SIZE) 来回溯堆顶指针,将原堆顶空间还给 OS ,如图 (6) 所示。而堆内的空闲空间还是不会归还 OS 的。

由此可见:

  1. malloc 使用 mmap 分配的内存 ( 大于 128k) , free 会调用 munmap 系统调用马上还给 OS ,实现真正释放。
  2. 堆内的内存,只有释放堆顶的空间,同时堆顶总连续空闲空间大于 128k 才使用 sbrk(-SIZE) 回收内存,真正归还 OS 。
  3. 堆内的空闲空间,是不会归还给 OS 的。 六. 程序代码中 malloc 的内存都有相应的 free ,就不会出现内存泄露了吗?

狭义上的内存泄露是指 malloc 的内存,没有 free ,导致内存浪费,直到程序结束。而广义上的内存泄露就是进程使用内存量不断增加,或大大超出系统原设计的上限。

上一节说到, free 了的内存并不会马上归还 OS ,并且堆内的空洞(碎片)更是很难真正释放,除非空洞成为了新的堆顶。所以,如上一例子情况 (5) ,释放了 40k 和 60k 两片内存,但如果此时需要申请大于 60k (如 70k ),没有可用碎片,必须向 OS 申请,实际使用内存仍然增大。

因此,随着系统频繁地 malloc 和 free ,尤其对于小块内存,堆内将产生越来越多不可用的碎片,导致“内存泄露”。而这种“泄露”现象使用 valgrind 是无法检测出来的。

下图是 MySQL 存在大量分区表时的内存使用情况 (RSS 和 VSZ) ,疑似“内存泄露”。

因此,当我们写程序时,不能完全依赖 glibc 的 malloc 和 free 的实现。更好方式是建立属于进程的内存池,即一次分配 (malloc) 大块内存,小内存从内存池中获得,当进程结束或该块内存不可用时,一次释放 (free) ,可大大减少碎片的产生。

七. 既然堆内内存不能直接释放,为什么不全部使用 mmap 来分配?

由于堆内碎片不能直接释放,而问题 5 中说到 mmap 分配的内存可以会通过 munmap 进行 free ,实现真正释放。既然堆内碎片不能直接释放,导致疑似“内存泄露”问题,为什么 malloc 不全部使用 mmap 来实现呢?而仅仅对于大于 128k 的大块内存才使用 mmap ?

其实,进程向 OS 申请和释放地址空间的接口 sbrk/mmap/munmap 都是系统调用,频繁调用系统调用都比较消耗系统资源的。并且, mmap 申请的内存被 munmap 后,重新申请会产生更多的缺页中断。例如使用 mmap 分配 1M 空间,第一次调用产生了大量缺页中断 (1M/4K 次 ) ,当 munmap 后再次分配 1M 空间,会再次产生大量缺页中断。缺页中断是内核行为,会导致内核态 CPU 消耗较大。另外,如果使用 mmap 分配小内存,会导致地址空间的分片更多,内核的管理负担更大。

而堆是一个连续空间,并且堆内碎片由于没有归还 OS ,如果可重用碎片,再次访问该内存很可能不需产生任何系统调用和缺页中断,这将大大降低 CPU 的消耗。

因此, glibc 的 malloc 实现中,充分考虑了 sbrk 和 mmap 行为上的差异及优缺点,默认分配大块内存 (128k) 才使用 mmap 获得地址空间,也可通过 mallopt(M_MMAP_THRESHOLD, <SIZE>)来修改这个临界值。

八. 如何查看进程的缺页中断信息?

可通过以下命令查看缺页中断信息

ps -o majflt,minflt -C <program_name>
ps -o majflt,minflt -p <pid>

其中, majflt 代表 major fault ,指大错误, minflt 代表 minor fault ,指小错误。这两个数值表示一个进程自启动以来所发生的缺页中断的次数。其中 majflt 与 minflt 的不同是, majflt 表示需要读写磁盘,可能是内存对应页面在磁盘中需要 load 到物理内存中,也可能是此时物理内存不足,需要淘汰部分物理页面至磁盘中。

例如,下面是 mysqld 的一个例子。

mysql@ TLOG_590_591:~> ps -o majflt,minflt -C mysqld
MAJFLT MINFLT
144856 15296294

如果进程的内核态 CPU 使用过多,其中一个原因就可能是单位时间的缺页中断次数多个,可通过以上命令来查看。

如果 MAJFLT 过大,很可能是内存不足。

如果 MINFLT 过大,很可能是频繁分配 / 释放大块内存 (128k) , malloc 使用 mmap 来分配。对于这种情况,可通过 mallopt(M_MMAP_THRESHOLD, <SIZE>)增大临界值,或程序实现内存池。

九. 如何查看堆内内存的碎片情况?

glibc 提供了以下结构和接口来查看堆内内存和 mmap 的使用情况。

struct mallinfo {
  int arena;    /* non-mmapped space allocated from system */
  int ordblks;  /* number of free chunks */
  int smblks;   /* number of fastbin blocks */
  int hblks;    /* number of mmapped regions */
  int hblkhd;   /* space in mmapped regions */
  int usmblks;  /* maximum total allocated space */
  int fsmblks;  /* space available in freed fastbin blocks */
  int uordblks; /* total allocated space */
  int fordblks; /* total free space */
  int keepcost; /* top-most, releasable (via malloc_trim) space */
};


/* 返回 heap(main_arena) 的内存使用情况,以 mallinfo 结构返回 */
struct mallinfo mallinfo();
/* 将 heap 和 mmap 的使用情况输出到 stderr  */
void malloc_stats();

可通过以下例子来验证 mallinfo 和 malloc_stats 输出结果。

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>
#include <malloc.h>
size_t  heap_malloc_total, heap_free_total,
                mmap_total, mmap_count;
void print_info()
{
        struct mallinfo mi = mallinfo();
        printf("count by itself:\n");
        printf("\theap_malloc_total=%lu heap_free_total=%lu heap_in_use=%lu\n\
\tmmap_total=%lu mmap_count=%lu\n",
                heap_malloc_total*1024, heap_free_total*1024, heap_malloc_total*1024 - heap_free_total*1024,
                mmap_total*1024, mmap_count);
        printf("count by mallinfo:\n");
        printf("\theap_malloc_total=%lu heap_free_total=%lu heap_in_use=%lu\n\
\tmmap_total=%lu mmap_count=%lu\n",
                mi.arena, mi.fordblks, mi.uordblks,
                mi.hblkhd, mi.hblks);
      printf("from malloc_stats:\n");
        malloc_stats();
}

#define ARRAY_SIZE 200
int main(int argc, char** argv)
{
        char** ptr_arr[ARRAY_SIZE];
        int i;
        for( i = 0; i < ARRAY_SIZE; i++) {
                ptr_arr[i] = malloc(i * 1024);
                if ( i < 128)
                        heap_malloc_total += i;
                else {
                        mmap_total += i;
                        mmap_count++;
                }

        }
        print_info();
        for( i = 0; i < ARRAY_SIZE; i++) {
                if ( i % 2 == 0)
                        continue;
                free(ptr_arr[i]);
                if ( i < 128)
                        heap_free_total += i;
                else {
                        mmap_total -= i;
                        mmap_count--;
                }
        }
        printf("\nafter free\n");
        print_info();
        return 1;
}

该例子第一个循环为指针数组每个成员分配索引位置 (KB) 大小的内存块,并通过 128 为分界分别对 heap 和 mmap 内存分配情况进行计数;第二个循环是 free 索引下标为奇数的项,同时更新计数情况。通过程序的计数与 mallinfo/malloc_stats 接口得到结果进行对比,并通过 print_info 打印到终端。

下面是一个执行结果:

count by itself:
        heap_malloc_total=8323072 heap_free_total=0 heap_in_use=8323072
        mmap_total=12054528 mmap_count=72
count by mallinfo:
        heap_malloc_total=8327168 heap_free_total=2032 heap_in_use=8325136
        mmap_total=12238848 mmap_count=72
from malloc_stats:
Arena 0:
system bytes     =    8327168
in use bytes     =    8325136
Total (incl. mmap):
system bytes     =   20566016
in use bytes     =   20563984
max mmap regions =         72
max mmap bytes   =   12238848

after free

count by itself:
        heap_malloc_total=8323072 heap_free_total=4194304 heap_in_use=4128768
        mmap_total=6008832 mmap_count=36
count by mallinfo:
        heap_malloc_total=8327168 heap_free_total=4197360 heap_in_use=4129808
        mmap_total=6119424 mmap_count=36
from malloc_stats:
Arena 0:
system bytes     =    8327168
in use bytes     =    4129808
Total (incl. mmap):
system bytes     =   14446592
in use bytes     =   10249232
max mmap regions =         72
max mmap bytes   =   12238848

由上可知,程序统计和 mallinfo 得到的信息基本吻合,其中 heap_free_total 表示堆内已释放的内存碎片总和。

如果想知道堆内片究竟有多碎 ,可通过 mallinfo 结构中的 fsmblks 、 smblks 、 ordblks 值得到,这些值表示不同大小区间的碎片总个数,这些区间分别是 0~80 字节, 80~512 字节, 512~128k 。如果 fsmblks 、 smblks 的值过大,那碎片问题可能比较严重了。

不过, mallinfo 结构有一个很致命的问题,就是其成员定义全部都是 int ,在 64 位环境中,其结构中的 uordblks/fordblks/arena/usmblks 很容易就会导致溢出,应该是历史遗留问题,使用时要注意!

十. 除了 glibc 的 malloc/free ,还有其他第三方实现吗?

其实,很多人开始诟病 glibc 内存管理的实现,就是在高并发性能低下和内存碎片化问题都比较严重,因此,陆续出现一些第三方工具来替换 glibc 的实现,最著名的当属 google 的 tcmalloc 和 facebook 的 jemalloc

网上有很多资源,可搜索之,这里就不详述了。

总结

基于以上认识,最后发现 MySQL 的疑似“内存泄露”问题一方面是 MySQL 5.5 分区表使用更多的内存,另一方面跟内存碎片有关,这也是 TMySQL 一个优化方向。

然而,以上主要介绍了 glibc 虚拟内存管理主要内容,事实上,在并发情况下, glibc 的虚存管理会更加复杂,碎片情况也可能更严重,这将在另一篇再做介绍。

参考文章

《深入理解计算机系统》第 10 章 http://www.kernel.org/doc/Documentation/x86/x86_64/mm.txt https://en.wikipedia.org/wiki/X86-64#Canonical_form_addresses https://www.ibm.com/developerworks/cn/linux/l-lvm64/ http://www.nosqlnotes.net/archives/105

原创声明,本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

登录 后参与评论
0 条评论

相关文章

  • 十问 Linux 虚拟内存管理 ( 一 )

    最近在做 MySQL 版本升级时( 5.1->5.5 ) , 发现了 mysqld 疑似“内存泄露”现象,但通过 valgrind 等工具检测后,并没发现类似的...

    陈福荣
  • Linux虚拟内存管理

    Linux的内存管理分为 虚拟内存管理 和 物理内存管理,本文主要介绍 虚拟内存管理 的原理和实现。在介绍 虚拟内存管理 前,首先介绍一下 x86 CPU 内存...

    用户7686797
  • Linux文件系统十问

    我在知乎和公众号上都提到过,我 2012 在腾讯工作的时候写过一篇《Linux文件系统十问》。总有人问我这篇文章在哪里能看到,如今外网唯一的正版链接-腾讯学堂也...

    开发内功修炼
  • 完全剖析 - Linux虚拟内存空间管理

    在 《漫画解说内存映射》一文中介绍过 虚拟内存 与 物理内存 映射的原理与过程,虚拟内存与物理内存进行映射的过程被称为 内存映射。内存映射是硬件(内存管理单元)...

    用户7686797
  • Linux(十)组管理

    leeqico
  • Linux内核虚拟内存管理之匿名映射缺页异常分析

    韩传华,就职于南京大鱼半导体有限公司,主要从事linux相关系统软件开发工作,负责Soc芯片BringUp及系统软件开发,乐于分享喜欢学习,喜欢专研Linux内...

    Linux阅码场
  • Linux 最常问的十个面试问题

    如果你要去面试一个Linux系统运维工程师的职位,下面这十个最常见的问题一定要会,否则你的面试可能就危险了。这些都是比较基本的问题,大家要理解,不能光死记硬背。

    Coding十日谈
  • Linux的用户管理(二)

    上次的博客我们讲了关于Linux的用户管理的内容,现在我们来讲第二部分——系统用户组的管理。

    Originalee
  • Linux进程内存管理(二)

    进程启动后,在 jemalloc 载入的时候会调用 jemalloc_constructor 执行一些初始化操作。这里利用了编译器的一些特殊支持,让函数在库加载...

    linjinhe
  • ExtJs十(ExtJs Mvc用户管理之二)

    为了防止意外情况,这里添加了try模块,在错误的时候会将错误信息作为Msg关键字的值返回。

    aehyok
  • shell十三问,为linux学习打基础(二)

    本文整理并转自CU上的帖子[学习共享] shell 十三問?,此贴是2003年发表的,但却是相当不错的linux基础知识汇集贴,原帖主使用的台湾风格,本文加以简...

    小小科
  • 【Linux】学习笔记(二) 用户管理

    /etc/group 的内容包括用户组(Group)、用户组口令、GID(组 ID) 及该用户组所包含的用户(User),每个用户组一条记录。格式如下:

    韩旭051
  • 二.Linux文件及目录管理

    声明:本文为原创,作者为 对弈,转载时请保留本声明及附带文章链接:http://www.duiyi.xyz/linux%e6%96%87%e4%bb%b6%e5...

    对弈
  • Linux内核线程kernel thread详解--Linux进程的管理与调度(十)

    Linux内核可以看作一个服务进程(管理软硬件资源,响应用户进程的种种合理以及不合理的请求)。

    233333
  • DNSPod十问Matt Overman:二维码真的代替域名了吗?

    问答时间:2020年6月24日 嘉宾简介: Matt Overman(SVP):Donuts注册局高级副总裁,Matt负责领导域名货币化和高级域名市场。曾任Na...

    腾讯云DNSPod团队
  • Linux内存描述之内存节点node--Linux内存管理(二)

    这点前面是说的很明白了, NUMA结构下, 每个处理器CPU与一个本地内存直接相连, 而不同处理器之前则通过总线进行进一步的连接, 因此相对于任何一个CPU访问...

    233333
  • Linux ubuntu系统管理命令归纳(二)

    tanoak
  • 【Linux笔记】make工程管理工具(二)

    上一篇笔记分享了使用make工具编译C程序的方法(【Linux笔记】make工程管理工具(一)),但是还未分享make工具是什么,本篇笔记就来看一下make工具...

    正念君
  • 小萌新—> Linux高手系列:包管理【二】

    作为一个有追求的测试,如何从一个运维小萌新成长为一个Linux 高手?请持续关注我们的

    TestOps

扫码关注腾讯云开发者

领取腾讯云代金券