LINUX内核研究----C/C++内存管理glibc运行库底层ptmalloc内存管理源码分析总结

基础知识:

32位进程的虚拟地址空间


64位进程的虚拟地址空间


应用程序的堆栈从最高地址处开始向下生长,.bss段与.Stack之间的空间是空闲的,空闲空间被分成两部分,一部分为heap,一部分为mmap映射区域。

Heap和mmap区域都可以供用户自由使用,但是它在刚开始的时候并没有映射到内存空间内,是不可访问的。

在向内核请求分配该空间之前,对这个空间的访问会导致segmentationfault(段错误)。

用户程序可以直接使用系统调用来管理heap和mmap映射区域,但更多的时候程序都是使用C语言提供的malloc()和free()函数来动态的分配和释放内存。在C++中使用new和delete来动态分配和释放内存,但是其底层还是通过调用malloc和free函数实现。

Stack区域是唯一不需要映射,用户却可以访问的内存区域,这也是利用堆栈溢出进行攻击的基础。

heapmmap映射区域是可以提供给用户程序使用的虚拟内存空间,如何获得该区域的内存呢?也就是如何获取额外的虚拟内存。

heap堆内存的操作

#include <unistd.h>

int brk(void *addr); linux操作系统提供了brk()系统调用函数

void *sbrk(intptr_t increment); C运行时库提供了sbrk()库函数:

Glibc的malloc函数族(realloc,calloc等)就调用sbrk()函数移动指向堆顶的brk指针,将数据段的下界移动,sbrk()函数在内核的管理下将虚拟地址空间映射到内存,供malloc()函数使用。

sbrk()的参数increment为0时,sbrk()返回的是进程的当前brk值,increment为正数时扩展brk值,当increment为负值时收缩brk值。

内核的进程PCBtask_struct)里一个mm_struct数据结构中有用来描述一个进程的虚拟地址空间的数据结构。

其中mm_struct成员变量中的有一个vm_area_struct结构体维护了进程的虚拟地址空间:

start_codeend_code是进程代码段的起始和终止地址

start_data end_data是进程数据段的起始和终止地址

start_stack是进程堆栈段起始地址

start_brkheap的起始地址,还有一个 brk(堆的当前最后地址),就是动态内存分配当前的终止地址。


C语言的动态内存分配基本函数malloc(),在Linux上的实现是通过内核的brk系统调用在使malloc之前,brk的值等于start_brk,也就是说heap大小为0

brk()是一个非常简单的系统调用,只是简单地改变mm_struct结构的成员变量brk的值来扩大堆的内存。

mmap映射区域的操作

操作系统提供了mmap()和munmap()函数。

mmap()函数将一个文件或者其它对象映射进内存。文件被映射到多个页上,如果文件的大小不是所有页的大小之和,则按页大小也就4K是整数倍申请且最后一个页不被使用的空间将会清零,这个清零的操作效率比较低。

munmap执行相反的操作,删除特定地址区域的对象映射。函数的定义如下:

#include <sys/mman.h>

void *mmap(void *addr, size_tlength, int prot, int flags, int fd, off_t offset);

int munmap(void *addr, size_tlength);

总结:

sbrk(),brk() 或者 mmap() 都可以用来向我们的进程添加额外的虚拟内存

Glibc同样是使用这些函数向操作系统申请虚拟内存,Linux内核在用户申请内存的时候,只是给它分配了一个线性区(也就是虚拟内存),并没有分配实际物理内存;只有当用户使用这块内存的时候会产生缺页中断,通过缺页异常的处理程序内核这才会分配具体的物理页面给用户,这时候才占用宝贵的物理内存。

常见C内存管理程序

内存管理程序:分配器

       它处在用户程序和内核之间,接收响应用户的分配请求,然后向操作系统申请内存,将其返回给用户程序。

为了保持高效的分配,分配器一般都会预先分配一块大于用户请求的内存,并通过某种算法管理这块内存,来满足用户的内存分配要求。

