ptmalloc

内存管理器的设计目标:

1.最大兼容性
2.最大可移植性
3.浪费最小空间
4.最快的速度
5.最大可调式性

ptmalloc折中上述目标:

  1. 具有长生命周期的大内存分配使用mmap。
  2. 特别大的内存分配总是使用mmap。
  3. 具有短生命周期的内存分配使用brk。
  4. 尽量只缓存临时使用的空闲小内存块,对大内存块或是长生命周期的大内存块在释放时都直接归还给操作系统。
  5. 对空闲的小内存块只会在malloc和free的时候进行合并,free时空闲内存块可能放入pool中,不一定归还给操作系统。
  6. 收缩堆的条件是当前free的块大小加上前后能合并chunk的大小大于64KB、,并且堆顶的大小达到阈值,才有可能收缩堆,把堆最顶端的空闲内存返回给操作系统。
  7. 需要保持长期存储的程序不适合用ptmalloc来管理内存。
  8. 不停的内存分配ptmalloc会对内存进行切割和合并,会导致一定的内存碎片

brk()和mmap()

brk():
--
 .bss 段之上的这块分配给用户程序的空间被称为 heap (堆). start_brk 指向 heap 的开始, 而 brk 指向 heap 的顶部. 可以使用系统调用 brk 和 sbrk 来增加标识 heap 顶部的 brk 值, 从而线性的增加分配给用户的 heap 空间. 在使用malloc之前, brk 的值等于start_brk, 也就是说 heap 大小为0. ptmalloc 在开始时, 若请求的空间小于 DEFAULT_MMAP_THRESHOLD (128K bytes)时, ptmalloc 会调用sbrk增加一块大小为 ( 128 KB + chunk_size ) align 4K 的空间作为heap. 这就是前面所说的 ptmalloc 所维护的分配空间, 当用户请求内存分配时, 首先会在这个区域内找一块合适的 chunk 给用户. 当用户释放了 heap 中的 chunk 时, ptmalloc 又会使用 fastbins 和 bins 来组织空闲 chunk.  若需要分配的 chunk 大小小于 DEFAULT_MMAP_THRESHOLD, 而 heap 空间又不够, 则此时 ptmalloc 会通过 sbrk 调用来增加 heap 值, 也就是增加 “top chunk”的大小, 每次 heap 增加的值都会 align 到4k bytes.

mmap():
--
用户的请求超过 DEFAULT_MMAP_THRESHOLD , 并且使用 sbrk 分配失败的时候, ptmalloc 会尝试使用 mmap 直接映射一块内存到进程内存空间(我机器上是在0x40159000地址处). 使用 mmap 直接映射的 chunk 在释放时直接解除映射, 而不再属于进程的内存空间. 任何对该内存的访问都会产生段错误. 而在 heap 中分配的空间则可能会留在进程内存空间内.

ptmalloc()

为了支持多线程:
--
增加了非主分配区支持,主分配区和非主分配区形成一个环形链表进行管理,每一个分配区使用互斥锁使多线程对于该分配区的访问互斥。如果只有主分配区,每次分配内存都必须对主分配区​加锁,分配完成后再释放锁。在多线程的环境下,对主分配区的锁的竞争很激烈,严重影响了malloc的分配效率。

主分配区和非主分配的区别:
--
1>每个进程只有一个主分配区,可以有多个非主分配区,ptmalloc根据系统对分配区的争用情况动态的增加非主分配区的个数,分配区的数量一旦增加了就不会再减少。
2>主分配区可以使用brk() 和 mmap()向系统申请虚拟内存,而非主分配区只能使用mmap()申请一个HEAP_MAX_SIZE(32下是1M),用户申请时候再进行分割
3>主分配区可以访问heap区域,如果用户不调用brk(),分配程序就可以保证分配到连续的虚拟内存,因为一个进程只有一个主分配区使用sbrk()分配heap区域的虚拟内存。如果主分配区的内存时通过mmap()向系统申请的,当free该内存时,主分配区会直接调用munmap()将内存归还给操作系统。

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准备单独的内存存储指针了。

