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

29 篇文章 1 订阅

基础知识:

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内存暴增现象的原因。


本文通过Glibc内存暴增问题,主要介绍了系统的内存管理问题,具体如下: 目录 1. 问题 2. 基础知识 2.1 X86平台Linux进程内存布局 2.1.1 32位模式下进程内存经典布局 2.1.2 32位模式下进程默认内存布局 2.1.3 64位模式下进程内存布局 2.2 操作系统内存分配的相关函数 2.2.1 Heap操作相关函数 2.2.2 Mmap映射区域操作相关函数 3. 概述 3.1 内存管理一般性描述 3.1.1 内存管理的方法 3.1.2 内存管理器的设计目标 3.1.3 常见C内存管理程序 3.2 Ptmalloc内存管理概述 3.2.1 简介 3.2.2 内存管理的设计假设 3.2.3 内存管理数据结构概述 3.2.4 内存分配概述 3.2.5 内存回收概述 3.2.6 配置选项概述 3.2.7 使用注意事项 4. 问题分析及解决 5. 源代码分析 5.1 边界标记法 5.2 分箱式内存管理 5.2.1 Small bins 5.2.2 Large bins 5.2.3 Unsorted bin 5.2.4 Fast bins 5.3 核心结构体分析 5.3.1 malloc_state 5.3.2 Malloc_par 5.3.3 分配区的初始化 5.4 配置选项 5.5 Ptmalloc的初始化 5.5.1 Ptmalloc未初始化时分配/释放内存 5.5.2 ptmalloc_init()函数 5.5.3 ptmalloc_lock_all(),ptmalloc_unlock_all(),ptmalloc_unlock_all2() 5.6 多分配区支持 5.6.1 Heap_info 5.6.2 获取分配区 5.6.3 Arena_get2() 5.6.4 _int_new_arena() 5.6.5 New_heap() 5.6.6 get_free_list()和reused_arena() 5.6.7 grow_heap(),shrink_heap(),delete_heap(),heap_trim() 5.7 内存分配malloc 5.7.1 public_mALLOc() 5.7.2 _int_malloc() 5.8 内存释放free 5.8.1 Public_fREe() 5.8.2 _int_free() 5.8.3 sYSTRIm()和munmap_chunk(
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值