用户释放掉的内存也并不是立即就返回给操作系统,相反分配器会管理这些被释放掉的空闲空间,以应对用户以后的内存分配要求。

也就是说,分配器不但要管理已分配的内存块,还需要管理空闲的内存块。

当响应用户分配要求时,分配器会首先在空闲空间中寻找一块合适的内存给用户,在空闲空间中找不到的情况下才分配一块新的内存。

为实现一个高效的分配器,需要考虑很多的因素。

比如,分配器本身管理内存块所占用的内存空间必须很小,分配算法必须要足够的快。

TCMalloc

TCMalloc是google开发的开源工具中的一种通用内存管理程序。

集成了内存池和垃圾回收的优点:

对于小内存,按8的整数次倍分配

对于大内存,按4K的整数次倍分配

这样做有两种好处

一:相比于其他提供几十种选择的内存池,内存管理程序往往要遍历一遍各种长度才能选出合适的内存块,TCMalloc只需要简单地做几个运算就开源分配,所以TCMalloc分配的速度比较快。

二:短期的收益比较大,分配的小内存至多浪费7个字节,大内存则之多浪费4K。但是长远来说,TCMalloc分配的种类还是比别的内存池要多很多的,可能会导致内存的复用率很低。

TCMalloc有高效的空闲内存回收机制:

1、当一个线程的空闲内存比较多的时候,会交还给进程,进程可以把它调配给其他线程使用

2、当线程的某种长度内存交还给进程后,其他线程并没有需求,进程则把这些长度合并成内存页,然后切割成其他长度。

3、周期性的内存回收,避免可能出现的内存爆炸式增长的问题。

TCMalloc有很高的空间利用率,只额外花费1%的空间:

1、尽量避免加锁(一次加锁解锁约浪费100ns),如果要使用锁的话使用更高效的spinlock,采用更合理的内存粒度:小于32K的被定义为小块内存。

总结

至少在性能与内存使用率上TCMalloc是领先很多的。Glibc的Ptmalloc在内存回收方面做得不太好,常见的一个问题,申请很多内存,然后又释放,只是有一小块没释放,这时候Glibc就必须要等待这一小块也释放了,也把整个大块释放,极端情况下,可能会造成几个G的浪费。

Ptmalloc简介:ptmalloc是linux下glibc库的malloc()和free()底层实现的内存管理程序,提供动态内存管理的和多线程的支持。

Chunk格式

ptmalloc 在给用户分配的空间的前后加上了一些控制信息,用这样的方法来记录分配的信息,以便完成分配和释放工作。

GLIBC中的源码中的chunk结构:

空闲chunk容器

用户调用malloc请求分配的空间在ptmalloc中都使用一个chunk结构来管理。

用户调用free()函数释放掉的内存也并不是都会立即归还给操作系统,相反:它们也会被表示为一个chunk,ptmalloc会统一管理heap和mmap映射区域中的空闲chunk。当用户进行下一次分配请求时,ptmalloc会首先试图在空闲的chunk中挑选一块给用户,这样就避免了频繁的系统调用,降低了内存分配的开销。

 

对于空闲的chunk,ptmalloc采用分箱式内存管理方式,根据空闲chunk的大小和处于的状态通过链表的方式将其放在四个不同的bin中,这四个空闲chunk的容器包括fast bins,unsorted bin, small bins和large bin




1、binsptmalloc将相似大小的chunk用双向链表链接起来,这样的一个链表被称为一个bin。

bins有128个队列(通俗的说法就是一个128大小的数组,里面放着指向双向链表的头结点的指针,每个相连bin大小差为8B)

前64个队列是定长的(smallbins),每隔8个字节大小的块分配在一个队列。

后面的64个队列是不定长的(largebins),就是在一个范围长度的都分配在一个队列中。所有长度小于512字节的都分配在定长的队列中,后面的64个队列是变长的队列,每个队列中的chunk都是从大到小排列的。

