最近想多写一些内存管理的文章。这次我们将从malloc动态内存分配开始。我在这篇文章中也做了一个小实验:malloc是如何分配内存的?malloc是否分配物理内存?malloc(1)将分配多少内存?free释放内存,会不会还给操作系统?free()函数只传入一个内存地址,为什么能知道要释放多少内存呢?开始!Linux进程的内存分布是怎样的?所以?在Linux操作系统中,虚拟地址空间内部分为两部分,内核空间和用户空间。具有不同位的系统具有不同的地址空间范围。比如最常见的32位和64位系统如下:从这里可以看出,32位系统的内核空间占用1G,处于最高点,剩下的3G是用户空间;64位系统的内核空间和用户空间空间都是128T,分别占据了整个内存空间的最高和最低部分,其余中间部分未定义。下面说一下内核空间和用户空间的区别:当进程处于用户态时,只能访问用户态内存;只有进入内核态后才能访问内核空间内存;虽然每个进程都有自己独立的虚拟内存,但是每个虚拟内存中的内核地址实际上关联的是同一个物理内存。这样,进程切换到内核态后,就可以方便地访问内核空间内存了。接下来,我们就来详细了解一下虚拟空间的划分。用户空间和内核空间的划分不同,内核空间的分布就不多说了。我们来看看用户空间的分布情况。以32位系统为例,我画了一张图来说明它们的关系:通过这张图可以看到,用户空间内存从低到高共有6种不同类型。内存段:程序文件段,包括二进制可执行代码;初始化数据段,包括静态常量;未初始化的数据段,包括未初始化的静态变量;堆段,包括动态分配的内存,从低地址向上增长;文件映射段,包括动态库、共享内存等,从低地址开始向上增长(与硬件和内核版本有关);堆栈段,包括局部变量和函数调用上下文等。堆栈大小是固定的,通常为8MB。当然,系统也提供了参数,方便我们自定义尺寸;在这6个内存段中,堆和文件映射段的内存是动态分配的。例如,使用C标准库的malloc()或mmap(),可以分别在堆和文件映射段动态分配内存。malloc是怎么分配内存的?实际上,malloc()并不是系统调用,而是C库中一个动态分配内存的函数。malloc申请内存时,有两种方式向操作系统申请堆内存。方法一:通过brk()系统调用从堆中分配内存;方法二:通过mmap()系统调用在文件映射区分配内存;方法一的实现方式很简单,就是通过brk()函数,将“堆顶”指针移动到更高的地址,获得新的内存空间。如下图所示:方法二通过mmap()系统调用中的“私有匿名映射”方法,在文件映射区分配一块内存,即从文件映射区“窃取”一块内存.如下图所示:malloc()在什么场景下通过brk()分配内存?mmap()在什么情况下分配内存?malloc()的源码中定义了一个默认阈值:如果用户分配的内存小于128KB,则通过brk()申请内存;如果用户分配的内存大于128KB,通过mmap()申请内存;malloc()分配物理内存吗?不,malloc()分配虚拟内存。如果分配的虚拟内存没有被访问,虚拟内存就不会映射到物理内存,这样物理内存就不会被占用。只有在访问分配的虚拟地址空间时,操作系统通过查找页表发现虚拟内存对应的页不在物理内存中,才会触发缺页中断,然后操作系统建立一个链接虚拟内存和物理内存之间的映射关系。malloc(1)将分配多少虚拟内存?malloc()在分配内存的时候,并不是老老实实按照用户申请的字节数来分配内存空间的大小,而是预先分配一块更大的空间作为内存池。预分配多少空间与malloc使用的内存管理器有关。我们将使用malloc的默认内存管理器(Ptmalloc2)来分析它。接下来,我们来做个实验。使用下面的代码可以看到当通过malloc申请1字节内存时,操作系统实际分配了多少内存空间。#include#includeintmain(){printf("使用cat/proc/%d/maps查看内存分配情况\n",getpid());//申请1字节内存void*addr=malloc(1);printf("这1字节内存的起始地址:%x\n",addr);printf("使用cat/proc/%d/maps查看内存分配情况\n",getpid());//阻塞程序,只有输入任意字符时才执行getchar();//释放内存free(addr);printf("1个字节的内存被释放了,但是heap堆不会释放\n");获取字符();return0;}执行代码:我们可以通过/proc//maps文件查看进程的内存分布情况。我通过这个1字节的内存起始地址过滤掉了maps文件中的内存地址范围。[root@xiaolin~]#cat/proc/3191/maps|grepd73000d73000-00d94000rw-p0000000000:000本例中分配的内存小于128KB,所以是通过brk()系统调用从堆空间申请的内存,所以可以看到标记[heap]在最右边。可以看出堆空间的内存地址范围是00d73000-00d94000,这个范围的大小是132KB,也就是说malloc(1)实际上预分配了132K字节的内存。可能有同学注意到,程序中打印的内存起始地址是d73010,而maps文件显示堆内存空间起始地址是d73000。为什么多了一个0x10(16字节)?这个问题,先放着吧,以后再说。空闲内存会返回给操作系统吗?我们执行上面的流程,看看通过free()函数释放内存后,堆内存是否还在?从下图可以看出,通过free释放内存后,堆内存依然存在,并没有归还给操作系统。这是因为与其将这1个字节释放给操作系统,还不如缓存起来放到malloc的内存池中。当进程再次申请1字节内存时,可以直接复用,速度快很多。当然,当进程退出时,操作系统会回收进程的所有资源。上面说的freememory是针对malloc通过brk()申请的内存,heapmemory还是存在的。如果malloc通过mmap申请内存,free会释放内存返回给操作系统。下面做个实验来验证一下,我们通过malloc申请了128KB的内存,这样malloc就可以通过mmap分配内存了。#include#includeintmain(){//申请1字节内存void*addr=malloc(128*1024);printf("这128KB字节内存的起始地址:%x\n",addr);printf("使用cat/proc/%d/maps查看内存分配情况\n",getpid());//阻塞程序,只有输入任意字符时才执行getchar();//释放内存free(addr);printf("128KB内存被释放,内存也归还给操作系统\n");获取字符();return0;}执行代码:查看进程的内存可以发现最右边没有[head]标记,说明匿名内存是通过mmap以匿名映射的形式从文件映射区分配的。那我们释放这段内存看看:再次查看128KB内存的起始地址,可以发现它已经不存在了,说明已经归还给操作系统了。关于“malloc申请的内存在free释放时是否会返还给操作系统?”这个问题,我们可以做一个总结:malloc通过brk()申请的内存在free释放内存时不会返还给操作系统系统,缓存在malloc的内存池中,以备下次使用;当malloc通过mmap()申请的内存被free释放后,内存就会归还给操作系统,内存才真正被释放。为什么不都使用mmap来分配内存呢?因为向操作系统申请内存是通过系统调用,执行系统调用就是进入内核态,然后再回到用户态,切换到运行态会花费很多时间。因此申请内存的操作要避免频繁的系统调用。如果使用mmap分配内存,意味着每次都要执行系统调用。另外,由于mmap分配的内存每次释放都会归还给操作系统,所以mmap分配的虚拟地址每次都处于pagefault状态,然后当第一次访问该虚拟地址时,将触发页面错误。分页符。也就是说,如果频繁使用mmap分配的内存,不仅每次都会发生运行状态的切换,而且还会发生pagefault中断(第一次访问虚拟地址后),这会导致大量的CPU消耗。为了改善这两个问题,malloc在使用brk()系统调用在堆空间申请内存时,由于堆空间是连续的,直接预分配一块较大的内存作为内存池,而当内存被释放,它被缓存在内存池中。下次申请内存时,直接从内存池中取出对应的内存块即可,而这块内存块的虚拟地址和物理地址的映射关系可能仍然存在,不仅减少了系统调用次数,同时也减少了页面错误中断的次数,这将大大降低CPU消耗。既然br??k这么牛逼,为什么不用brk全部分配呢?前面我们提到通过brk从堆空间分配的内存是不会归还给操作系统的,那么我们来考虑这样一个场景。如果我们连续申请10k、20k、30k的内存,如果10k、20k被释放,成为空闲内存空间,如果下次申请的内存小于30k,那么这个空闲内存空间就可以重新使用了。但是,如果下次申请的内存大于30k,没有可用的空闲内存空间,则必须向OS申请,实际使用的内存会不断增加。因此,随着系统频繁的mallocs和free,尤其是对于小块内存,会在堆中产生越来越多的不能使用的碎片,从而导致“内存泄漏”。使用valgrind无法检测到这种“泄漏”。因此,在malloc的实现中,充分考虑了sbrk和mmap行为的差异和优缺点,在使用mmap分配内存空间之前,默认分配了一大块内存(128KB)。free()函数只传入一个内存地址,为什么能知道要释放多少内存呢?还记得吗,前面提到过malloc返回给用户态的内存起始地址比进程Section的堆空间起始地址多了16个字?多出来的16个字节用来存放内存块的描述信息,比如内存块的大小。这样,在执行free()函数的时候,free会将传入的内存地址向左偏移16个字节,然后从这16个字节分析出当前内存块的大小,自然就知道要释放多少内存了向上。