glic内存管理学习笔记
华庭师傅的glibc内存管理简直逆天,这里写一下笔记,更新中
内存经典布局
这里随便画一下QAQ
32位
Kernel | <- 0xc00000000==TASK_SIZE |
---|---|
<- Random stack offset | |
——- | |
stack | |
——- | |
<-Random mmap offset | |
——- | |
Memory Mapping region | <- 0x40000000 |
<- program break | |
——- | <- brk |
heap | |
——- | <- start brk |
BSS segment | |
——- | <- end data |
Data segment | <- start data |
——- | <- end code |
Text segment | <- 0x08048000 |
——- | |
<- 0 |
64位
Kernel | <- 0xFFFF800000000000 |
---|---|
undefine region | <- 0X00007FFFFFFFF000=TASK_SIZE |
——- | |
stack | |
——- | |
——- | |
Memory Mapping region | <- 0x00002AAAAAAAA000 |
——- | |
heap | |
——- | |
BSS segment | |
——- | |
Data segment | |
——- | |
Text segment | <- 0x00000000040000 |
——- | |
<- 0 |
内容设计假设
6个假设
- 对长生命周期的大内存分配使用mmap
- 特别大的内存分配总使用mmap
- 具有短生命周期的内存分配使用brk
- 尽量只缓存临时使用的空闲小内存块
- 空闲小内存块只会在malloc和free时合并,free时空闲内存块可能放入pool中,不一定归还给操作系统
- 收缩堆的条件
- 当前free块大小加上前后可合并chunk大于64kb
- 堆顶大小达到阈值
- 这样会把堆最顶端空闲内存返回给操作系统
记忆问题
- 收缩堆的条件有?
- 结果是?
- 都有哪些假设?
总结
- 大内存,长周期 -> mmap
- 短周期 -> brk
- 缓存的内存块尽可能时大小较小的
- 空闲块只有在分配和free的时候会合并
- free的时候不一定归还给操作系统(会成为cache)
- 空闲chunk大小 > 64KB 时合并chunk ->收缩堆
- 堆顶大小达到阈值 -> 收缩堆
- 收缩堆会将堆最顶端空间内存返回给操作系统
数据结构概述
Main_arena 与 non_main_arena
原文概述
- 在 Doug Lea 实现的内存分配器中只有一个主分配区(main arena)
- 每次分配内存都必须对主分配区加锁,分配完成后释放锁
- 在 SMP 多线程环境下,对主分配区的锁的争用很激烈,严重影响了malloc 的分配效率。于是Wolfram Gloger 在 Doug Lea 的基础上改进使得 Glibc 的malloc 可以支持多线程,增加了非主分配区(non main arena)支持
- 主分配区与非主分配区用环形链表进行管理。每一个分配区利用互斥锁(mutex)使线程对于该分配区的访问互斥。
- 每个进程只有一个主分配区,但可能存在多个非主分配区
- ptmalloc 根据系统对分配区的争用情况动态增加非主分配区的数量,分配区的数量一旦增加,就不会再减少了。
- 主分配 区可以访问进程的heap区域和mmap映射区域
- 主分配区可以使用 sbrk 和mmap 向操作系统申请虚拟内存。而非主分配区只能访问进程的 mmap 映射区域
- 非主分配区每次使用mmap()向操作系统“批发”HEAP_MAX_SIZE(32 位系统上默认为 1MB,64 位系统默 认为 64MB)大小的虚拟内存
- 当用户向非主分配区请求分配内存时再切割成小块“零售” 出去
- ptmalloc 在必要的情况下才会调用mmap()函数向操作系统申请虚拟内存。
- 主分配区可以访问 heap 区域,如果用户不调用 brk()或是 sbrk()函数,分配程序就可以保证分配到连续的虚拟地址空间,因为每个进程只有一个主分配区使用 sbrk()分配 heap 区域的虚拟内存。
- 内核对 brk 的实现可以看着是mmap 的一个精简版,相对高效一些。
- 如果主 分配区的内存是通过mmap()向系统分配的,当free该内存时,主分配区会直接调用munmap() 将该内存归还给系统。
- 当某一线程需要调用malloc()分配内存空间时,该线程先查看线程私有变量中是否已经存在一个分配区,如果存在,尝试对该分配区加锁,如果加锁成功,使用该分配区分配内存, 如果失败,该线程搜索循环链表试图获得一个没有加锁的分配区。如果所有的分配区都已经 加锁那么malloc()会开辟一个新的分配区,把该分配区加入到全局分配区循环链表并加锁, 然后使用该分配区进行分配内存操作。在释放操作中,线程同样试图获得待释放内存块所在 分配区的锁,如果该分配区正在被别的线程使用,则需要等待直到其他线程释放该分配区的 互斥锁之后才可以进行释放操作。
- 申请小块内存时会产生很多内存碎片,ptmalloc 在整理时也需要对分配区做加锁操作。
- 每个加锁操作大概需要 5~10 个 cpu 指令,而且程序线程很多的情况下,锁等待的时间就会 延长,导致malloc性能下降。一次加锁操作需要消耗100ns左右,正是锁的缘故,导致ptmalloc 在多线程竞争情况下性能远远落后于 tcmalloc。最新版的 ptmalloc 对锁进行了优化,加入了 PER_THREAD和 ATOMIC_FASTBINS 优化,但默认编译不会启用该优化,这两个对锁的优化应 该能够提升多线程内存的分配的效率。
问题
- 该结构有什么特点?
- 主分配区和非主分配区的区别是什么?
- 两者不同的分配方式分别时如何分配的?
总结
- main_arena(MA)为主分配区;non_main_arena(N_MA)为非主分配区
- 两者通过环形链表进行管理,每个分区采用互斥锁使线程的访问互斥
- 每个分配区有一个MA,可能有多个N_MA;
- N_MA数里只加不减
- MA可访问进程的heap和mmap映射区域
- MA可以使用Sbrk和mmap来向系统申请内存
- N_MA只能访问进程的MMAP映射区域
- 主分配区可访问heap区域,若用户不调用brk()/sbrk(),将可分配到连续的虚拟内存地址
- malloc时先看线程是否有分配区,存在则先加锁,成功则分配内存,否则开辟新分配区,加入环形链表并加锁
chunk的组织
不管内存是在哪里被分配的,用什么方法分配,用户请求分配的空间在 ptmalloc 中都使用一个 chunk 来表示。用户调用 free()函数释放掉的内存也并不是立即就归还给操作系统, 相反,它们也会被表示为一个chunk,ptmalloc使用特定的数据结构来管理这些空闲的chunk
chunk格式
使用中的chunk
Size of pre chunk | <-chunk |
---|---|
Size of chunk,in bytes A/M/P | |
——- | <-mem |
user data | |
——- | <- next chunk |
Size of chunk,in bytes |
- 在图中,chunk指针指向一个chunk的开始,一个chunk中包含了用户请求的内存区域和相关的控制信息。
- 图中的mem指针才是真正返回给用户的内存指针。
- chunk的第二个域的最低一位为P,它表示前一个块是否在使用中,P为0则表示前一个chunk为空闲,这时 chunk的第一个域prev_size才有效
- prev_size表示前一个chunk的size,程序可以使用这个值来找到前一个chunk的开始地址。当P为1时,表示前一个chunk正在使用中,prev_size无效,程序也就不可以得到前一个chunk的大小。不能对前一个chunk进行任何操作。ptmalloc分配的第一个块总是将P设为1,以防止程序引用到不存在的区域。
- Chunk的第二个域的倒数第二个位为M,他表示当前chunk是从哪个内存区域获得的虚拟内存。M为1表示该chunk是从mmap映射区域分配的,否则是从heap区域分配的。
- Chunk的第二个域倒数第三个位为A,表示该chunk属于主分配区或者非主分配区,如果属于非主分配区,将该位置为1,否则置为0。
空闲的chunk
Size of pre chunk | <-chunk |
---|---|
Size of chunk,in bytes A/ /P | |
——- | <-mem |
*fd | |
*bk | |
*fd_next size | |
*bk_next size | |
user data | |
——- | <- next chunk |
Size of chunk,in bytes |
- 当chunk空闲时,其M状态不存在,只有AP状态,
- 原本是用户数据区的地方存储了四个指针,指针fd指向后一个空闲的chunk,而bk指向前一个空闲的chunk,ptmalloc通过这两个指针将大小相近的chunk连成一个双向链表。
- 对于large bin中的空闲chunk,还有两个指针,fd_nextsize和bk_nextsize,这两个指针用于加快在large bin中查找最近匹配的空闲chunk。不同的chunk链表又是通过bins或者fastbins来组织的
问题
- 使用中的chunk和空闲时有什么不同?
- chunk的第二个域倒数几位都各自代表了什么?
- 两者各自域都代表了什么?
总结
chunk in use
- chunk指针指向chunk的开始,但是mem指针才是返回给用户的内存指针
- chunk第二个域的倒数三位代表
- A -> 是不是主分配区 .
- 0-> MA ;
- 1-> N_MA;
- M -> 是不是由mmap分配的
- 0-> heap
- 1-> mmap;
- P -> 前一个chunk是否在使用中
- 0-> 空闲 此时prev_size有效
- 1-> 使用中, 此时禁止对前一个chunk有任何操作,无法得到其大小
- A -> 是不是主分配区 .
free chunk
- chunk空闲时,M态不存在,只有AP状态
- 此时用户区存储了四个指针
- fd :指向后一个空闲的chunk
- bk :指向前一个空闲的chunk
- fd_nextsize :只有在large bin中才使用,因为相同大小的chunk可能有很多,因此为了方便查找,会指向后一个比他小的chunk
- bk_nextsize :上同,但是会指向前一个比他小的chunk
chunk的空间复用
内容
- 为了使得 chunk 所占用的空间最小,ptmalloc使用了空间复用
- 一个 chunk或者正在被使用,或者已经被free掉,所以chunk的中的一些域可以在使用状态和空闲状态表示不同的意义,来达到空间复用的效果。
- 以32位系统为例,
- 空闲时,一个chunk中至少需要4个size_t(4B)大小的空间,用来存储prev_size,size,fd和bk (见上图),也就是16B,chunk的大小要对齐到8B。
- 当一个chunk处于使用状态时,它的下一个chunk的prev_size域肯定是无效的。所以实际上,这个空间也可以被当前chunk使用
- 故而实际上,一个使用中的chunk的大小的计算公式应该是:in_use_size = (用户请求大小+ 8 - 4 ) align to 8B,这里加8是因为需要存储prev_size和size,但又因为向下一个chunk“借”了4B,所以要减去4。
- 最后,因为空闲的chunk 和使用中的chunk使用的是同一块空间。所以肯定要取其中最大者作为实际的分配空间。即最终的分配空间chunk_size = max(in_use_size, 16)。
- 这就是当用户请求内存分配时,ptmalloc 实际需要分配的内存大小,在后面的介绍中。如果不是特别指明的地方,指的都是这个经过转换的实际需要分配的内存大小,而不是用户请求的内存分配大小。
问题
- 如何空间复用?
- 用户请求分配内存后会实际得到多少内存?
总结
- P为1时,pre chunk size归上一个 chunk使用
- size of(chunk in use )=(用户请求大小 + 8 -4 ) align to 8B
- size of (free)min =16B
空闲chunk容器
bins
用户free掉的内存并不是都会马上归还给系统,ptmalloc会统一管理heap和mmap映射区域中的空闲的chunk,当用户进行下一次分配请求时,ptmalloc会首先试图在空闲的chunk中挑选一块给用户,这样就避免了频繁的系统调用,降低了内存分配的开销。ptmalloc将相似大小的chunk用双向链表链接起来,这样的一个链表被称为一个bin。Ptmalloc一共维护了128个bin,并使用一个数组来存储这些bin
概况
- 数组中的第一个为unsorted bin,
- 数组中从2开始编号的前64个bin称为small bins
- 同一个small bin中的chunk具有相同的大小。
- 两个相邻的small bin中的chunk大小相差8bytes。
- small bins中的chunk按照最近使用顺序进行排列,最后释放的chunk被链接到链表的头部,而申请chunk是从链表尾部开始,这样,每一个chunk 都有相同的机会被ptmalloc选中。
- Small bins后面的bin被称作large bins。large bins中的每一个bin分别包含了一个给定范围内的chunk,其中的chunk按大小序排列。相同大小的chunk同样按照最近使用顺序排列。
- ptmalloc使用“smallest-first,best-fit”原则在空闲large bins中查找合适的chunk。
- 当空闲的chunk被链接到bin中的时候,ptmalloc会把表示该chunk是否处于使用中的标志P设为0(注意,这个标志实际上处在下一个chunk中)同时ptmalloc还会检查它前后的chunk是否也是空闲的,如果是的话,ptmalloc会首先把它们合并为一个大的chunk,然后将合并后的chunk放到unstored bin中。
- 要注意的是,并不是所有的chunk被释放后就立即被放到bin中。
- ptmalloc为了提高分配的速度,会把一些小的的chunk先放到一个叫做fast bins的容器内。
fast bin
- 程序在运行时会经常需要申请和释放一些较小的内存空间。当分配器合并了相邻的几个小的chunk之后,也许马上就会有另一个小块内存的请求,这样分配器又需要从大的空闲内存中切分出一块,这样无疑是比较低效的,故而,ptmalloc中在分配过程中引入了fast bins,不大于max_fast (默认值为64B)的chunk被释放后,首先会被放到fast bins
- fast bins中的chunk并不改变它的使用标志P。这样也就无法将它们合并
- 当需要给用户分配的chunk小于或等于max_fast时,ptmalloc首先会在fast bins中查找相应的空闲块,然后才会去查找bins中的空闲chunk。
- 在某个特定的时候,ptmalloc会遍历fast bins中的chunk,将相邻的空闲chunk进行合并,并将合并后的chunk加入unsorted bin中,然后再将usorted bin里的chunk加入bins中。
Unsorted Bin
- unsorted bin的队列使用bins数组的第一个
- 如果被用户释放的chunk大于max_fast,或者fast bins中的空闲chunk合并后,这些chunk首先会被放到unsorted bin队列中
- 在进行malloc操作的时候,如果在fast bins中没有找到合适的chunk,则ptmalloc会先在unsorted bin中查找合适的空闲chunk,然后才查找bins。
- 如果unsorted bin不能满足分配要求。malloc便会将unsorted bin中的chunk加入bins中。然后再从bins中继续进行查找和分配过程。
- unsorted bin可以看做是bins的一个缓冲区,增加它只是为了加快分配的速度。
并不是所有的chunk都按照上面的方式来组织,实际上,有三种例外情况。Top chunk,mmaped chunk和last remainder
Top chunk
top chunk对于主分配区和非主分配区是不一样的。
- 非主分配区
- 对于非主分配区会预先从mmap区域分配一块较大的空闲内存模拟sub-heap,通过管理sub-heap来响应用户的需求
- 因为内存是按地址从低向高进行分配的,在空闲内存的最高处,必然存在着一块空闲chunk,叫做top chunk。
- 当bins和fast bins都不能满足分配需要的时候,ptmalloc会设法在top chunk中分出一块内存给用户 .如果top chunk本身不够大,分配程序会重新分配一个sub-heap,并将top chunk迁移到新的sub-heap上,新的sub-heap与已有的sub-heap用单向链表连接起来,然后在新的top chunk上分配所需的内存以满足分配的需要
- 实际上,top chunk在分配时总是在fast bins和bins之后被考虑,所以,不论top chunk有多大,它都不会被放到fast bins或者是bins中。
- Top chunk的大小是随着分配和回收不停变换的,如果从top chunk分配内存会导致top chunk减小,如果回收的chunk恰好与top chunk相邻,那么这两个chunk就会合并成新的top chunk,从而使top chunk变大。
- 如果在free时回收的内存大于某个阈值,并且top chunk的大小也超过了收缩阈值,ptmalloc会收缩sub-heap,如果top-chunk包含了整个sub-heap,ptmalloc会调用munmap把整个sub-heap的内存返回给操作系统。
- 主分配区
- 由于主分配区是唯一能够映射进程heap区域的分配区,它可以通过sbrk()来增大或是收缩进程heap的大小
- ptmalloc在开始时会预先分配一块较大的空闲内存(也就是所谓的 heap),主分配区的top chunk在第一次调用malloc时会分配一块(chunk_size + 128KB) align 4KB大小的空间作为初始的heap
- 用户从top chunk分配内存时,可以直接取出一块内存给用户。在回收内存时,回收的内存恰好与top chunk相邻则合并成新的top chunk,当该次回收的空闲内存大小达到某个阈值,并且top chunk的大小也超过了收缩阈值,会执行内存收缩,减小top chunk的大小,但至少要保留一个页大小的空闲内存,从而把内存归还给操作系统。
- 如果向主分配区的top chunk申请内存,而top chunk中没有空闲内存,ptmalloc会调用sbrk()将的进程heap的边界brk上移,然后修改top chunk的大小。
mmaped chunk
当需要分配的chunk足够大,而且fast bins和bins都不能满足要求,甚至top chunk本身也不能满足分配需求时,ptmalloc会使用mmap来直接使用内存映射来将页映射到进程空间。
chunk在被free时将直接解除映射,于是就将内存归还给了操作系统,再次对这样的内存区的引用将导致segmentation fault错误。这样的chunk也不会包含在任何bin中
Last remainder
- Last remainder是另外一种特殊的chunk,就像top chunk和mmaped chunk一样,不会在任何bins中找到这种chunk。
- 当需要分配一个small chunk,但在small bins中找不到合适的chunk,如果last remainder chunk的大小大于所需的small chunk大小,last remainder chunk被分裂成两个chunk,其中一个chunk返回给用户,另一个chunk变成新的last remainder chuk
问题
- bins是以何种结构存在的?
- 都有哪些bins?各自的特点如何?
- 有哪三种不同的bins?各自在什么地方使用?
- top chunk的主分配区和非主分配区在使用时有什么不一样?
总结
bin
- 用户free掉的内存不都还给系统,ptmalloc会将相似大小的chunk用双向链表连起来,这就是Bin
- 一共维护有128个bin,第一个为unsorted bins
- 2-64为small bins
- 两相邻大小bins相差8bytes(small bins)
- 除了FAST BINS 都使用 FIFO原则
- 64-128为large bins ,每一个大小都在一定范围内,按大小排序,一样大的类似small bins使用
- 空闲的chunk连入bin时,会将 P 设为 0 , 并检查前后chunk是否空闲,若空闲则合并后加入unsorted bins中
fast bins
- size < max_fast的chunk被free后先放入fastbins中,且不改变P
- 用户所要Chunk < max_fast 时先便利fast bin
- 有时会便利fast bin,将相邻的chunk合并后放入unsorted bin中,然后将unsorted中的chunk 加入bin中
unsorted bins
- 使用bins数组的第一个,若free的chunk>max_fast,或fast bins 合并后,先放入unsorted bin中
- malloc时若fastbin中没有合适的,就现在unsorted中找,最后才是bins
- 若unsorted也不能满足分配,malloc会将其内的chunk放入bins中再分配
- 可以看作时bins的缓冲区,用来加快分配速度
top chunk
- 主分配区和非主分配区不一样
- N_MA
- 预先从mmap中分配一块较大的空闲内存模拟sub-heap(内存从低到高分配)
- 内存最高处必有空闲chunk为Top chunk
- bins和fast bins都没有满足的时,会在TOP中分配
- 永远不会放入fastbins和bins中
- 若回收的chunk和top相连,会将它们合并,若大小大于某只且top大于收缩阈值,将收缩sub-heap,返回给系统
- MA
- 先预先分配空闲内存(sub-heap)
- MA的top首先malloc时会分配(chunk+128KB)align 4KB的空间为初始HEAP
- 回收大于阈值,top也大于阈值,则内存收缩.减少TOP但至少保留一页大小,剩下的归还给系统
- 申请内存时,top无空闲,将会用sbrk上移heap边界
- 若本身不够大,则重新分配sub-heap,并将top迁移到新的sub-heap上,以链表连接,重新分配
mmap chunk
- 若分配内存太大,且fast和bins中都不能满足时,甚至连TOP也不够时,用mmap直接映射页到进程空间
- 分配的chunk被free时直接解除映射,还给系统
last remainder
- 需要分配small bins,但small中无合适的chunk时,若last大小大于所需的small chunk,将会分裂成两个chunk,一个给用户,一个变成新的last
sbrk和mmap
内容
- 从进程的内存布局可知,.bss 段之上的这块分配给用户程序的空间被称为 heap (堆)。
- start_brk 指向 heap 的开始,而 brk 指向 heap 的顶部。
- 可以使用系统调用 brk()和 sbrk()来增 加标识 heap 顶部的 brk 值,从而线性的增加分配给用户的 heap 空间。
- 在使用malloc 之前, brk的值等于start_brk,也就是说heap大小为0。
- ptmalloc在开始时,若请求的空间小于 mmap 分配阈值(mmap threshold,默认值为 128KB)时
- 主分配区会调用 sbrk()增加一块大小为 (128 KB + chunk_size) align 4KB 的空间作为 heap。
- 非主分配区会调用 mmap 映射一块大小为 HEAP_MAX_SIZE(32 位系统上默认为1MB,64 位系统上默认为64MB)的空间作为 sub-heap。 这就是前面所说的 ptmalloc 所维护的分配空间,
- 当用户请求内存分配时,首先会在这个区域内找一块合适的 chunk 给用户。
- 当用户释放了 heap 中的 chunk 时,ptmalloc 又会使用 fast bins 和 bins 来组织空闲 chunk。以备用户的下一次分配。
- 若需要分配的 chunk 大小小于mmap 分配阈值,而 heap 空间又不够
- 此时主分配区会通过 sbrk()调用来增加 heap 大小
- 非主分配区会调用mmap 映射一块新的 sub-heap,也就是增加 top chunk 的大小,每次 heap 增 加的值都会对齐到 4KB。
- 当用户的请求超过mmap 分配阈值,并且主分配区使用 sbrk()分配失败的时候,或是非主分配区在 top chunk 中不能分配到需要的内存时,ptmalloc 会尝试使用mmap()直接映射一块内存到进程内存空间。使用 mmap()直接映射的 chunk 在释放时直接解除映射,而不再属于进程的内存空间。任何对该内存的访问都会产生段错误。而在 heap 中或是 sub-heap中分配的空间则可能会留在进程内存空间内,还可以再次引用(当然是很危险的)。
- 当 ptmalloc munmap chunk 时,如果回收的 chunk 空间大小大于mmap 分配阈值的当前值,并且小于 DEFAULT_MMAP_THRESHOLD_MAX(32 位系统默认为 512KB,64 位系统默认 为 32MB),ptmalloc 会把 mmap 分配阈值调整为当前回收的 chunk 的大小,并将 mmap 收 缩阈值(mmap trim threshold)设置为mmap 分配阈值的 2 倍。这就是 ptmalloc 的对mmap 分配阈值的动态调整机制,该机制是默认开启的,当然也可以用mallopt()关闭该机制。
问题
- 有什么不同的机制?
- sbrk和mmap的区别是?
总结
- ptmalloc开始时,若小于mmap的分配阈值(128KB)
- 则主分配区用sbrk增加一块大小为(128KB+chunk_size) align 4KB的内存作为heap
- 非主分配区会使用mmap映射一块大小为HEAP_SIZE(32-1MB,64-64MB)的空间为sub-heap
- 请求分配时,现在期内寻找相应的chunk
- 用户free chunk后会放入fast bins中
- 若chunk < mmap的分配阈值,但heap空间不足时
- 主分配区会使用sbrk来增加大小
- 非主会用mmap映射新的sub-heap(对齐到4KB)
- 若chunk >mmap的分配阈值,但主分配区sbrk失败,非主分配区top chunk不足,就会用mmap映射内存
- ptmalloc munmap chunk时 ,DEFAULT_MMAP_THRESHOLD_MAX(32 - 512kb , 64 - 32MB) > 回收的chunk > mmap分配阈值时
- 将把mmap分配阈值调整为当前回收的chunk大小
- 将mmap收缩阈值设置为mmap分配阈值的2倍
内存分配概述
分配算法概述,以 32 系统为例,64 位系统类似。
内容
- 小于等于 64 字节:用 pool 算法分配。
- 64 到 512 字节之间:在最佳匹配算法分配和 pool 算法分配中取一种合适的。
- 大于等于 512 字节:用最佳匹配算法分配。
- 大于等于mmap 分配阈值(默认值 128KB):根据设置的 mmap 的分配策略进行分配
- 如果没有开启mmap分配阈值的动态调整机制,大于等于 128KB 就直接调用mmap分配。
- 否则,大于等于mmap分配阈值时才直接调用mmap()分配。
分配的具体步骤
- 获取分配区的锁,为了防止多个线程同时访问同一个分配区,在进行分配之前需要 取得分配区域的锁。线程先查看线程私有实例中是否已经存在一个分配区,如果存 在尝试对该分配区加锁,如果加锁成功,使用该分配区分配内存,否则,该线程搜 索分配区循环链表试图获得一个空闲(没有加锁)的分配区。如果所有的分配区都 已经加锁,那么 ptmalloc 会开辟一个新的分配区,把该分配区加入到全局分配区循 环链表和线程的私有实例中并加锁,然后使用该分配区进行分配操作。开辟出来的 新分配区一定为非主分配区,因为主分配区是从父进程那里继承来的。开辟非主分 配区时会调用mmap()创建一个 sub-heap,并设置好 top chunk。
- 将用户的请求大小转换为实际需要分配的 chunk 空间大小。
- 判断所需分配chunk的大小是否满足chunk_size <= max_fast (max_fast 默认为 64B), 如果是的话,则转下一步,否则跳到第 5 步。
- 首先尝试在 fast bins 中取一个所需大小的 chunk 分配给用户。如果可以找到,则分 配结束。否则转到下一步。
- 判断所需大小是否处在 small bins 中,即判断 chunk_size < 512B 是否成立。如果 chunk 大小处在 small bins 中,则转下一步,否则转到第 6 步。
- 根据所需分配的 chunk 的大小,找到具体所在的某个 small bin,从该 bin 的尾部摘 取一个恰好满足大小的 chunk。若成功,则分配结束,否则,转到下一步。
- 到了这一步,说明需要分配的是一块大的内存,或者 small bins 中找不到合适的 chunk。于是,ptmalloc 首先会遍历 fast bins 中的 chunk,将相邻的 chunk 进行合并, 并链接到 unsorted bin 中,然后遍历 unsorted bin 中的 chunk,如果 unsorted bin 只 有一个 chunk,并且这个 chunk 在上次分配时被使用过,并且所需分配的 chunk 大 小属于 small bins,并且 chunk 的大小大于等于需要分配的大小,这种情况下就直 接将该 chunk 进行切割,分配结束,否则将根据 chunk 的空间大小将其放入 small bins 或是 large bins 中,遍历完成后,转入下一步。
- 到了这一步,说明需要分配的是一块大的内存,或者 small bins 和 unsorted bin 中 都找不到合适的 chunk,并且 fast bins 和 unsorted bin 中所有的 chunk 都清除干净 了。从 large bins 中按照“smallest-first,best-fit”原则,找一个合适的 chunk,从 中划分一块所需大小的 chunk,并将剩下的部分链接回到 bins 中。若操作成功,则 分配结束,否则转到下一步。
- 如果搜索 fast bins 和 bins 都没有找到合适的 chunk,那么就需要操作 top chunk 来 进行分配了。判断 top chunk 大小是否满足所需 chunk 的大小,如果是,则从 top chunk 中分出一块来。否则转到下一步。
- 到了这一步,说明 top chunk 也不能满足分配要求,所以,于是就有了两个选择: 如 果是主分配区,调用 sbrk(),增加 top chunk 大小;如果是非主分配区,调用mmap 来分配一个新的 sub-heap,增加 top chunk 大小;或者使用mmap()来直接分配。在 这里,需要依靠 chunk 的大小来决定到底使用哪种方法。判断所需分配的 chunk 大小是否大于等于 mmap 分配阈值,如果是的话,则转下一步,调用mmap 分配, 否则跳到第 12 步,增加 top chunk 的大小。
- 使用mmap 系统调用为程序的内存空间映射一块 chunk_size align 4kB 大小的空间。 然后将内存指针返回给用户。
- 判断是否为第一次调用malloc,若是主分配区,则需要进行一次初始化工作,分配一块大小为(chunk_size + 128KB) align 4KB 大小的空间作为初始的 heap。若已经初 始化过了,主分配区则调用 sbrk()增加 heap 空间,分主分配区则在 top chunk 中切 割出一个 chunk,使之满足分配需求,并将内存指针返回给用户。
问题
- 用了什么算法?
- 分配的步骤大概为?
总结
这里用了华庭师傅的总结
- 根据用户请求分配的内存的大小,ptmalloc 有可能会在两个地方为用户分配内存空间。
- 在第一次分配内存时,一般情况下只存在一个主分配区,但也有可能从父进程那里继承来了多个非主分配区,在这里主要讨论主分配区的情况.
- 开始时brk 值等于 start_brk,所以实际上 heap 大小为 0,top chunk 大小也是 0。这时,如果不增加 heap 大小,就不能满足任何分配要求。
- 若用户的请求的内存大小小于mmap 分配阈值, 则 ptmalloc 会初始 heap。然后在 heap 中分配空间给用户,以后的分配就基于这个 heap 进行。
- 若第一次用户的请求就大于 mmap 分配阈值,则 ptmalloc 直接使用 mmap()分配 一块内存给用户,而 heap 也就没有被初始化,直到用户第一次请求小于 mmap 分配阈 值的内存分配。
- 第一次以后的分配就比较复杂了
- 简单说来,ptmalloc首先会查找fast bins
- 如果不能找到匹配的 chunk,则查找 small bins。
- 若还是不行,合并 fast bins,把 chunk 加入 unsorted bin,在 unsorted bin 中查找
- 若还是不行,把 unsorted bin 中的 chunk 全 加入 large bins 中,并查找 large bins。
- 在 fast bins 和 small bins 中的查找都需要精确匹配, 而在 large bins 中查找时,则遵循“smallest-first,best-fit”的原则,不需要精确匹配。
- 若以上方法都失败了,则 ptmalloc 会考虑使用 top chunk。
- 若 top chunk 也不能满足分配 要求。而且所需 chunk 大小大于 mmap 分配阈值,则使用 mmap 进行分配。否则增加 heap,增大 top chunk。以满足分配要求
内存回收概述
free() 函数接受一个指向分配区域的指针作为参数,释放该指针所指向的 chunk。而具体的释放方法则看该 chunk 所处的位置和该 chunk 的大小
工作步骤
- free()函数同样首先需要获取分配区的锁,来保证线程安全。
- 判断传入的指针是否为 0,如果为 0,则什么都不做,直接 return。否则转下一步。
- 判断所需释放的 chunk 是否为 mmaped chunk,如果是,则调用 munmap()释放 mmaped chunk,解除内存空间映射,该该空间不再有效。如果开启了mmap 分配 阈值的动态调整机制,并且当前回收的 chunk 大小大于mmap 分配阈值,将mmap 分配阈值设置为该 chunk 的大小,将 mmap 收缩阈值设定为 mmap 分配阈值的 2 倍,释放完成,否则跳到下一步。
- 判断 chunk 的大小和所处的位置,若 chunk_size <= max_fast,并且 chunk 并不位于 heap 的顶部,也就是说并不与 top chunk 相邻,则转到下一步,否则跳到第 6 步。 (因为与 top chunk 相邻的小 chunk 也和 top chunk 进行合并,所以这里不仅需要 判断大小,还需要判断相邻情况)
- 将 chunk 放到 fast bins 中,chunk 放入到 fast bins 中时,并不修改该 chunk 使用状 态位 P。也不与相邻的 chunk 进行合并。只是放进去,如此而已。这一步做完之后 释放便结束了,程序从 free()函数中返回。
- 判断前一个 chunk 是否处在使用中,如果前一个块也是空闲块,则合并。并转下一 步。
- 判断当前释放 chunk 的下一个块是否为 top chunk,如果是,则转第 9 步,否则转 下一步。
- 判断下一个 chunk 是否处在使用中,如果下一个 chunk 也是空闲的,则合并,并将合并后的 chunk 放到 unsorted bin 中。注意,这里在合并的过程中,要更新 chunk 的大小,以反映合并后的 chunk 的大小。并转到第 10 步。
- 如果执行到这一步,说明释放了一个与 top chunk 相邻的 chunk。则无论它有多大, 都将它与 top chunk 合并,并更新 top chunk 的大小等信息。转下一步。
- 判断合并后的 chunk 的大小是否大于 FASTBIN_CONSOLIDATION_THRESHOLD(默认 64KB),如果是的话,则会触发进行 fast bins 的合并操作,fast bins 中的 chunk 将被 遍历,并与相邻的空闲 chunk 进行合并,合并后的 chunk 会被放到 unsorted bin 中。 fast bins 将变为空,操作完成之后转下一步。
- 判断 top chunk 的大小是否大于mmap 收缩阈值(默认为 128KB),如果是的话,对 于主分配区,则会试图归还 top chunk 中的一部分给操作系统。但是最先分配的 128KB 空间是不会归还的,ptmalloc 会一直管理这部分内存,用于响应用户的分配 请求;如果为非主分配区,会进行 sub-heap 收缩,将 top chunk 的一部分返回给操作系统,如果 top chunk 为整个 sub-heap,会把整个 sub-heap 还回给操作系统。做 完这一步之后,释放结束,从 free() 函数退出。可以看出,收缩堆的条件是当前 free 的 chunk 大小加上前后能合并 chunk 的大小大于 64k,并且要 top chunk 的大 小要达到mmap 收缩阈值,才有可能收缩堆。
问题
- 概述一下大概过程?
- 有什么特点?
总结
- 先获取锁,然后判断指针是否为0,为0就直接return
- 否则判断是否时mmap分配的,是就直接删除映射,进行动态阈值调整
- 否则就判断chunk的大小和位置,如果 chunk < max_fast 并且不位于heap顶部,就放入fast bins中
- 否则判断前一个chunk是否在使用中,如果前一个也是空闲块就合并然后放入unsorted bins中
- 判断是否大于FASTBIM_CONNSOLIDATION_THRESHOLD,若大于就合并fast bins然后放入unsorted中,此时Fast bins变为空
- 判断top chunk大小是否大于mmap收缩阈值,若是
- 主分配区就归还top chunk的一部分给系统,开始的128KB不会归还
- 非主分配区会进行sub-heap的收缩,将其中一部分归还给操作系统,若top chunk为整个sub-heap就全部归还
- 如果chunk与top chunk相邻也要如此操作
关于源代码的相关分析部分这里就不记了,读完华庭师傅的文章收益良多,可能得多读几遍才能领会其中奥秘
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!