ptmalloc 内存管理概述

ptmalloc是Glibc中的一个多线程内存管理器,它在Doug Lea的内存分配器基础上增加了对多线程的支持。ptmalloc通过主分配区(main_arena)和非主分配区(non_main_arena)管理内存,使用chunk表示用户分配和空闲空间,并通过bins、fast bins和unsorted bin等结构组织空闲chunk。分配内存时,首先尝试fast bins、small bins、large bins,最后可能涉及top chunk或mmap。释放内存时,若满足条件会进行内存合并或归还给操作系统。ptmalloc适用于短生命周期的内存分配,长生命周期的内存应使用大于1MB(32位系统)或32MB(64位系统)的mmap分配,以避免内存暴增。
摘要由CSDN通过智能技术生成

ptmalloc引入

Linux 中 malloc 的早期版本是由 Doug Lea 实现的,它有一个重要问题就是在并行处理时多个线程共享进程的内存空间,各线程可能并发请求内存,在这种情况下应该如何保证分配和回收的正确和高效。

Wolfram Gloger 在 Doug Lea 的基础上改进使得 Glibc 的 malloc 可以支持多线程——ptmalloc。

ptmalloc简述

ptmalloc 实现了 malloc(),free()以及一组其它的函数,以提供动态内存管理的支持。
分配器处在用户程序和内核之间,它响应用户的分配请求,向操作系统申请内存,然后将其返回给用户程序。

为了保持高效的分配,分配器一般都会预先分配一块大于用户请求的内存, 并通过某种算法管理这块内存。来满足用户的内存分配要求,用户释放掉的内存也并不是立即就返回给操作系统,相反,分配器会管理这些被释放掉的空闲空间,以应对用户以后的内存分配要求。

也就是说,分配器不但要管理已分配的内存块,还需要管理空闲的内存块,当响应用户分配要求时,分配器会首先在空闲空间中寻找一块合适的内存给用户,在空闲空间中找不到的情况下才分配一块新的内存。

为实现一个高效的分配器,需要考虑很多的因素。 比如,分配器本身管理内存块所占用的内存空间必须很小,分配算法必须要足够的快。

内存管理的设计假设

ptmalloc 在设计时折中了高效率,高空间利用率,高可用性等设计目标。

在其实现代码中,隐藏着内存管理中的一些设计假设,由于某些设计假设,导致了在某些情况下 ptmalloc 的行为很诡异。

这些设计假设包括:

  1. 具有长生命周期的大内存分配使用 mmap
  2. 特别大的内存分配总是使用 mmap。
  3. 具有短生命周期的内存分配使用 brk,因为用 mmap 映射匿名页,当发生缺页异常 时,linux 内核为缺页分配一个新物理页,并将该物理页清 0,一个 mmap 的内存块 需要映射多个物理页,导致多次清 0 操作,很浪费系统资源,所以引入了 mmap 分配阈值动态调整机制,保证在必要的情况下才使用 mmap 分配内存。
  4. 尽量只缓存临时使用的空闲小内存块,对大内存块或是长生命周期的大内存块在释放时都直接归还给操作系统。
  5. 空闲的小内存块只会在 malloc 和 free 的时候进行合并,free 时空闲内存块可能放入 pool 中,不一定归还给操作系统。
  6. 收缩堆的条件是当前 free 的块大小加上前后能合并 chunk 的大小大于 64KB,并且堆顶的大小达到阈值,才有可能收缩堆,把堆最顶端的空闲内存返回给操作系统。
  7. 需要保持长期存储的程序不适合用 ptmalloc 来管理内存
  8. 为了支持多线程,多个线程可以从同一个分配区(arena)中分配内存,ptmalloc 假设线程 A 释放掉一块内存后,线程 B 会申请类似大小的内存,但是 A 释放的内存跟 B 需要的内存不一定完全相等,可能有一个小的误差,就需要不停地对内存块作切割和合并,这个过程中可能产生内存碎片。

内存管理数据结构概述

主分配区(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 优化,但默认编译不会启用该优化,这两个对锁的优化应该能够提升多线程内存的分配的效率。

chunk的组织

不管内存是在哪里被分配的,用什么方法分配,用户请求分配的空间在 ptmalloc 中都使用一个 chunk 来表示。用户调用 free()函数释放掉的内存也并不是立即就归还给操作系统, 相反,它们也会被表示为一个 chunk,ptmalloc 使用特定的数据结构来管理这些空闲的 chunk。

chunk 格式

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

一个使用中的 chunk(使用中,就是指还没有被 free 掉)在内存中的样子如图所示:
在这里插入图片描述

  • 在图中,chunk 指针指向一个 chunk 的开始,一个 chunk 中包含了用户请求的内存区域 和相关的控制信息。图中的

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值