《glibc内存管理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)中分配内存,ptamlloc假设线程 A 释放掉一块内存后,线程 B 会申请类似大小的内存,但是 A 释放的内存跟 B 需要的内存不一定完全相等,可能有一个小的误差,就需要不停地对内存块作切割合并,这个过程可能产生内存碎片。

内存管理数据结构概述

Main_arena 与 non_main_arena

1.主分配区与非主分配区用环形链表管理。
2.每个分配区利用互斥锁(mutex)使线程对于该分配区的访问互斥
3.每个进程只有一个主分配区,但可以存在多个非主分配区,而且分配区的数量一旦增加就不会再减少了。
4.主分配区可以访问进程的 heap 区域和 mmap 映射区域(sbrk和mmap),非主分配区只能访问进程的 mmap 映射区域(mmap)

chunk 中的空间复用

1.当一个 chunk 处于使用状态,则它的下一个 chunk 的 prev_size 域是无效的,可以被当前 chunk 使用。

空闲 chunk 容器

Fast Bins

1.不大于 max_fast 的 chunk 被释放后,首先会被放到 fast bins 中,fast bins 中的 chunk 并不改变它的使用标记 P。
2.在某个特定的时候, ptmalloc 会遍历 fast bins 中的 chunk , 将相邻的空闲 chunk 进行合并,并将合并后的 chunk 加入 unsorted bin 中,然后再将 unsorted bin 里的 chunk 加入 bins 中。

Unsorted Bin

1.如果用户释放的 chunk 大于 max_fast ,或者 fast bins 中的空闲 chunk 合并后,这些 chunk 首先会被放到 unsorted bin 队列中。
2.如果 unsorted bin 不能满足分配要求, malloc 便会将 unsorted bin 中的 chunk 加入 bins 中。

Top chunk

1.Top chunk 在分配时总是在 fast bins 和 bins 之后考虑。

mmaped chunk

1.当fast bins 、 bins 和 Top chunk 都不满足分配需求时,则使用 mmap 来直接使用内存映射来将页映射到进程空间。

Last remainder

1.当需要分配一个 small chunk ,但是在small bins 中找不到合适的 chunk ,如果 last remainder chunk 的大小大于所需的 small chunk 大小, last remainder chunk 被分裂成两个 chunk,其中一个 chunk 返回给用户,另一个 chunk 变成新的 last remainder chunk。

内存分配概述

以 32 系统为例
1.获取分配区的锁,为了防止多个线程同时访问一个分配区,在进行分配之前需要取得分配区的锁。线程先查看线程私有实例中是否已经存在一个分配区,如果存在尝试对该分配区加锁,如果加锁成功,使用该分配区分配内存,否则,该线程搜索分配区循环链表试图获得一个空闲的分配区。如果所有的分配区都已经枷锁,那么 ptmalloc 会开辟一个新的分配区,把该分配区加入到全局分配区循环链表和线程的私有实例中并加锁,然后使用该分配区进行分配操作。开辟出来的新分配区一定为非主分配区,因为主分配区是父进程那里继承来的。开辟非主分配区时会调用 mmap 创建一个 sub-heap ,并设置好 top chunk 。
2.将用户的请求大小转换为实际需要分配的 chunk 空间大小。
3.判断所需分配的 chunk 的大小是否满足 chunk_size <= max_fast,如果是的话,则转下一步,否则跳到第 5 步。
4.首先尝试在fast bins 中取一个所需大小的 chunk 分配给用户。如果可以找到,则分配结束,否则转到下一步。
5.判断所需的大小是否处在 small bins 中,即判断 chunk_size < 512B 是否成立。如果 chunk 大小处在 small bins 中,则转下一步,否则转到第 6 步。
6.根据所需分配的 chunk 的大小,找到具体所在的某个 small bin,从该 bin 的尾部摘取一个恰好满足大小的 chunk 。若成功,则分配结束,否则,转到下一步。
7.到了这一步,说明需要分配的是一块大的内存,或者 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 中,遍历完成后,转入下一步。
8.到了这一步,说明需要分配的是一块大的内存,或者 small bins 和 unsorted bin 中都找不到合适的 chunk ,并且 fast bins 和 unsorted bin 中所有的 chunk 都清除干净了。从 large bins 中按照"smallest-first, best-fit"原则,找到一个合适的 chunk ,从中划分一块所需大小的 chunk ,并将剩下的部分来链接回到 bins 中。若操作成功,则分配结束,否则转到下一步。
9.如果搜索 fast bins 和 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 步,增加 top chunk 的大小。
11.使用 mmap 系统调用为程序的内存空间映射一块 chunk_size align 4KB 大小的空间,然后将内存指针返回给用户。
12.判断是否为第一次调用 malloc,若是主分配区,则需要进行依次初始化工作,分配一块大小为(chunk_size + 128KB) align 4KB 大小的空间作为初始的 heap。若已经初始化过了,主分区则调用 sbrk 增加 heap 空间,分主分配区则在 top chunk 中切割出一个 chunk,使之满足分配需求,并将内存指针返回给用户。

内存回收概述

1.free 函数同样首先需要获取分配区的锁,来保证线程安全。
2.判断传入的指针是否为 0,如果为 0,则什么都不做,直接 return。否则转入下一步。
3.判断所需释放的 chunk 是否为 mmaped chunk ,如果是,则调用 munmap 释放 mmaped chunk ,解除内存空间映射,该空间不再有效。如果开启了 mmap 分配阈值的动态调整机制,并且当前回收的 chunk 大小大于 mmap 分配的阈值,将 mmap 分配阈值设置为该 chunk 的大小,将 mmap 收缩的阈值设定为 mmap 分配阈值的 2 倍,释放完成,否则跳到下一步。
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 的大小是否大于 FASTBIN_CONSOLIDATION_THRESHOLD,如果是的话,则会触发进行 fsat bins 的合并操作, fast bins 中的 chunk 将被遍历,并于相邻的空闲 chunk 进行合并,合并后的 chunk 会被放到 unsroted bin 中。fast bins 将被为空,操作完成之后转下一步。
11.判断 top chunk 的大小是否大于 mmap 收缩的阈值,如果是的话,对于主分配区,则会试图归还 top chunk 中的一部分给操作系统。但是最先分配的128kb 空间是不会归还的, ptmalloc 会一直管理这部分内存,用于响应用于的分配请求;如果为非主分配区,会进行 sub-heap 收缩,将top chunk 的一部分返回给操作系统,如果 top chunk 为整个 sub-heap ,会把整个 sub-heap 还给操作系统。做完这一步之后,释放结束,从 free 函数退出。可以看出,收缩堆的条件是当前 free 的
chunk 大小加上前后能合并 chunk 的大小大于 64K,并且要 top chunk 的大小要达到 mmap 收缩阈值,才有可能收缩堆。

内容来源

《glibc内存管理ptmalloc源码分析》

posted @ 2020-02-20 15:42  PwnKi  阅读(967)  评论(0编辑  收藏  举报