2unsort队列(只有一个队列):它是一个cache,所有free下来的如果要进入bins队列中都要经过unsort队列,分配内存时会查看unsorted bin中是否有合适的chunk,如果找到满足条件的chunk,则直接返回给用户,否则将unsorted bin中所有chunk放入bins中。

3fastbins大约有10个定长队列,它是一个高速缓冲,所有free下来的并且长度是小于max_fast(默认80B)的chunk就会进入这种队列中。进入此队列的chunk在free的时候并不修改使用位,目的是为了避免被相邻的块合并掉

如果内存块是空闲的,它会挂在其中的一个队列中,它是通过复用的方式,使用空闲chunk的第3个字和第4个字当作它的前链和后链(变长块是第5个字和第6个字)。

 

 

并不是所有的chunk都按照上面的方式来组织,实际上,有三种例外情况。Top chunkmmaped chunklast remainder,下面会分别介绍这三类特殊的chunktop chunk对于主分配区和非主分配区是不一样的。

 

4.Top chunk

对于非主分配区会预先从mmap区域分配一块较大的空闲内存模拟sub-heap,通过管理sub-heap来响应用户的需求,因为内存是按地址从低向高进行分配的,在空闲内存的最高处,必然存在着一块空闲chunk,叫做top chunk。

 

由于主分配区是唯一能够映射进程heap区域的分配区,它可以通过sbrk()来增大或是收缩进程heap的大小,ptmalloc在开始时会预先分配一块较大的空闲内存(也就是所谓的 heap),主分配区的top chunk在第一次调用malloc时会分配一块(chunk_size +128KB) align 4KB大小的空间作为初始的heap,用户从top chunk分配内存时,可以直接取出一块内存给用户

5.mmaped chunk

当需要分配的chunk足够大,而且fast bins和bins都不能满足要求,甚至top chunk本身也不能满足分配需求时,ptmalloc会使用mmap来直接使用内存映射来将页映射到进程空间。这样分配的chunk在被free时将直接解除映射,于是就将内存归还给了操作系统,再次对这样的内存区的引用将导致segmentationfault错误

6.last remainder

Last remainder是另外一种特殊的chunk,就像top chunk和mmaped chunk一样,不会在任何bins中找到这种chunk。当需要分配一个small chunk,但在small bins中找不到合适的chunk,如果last remainderchunk的大小大于所需的small chunk大小,last remainderchunk被分裂成两个chunk,其中一个chunk返回给用户,另一个chunk变成新的last remainderchuk


ptmalloc分配算法

  小于等于64字节:

用pool算法分配。

  64到512字节之间:

在最佳匹配算法分配和pool算法分配中根据情况折中选取策略取一种合适算法的配算法。

 大于等于512字节:

用最佳适配算法分配。

 大于等于mmap分配阈值(默认值128KB):根据设置的mmap的分配策略进行分配,如果没有开启mmap分配阈值的动态调整机制,大于等于128KB就直接调用mmap分配,否则,大于等于mmap分配阈值时才直接调用mmap()分配。

我们在linux上验证一下这个理论:首先提一个问题,以下两个代码有什么问题?不考虑其他,只考虑程序能否成功运行?

运行代码:



该代码在linux下可以成功输出hello world hello c 。在windows有类似的效果


该代码直接异常结束,产生段错误的提示。在windows有类似的效果


我们通过命令$strace  ./a.out 来跟踪a.out程序的运行找出答案:

程序1:


程序2:


从实验中我们看到申请小于128Kb的内存是通过系统调用brk()函数,但是free()后并没有产生系统调用。也就是说该内存并没有归还给操作系统。所以可以成功打印出想要的结果,并没有发生段错误。

但是大于128Kb的内存malloc底层是用系统调用mmap()函数分配的,free()后马上用munmap()函数归还给操作系统了。如果程序再次访问该内存就发生段错误,程序直接被操作系统结束了。



总结ptmalloc的内存申请和释放步骤:

