ptmalloc内存分配分析

来源:互联网 发布:宾得k3ii知乎 编辑:程序博客网 时间:2024/06/07 08:15

背景

系统内存布局

LINUX进程内存布局:

上图即为linux操作系统的内存布局。内存从低到高分别展示了操作系统各个模块的内存分布。
Text Segment:存放程序代码,只读,编译的时候确定。
Data Segment:存放程序运行的时候就能确定的数据,可读可写。
BBS Segment:定义而没有初始化的全局变量和静态变量。
Heap:堆。堆的内存地址由低到高。
Memory Mapping Region:映射区域。内存地址由低到高。
Stack:栈。编译器自动分配和释放。内存地址由高到低。

ptmalloc

内存分配策略

chunk 内存块的基本组织单元

ptmalloc通过chunk的数据结构来组织每个内存单元,chunk的结构可以分为使用中的chunk和空闲的chunk。使用中的chunk和空闲的chunk数据结构基本项同,但是会有一些设计上的小技巧,巧妙的节省了内存。
使用中的chunk:

1. chunk指针指向chunk开始的地址;mem指针指向用户内存块开始的地址。
2. p=0时,表示前一个chunk为空闲,prev_size才有效
3. p=1时,表示前一个chunk正在使用,prev_size无效 p主要用于内存块的合并操作
4. ptmalloc分配的第一个块总是将p设为1,以防止程序引用到不存在的区域
5. M=1 为mmap映射区域分配;M=0为heap区域分配
6. A=1 为非主分区分配;A=0 为主分区分配

空闲的chunk:

1. 空闲的chunk会被放置到空闲的链表bins上。当用户申请内存malloc的时候,会先去查找空闲链表bins上是否有合适的内存。
2. fp和bp分别指向前一个和后一个空闲链表上的chunk。
3. fp_nextsize和bp_nextsize分别指向前一个空闲chunk和后一个空闲chunk的大小,主要用于在空闲链表上快速查找合适大小的chunk。
4. fp、bp、fp_nextsize、bp_nextsize的值都会存在原本的用户区域,这样就不需要专门为每个chunk准备单独的内存存储指针了。

空闲链表bins:
当用户使用free函数释放掉的内存,ptmalloc并不会马上交还给操作系统(这边很多时候我们明明执行了free函数,但是进程内存并没有回收就是这个原因),而是被ptmalloc本身的空闲链表bins管理起来了,这样当下次进程需要malloc一块内存的时候,ptmalloc就会从空闲的bins上寻找一块合适大小的内存块分配给用户使用。这样的好处可以避免频繁的系统调用,降低内存分配的开销。

ptmalloc一共维护了128bin。每个bins都维护了大小相近的双向链表的chunk。
fast bins

fast bins是bins的高速缓冲区,大约有10个定长队列。当用户释放一块不大于max_fast(默认值64)的chunk(一般小内存)的时候,会默认会被放到fast bins上。当用户下次需要申请内存的时候首先会到fast bins上寻找是否有合适的chunk,然后才会到bins上空闲的chunk。ptmalloc会遍历fast bin,看是否有合适的chunk需要合并到bins上。

unsorted bin

unsorted bins是bins的第一个区域。如果在fast bins中没有找到合适的chunk,则会到unsorted bins中进行寻找。如果在unsorted bins中也未找到,将unsorted bins中的chunk放回到bins中,再到bins中寻找。所以其实unsorted bins可以看作bins的一个缓冲区。

small bins和large bins

small bins和large bins是真正用来放置chunk双向链表的。每个bin之间相差8个字节,并且通过上面的这个列表,可以快速定位到合适大小的空闲chunk。前64个为small bins,定长;后64个为large bins,非定长。

Top chunk

并不是所有的chunk都会被放到bins上。top chunk相当于分配区的顶部空闲内存,当bins上都不能满足内存分配要求的时候,就会来top chunk上分配。

mmaped chunk

当分配的内存非常大(大于分配阀值,默认128K)的时候,需要被mmap映射,则会放到mmaped chunk上,当释放mmaped chunk上的内存的时候会直接交还给操作系统。

内存分配malloc流程