chunk中的空间复用:

fp、bp、fp_nextsize、bp_nextsize的值都会存在原本的用户区
这些指针一共占用4*4=16B,但是这些只是用于维护空闲链表的,当使用中就无意义,且浪费,所以放在用户空间,空闲时加入。

chunk管理:
这里写图片描述

将大小相似的chunk用双链表维护,一个链称为一个bin.
分类:

fast bins: 这是一个数组,用于记录所有的fast bins:
bins: 这也是一个数组用于记录除fast bins之外的所有bins
一共有128个bins,分别是:
bin 1 为 unsorted bin
bin 2 到 64为small bin
bin 65到 128为large bin

fast bins:
--
维护7条单链表<16B.24B.32B.40B.48B.56B.64B>
是small bins的高速缓冲区,由单链表维护,程序中释放的小内存(<=64B)会被放至此处,分配小内存时会优先在fsat bins中找,找不到才会去bins中找,特定时刻会进行合并放至unshort bin

unshort bins: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中继续进行查找和分配过程。

small bins:
--
同一个small bin中的chunk具有相同的大小。两个相邻的small bin中的chunk大小相差8bytes,small bins中的chunk按照最近使用顺序进行排列,最后释放的chunk被链接到链表的头部,而申请chunk是从链表尾部开始,这样,每一个chunk 都有相同的机会被ptmalloc选中.

large bins: >=512B
--
63个large bin,每个bin中的chunk大小不是一个固定的等差数列,而是分成6组bin,每组bin是一个固定的等差数列。每组bin数量依次为32、16、8、4、2、1,公差依次为64B、512B、4096B、32768B、262144B等。

top chunk:
--
Fast bin、Unsorted bin、Small bin和 Large bin中保存的都是用户曾经释放的内存块(可能经过合并);
top chunk包含Arena扩容的部分,不属于任何bin!

.non_main非主分配区:
对于非主分配区会预先从 mmap 区域分配一块较大的空闲内存模拟 sub-heap,通过管理sub-heap 来响应用户的需求,因为内存是按地址从低向高进行分配的,在空闲内存的最高处,必然存在着一块空闲chunk,叫做 top chunk。当bins和fast bin都满足不了用户的需求,ptmalloc会从top chunk分出一块内存给用户,如果top chunk空间不足,会重新分配一个sub-heap,将top chunk迁移到行的sub-heap上。新的sub-heap和旧的sub。在分配过程中,top chunk的大小随着切割动态变化。

.main主分配区:
主分配区是唯一能够映射进程 heap 区域的分配区,它可以通过 sbrk()来增大或是收缩进程 heap 的大小。top chunk在heap的最上面,如果申请内存时,top chunk空间不足,ptmalloc会调用 sbrk()将的进程 heap 的边界 brk 上移,然后修改 top chunk 的大小。

内存分配过程:

这里写图片描述

缺陷:

1> 后分配的内存先释放,因为 ptmalloc 收缩内存是从 top chunk 开始,如果与 top chunk 相邻的 chunk 不能释放, top chunk 以下的 chunk 都无法释放。
2> 多线程锁开销大, 需要避免多线程频繁分配释放==> #内存暴增
3> 内存从threadareana中分配, 内存不能从一个arena移动到另一个arena, 就是说如果多线程使用内存不均衡,容易导致内存的浪费。 比如说线程1使用了300M内存,完成任务后glibc没有释放给操作系统,线程2开始创建了一个新的arena, 但是线程1300M却不能用了。
4> 每个chunk至少8字节的开销很大
5> 不定期分配长生命周期的内存容易造成内存碎片,不利于回收。 64位系统最好分配32M以上内存,这是使用mmap的阈值。

“`

猜你喜欢

转载自blog.csdn.net/m18706819671/article/details/80640072