malloc的步骤:

1.先在fastbins中找,如果能找到,从队列中取下后(不需要再置使用位为1)立刻返回;

2. 判断需求的块是否在small bins(bins的前64个bin)范围,如果在小箱子范围,并且刚好有满足需求的块,则直接返回内存地址;

3.到了这一步,说明需要分配的是一块大内存,或者小箱子里找不到合适的chunk;这个时候,会触发consolidate,ptmalloc首先会遍历fastbins中的chunk,将相邻的chunk合并,并链接到unsorted bin中(因为在大箱子找一般都要切割,所以要优先合并,避免过多碎片);

4. 在unsort bin中取出一个chunk,如果能找到刚好和想要的chunk相同大小的chunk,立刻返回,如果不是想要的chunk大小的chunk,就把它插入到bins对应的队列中去,转到2。

5.  到了这一步,说明需要分配的是一块大的内存,或者small bins和unsorted bin中都找不到合适的chunk,并且fastbins和unsorted bin中所有的chunk都清楚干净了。在large bins中找,找到一个最小的能符合需求的chunk从队列中取下,如果剩下的大小还能建一个chunk,就把chunk分成两个部分,把剩下的chunk插入到unsort队列中取,把chunk的内存地址返回;

6.  如果搜索fastbins和bins都没有找到合适的chunk,那么就需要操作topchunk(是堆顶的一个chunk,不会放在任何一个队列里)来进行分配了。在topchunk找,如果能切出符合要求的,把剩下的一部分当作topchunk,然后返回内存地址;

7. 到了这一步说明topchunk也不能满足分配要求,就只能调用sysalloc,其实就是增长堆了,然后返回内存地址。


free的步骤:

1.判断所需释放的chunk是否为mmaped chunk,如果是,则调用munmap释放mmaped chunk,解除内存空间映射,该空间不再有效,然后立刻返回;

2. 如果和topchunk相邻,直接和topchunk合并,不会放到其他的空闲队列中取,然后立刻返回;

3. 如果释放的大小小于max_fast(80字节),就把它挂到fastbins中去返回,使用位仍然为1,当然更不会去合并相邻块,然后立刻返回;

4.如果释放块得大小介于80—128K,把chunk的使用位置为0,判断前一个chunk是否处于使用中,如果前一块也是空闲块,则合并,并转入下一步;

5.判断当前释放chunk的下一个块是否为top chunk,如果是,则转到第7步,否则转下一步;

6.判断下一个chunk是否处在使用中,如果也是空闲的,则合并,并将合并后的chunk挂到unsort队列中去;

7.如果执行到了这一步,说明释放了一个与top chunk相邻的chunk;则无论它有多大,都将它与top chunk合并,并更新top chunk的大小等信息,转下一步;

8.如果合并后的大小大于FASTBIN_CONSOLIDATION_THRESHOLD(64K),也会触发consolidate,即fastbins的合并操作,合并后的chunk会被放到unsorted bin中,fastbins将变为空,操作完成之后转下一步;

9.试图收缩堆。(判断top chunk的大小是否大于mmap的收缩阈值,默认为128KB)

 

ptmalloc对于大于128K的块通过mmap方式来分配,小于128K(mmap分配阈值)的块在heap中分配。

堆是通过brk的方式来增加或压缩的,如果在现有的堆中不能找到合适的chunk,会通过增长堆的方式来满足分配,如果堆顶的空闲块超过一定的阈值会收缩堆,所以只要堆顶的空间没释放,堆是一直不会收缩的。

因为ptmalloc的内存收缩是从top chunk开始,如果与top chunk堆顶的一个chunk)相邻的那个chunk在内存池中没有释放,top chunk以下的空闲内存都无法返回给系统,即使这些空闲内存有几十个G也不行。这也是GLIBC内存暴增现象的原因。


猜你喜欢

转载自blog.csdn.net/run32875094/article/details/79365255
今日推荐