(1)​获取分配区的锁。目的是为了防止多个线程同时访问同一个区域,在进行分配之前需要取得分配区域的锁。
(2)​将用户的请求大小转换成实际需要分配的chunk空间的大小。
(3)判断所需分配的chunk的大小是否满足chunk_size<= max_size,如果是则转到第4步,否则,转第5步。
(4)首先尝试在fast bins中取一个所需大小的chunk分配给用户。如果可以找到,则分配结束,否则,转到下一步。
(5)判断所需要的大小是否处在small bins 中,如果在small bins中则转下一步,否则,转第7步。
(6)根据所需要分配的​chunk的大小,找到具体所在的某个small bin,从该bin的尾部摘取一个恰好满足大小的chunk。若成功,则分配结束,否则,转下一步。
(7)到了这一步说明需要分配的是一块大内存,或者是small bin中找不到合适的chunk,于是,ptmalloc会遍历所有的fast bins中的chunk,将相邻的chunk进行合并,并连接到unsorted bin中,然后遍历unsorted bin中的chunk。​如果unsorted bin中只有一个chunk,并且这个chunk大于等于需要分配的大小,这种情况下就直接将该chunk切割,分配结束。否则将根据chunk的空间大小将其放入到相应的small bins 或者 large bins 中。否则,进行下一步。
(8)到了这一步说明分配的是一块很大的内存,或者是在unsorted bin和small bins中都没有找到合适的chunk,fast bins​和unsorted bin中所有的chunk都清除干净了,从large bins中找到一个合适的chunk,从中划分一块所需大小的chunk,并将剩下的部分连接回bins中,如果操作成功就结束分配,否则,转下一步。
(9)如果搜索bins都没有找到合适的chunk,那么需要操作top chunk来进行分配了。判断top chunk大小是否满足所需要的chunk的大小,如果是,则从top chunk中分出一块来。
(10)​到了这一步说明top chunk也不能满足分配需求。所以有两个选择,如果是主分配区,调用sbrk(),增加top chunk 的大小,如果是非主分配区,调用mmap()来分配一个新的sub_heap,增加top chunk大小,或者是使用mmap()来直接分配。需要根据chunk的大小来决定使用哪种方法。如果所需要分配的chunk大小大于等于mmap分配阀值,则转下一步使用mmap分配原则,否则转12步。
(11)使用mmap系统调用为程序的内存空间映射一块chunk_size align 4KB大小的空间。然后将内存指针返回给用户。
(12)判断是否是第一次调用malloc,若是主分配区,则需要进行一次初始化工作,分配一块大小为(chunk_size + 128kb)slign大小的空间作为初始化的heap。若已经初始化过了,主分配区则调用sbrk()增加heap空间,非主分配区则在top chunk 中切割一个chunk,使之满足分配需求,并将用户指针返回给用户。​

内存释放free流程

free()函数接受一个指向分配区域的指针作为参数,释放指针指向需要释放的chunk。
(1)free()函数首先需要获取分配区的锁来保证线程安全。
(2)判断传入的指针是否为0,如果为0,则什么都不做,直接return。否则转下一步。
(3)判断所需释放的chunk是否为mmaped chunk,如果是,则调用munmap()释放解除空间映射,该空间不再有效。
(4)​判断chunk的大小和所处的位置,若chunk_size<= max_fast,并且chunk并不处于heap的顶部,也就是说不与top chunk相邻,则转到下一步,否则转到第6步。
(5)​将chunk方到fast bins中,chunk放入到fast bins中时,并不修改该chunk使用状态位P,也不与相邻的chunk进行合并。只是放进去。这一步做完之后释放就结束了,程序从free()函数中返回。
(6)判断前一个chunk是否正在使用中,如果前一个块也是空闲块,则合并。并转下一步。
(7)判断当前释放的chunk的下一个块是否为top chunk,如果是,则转第9步,否则转下一步。
(8)判断下一个chunk是否处于使用中,如果下一个chunk也是空闲的,则合并,并将合并后的chunk放到unsorted bin中。注意,这里在合并过程中,要更新chunk的大小,以反映合并后的chunk的大小。并转到第10步。
(9)如果执行到这一步,说明释放了一个与top chunk相邻的chunk。则无论它有多大,都将它和top chunk合并,并更新top chunk的大小等信息。转下一步。
(10)判断合并后的chunk的大小是否会大于max_fast(默认是64kb)​,如果是的话,则会触发进行fast bins的合并操作,fast bins中的chunk将被遍历,并与相邻的chunk进行合并,合并后的chunk会被放到unsorted bin中。fast bins将变为空,操作完成后进入到下一步。
(11)​判断 top chunk的大小是否大于mmap收缩阀值(默认是128kb),如果是的话,对于主分配区,则会试图归还top chunk中的一部分给操作系统。但是最先分配的128KB空间是不会归还给操作系统的,ptmalloc会一直管理这部分内存,用来响应用户的分配请求。如果是非主分配区,会进行sub_heap收缩,将top chunk的一部分返回给操作系统,如果 top chunk是整个sub_heap,会将整个sub_heap归还给操作系统。做完这一步后,释放结束,从free()函数退出。

使用注意事项

为了避免Glibc内存暴增,需要注意:
1. 后分配的内存先释放,因为ptmalloc收缩内存是从top chunk开始,如果与top chunk相邻的chunk不能释放,top chunk以下的chunk都无法释放。
2. Ptmalloc不适合用于管理长生命周期的内存,特别是持续不定期分配和释放长生命周期的内存,这将导致ptmalloc内存暴增。
3. 多线程分阶段执行的程序不适合用ptmalloc,这种程序的内存更适合用内存池管理。
4. 尽量减少程序的线程数量和避免频繁分配/释放内存。频繁分配,会导致锁的竞争,最终导致非主分配区增加,内存碎片增高,并且性能降低。
5. 防止内存泄露,ptmalloc对内存泄露是相当敏感的,根据它的内存收缩机制,如果与top chunk相邻的那个chunk没有回收,将导致top chunk一下很多的空闲内存都无法返回给操作系统。
6. 防止程序分配过多内存,或是由于Glibc内存暴增,导致系统内存耗尽,程序因OOM被系统杀掉。预估程序可以使用的最大物理内存大小,配置系统的/proc/sys/vm/overcommit_memory,/proc/sys/vm/overcommit_ratio,以及使用ulimt –v限制程序能使用虚拟内存空间大小,防止程序因OOM被杀掉。