内存管理maclloc,jemalloc

内存分配malloc

一、c库函数Malloc()和free()
Gun下默认会链接 libc库,路径为/lib64/libc.so.6。Malloc()类函数调用时相应的会调用libc中的maloc函数。
在这里插入图片描述
在这里插入图片描述

定义如图所示,其中用了一些gun的c关键字,大概意思为malloc的是__libc_malloc的别名,__malloc也是__libc_malloc的别名。
在这里插入图片描述
二、内存分配的具体实现

在调用__libc_malloc函数时首先会判断全局钩子函数malloc_hook_ini是否被定义,在libc库中钩子函数有被定义为malloc_hook_ini
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

其中用了gnuc的关键字将函数定义为弱符号。弱符号的使用为:若两个或两个以上全局符号(函数或变量名)名字一样,而其中之一声明为弱符号,则这些全局符号不会引发重定义错误。链接器会忽略弱符号,去使用普通的全局符号来解析所有对这些符号的引用,但当普通的全局符号不可用时,链接器会使用弱符号。
Malloc_hook_ini定义为:
在这里插入图片描述
意思为当没有其他全局钩子定义时,将调用libc自动的弱符号钩子函数Malloc_hook_ini
其中进行了ptmalloc分配器的初始化,随后将Malloc_hook_ini置为空,再次调用__libc_malloc函数最终在
在这里插入图片描述

_int_malloc()中实现内存分配,其中victim为返回的地址值,a_ptr为ptmalloc分配器维护的内存分配区,bytes为需要分配的内存大小。
第一次调用:
malloc->__libc_malloc->__malloc_hook->malloc_hook_in -> ptmalloc_init -> __libc_malloc -> _int_malloc
再次调用:
malloc -> __libc_malloc -> _int_malloc
最终小内存通过brk()和sbrk()系统调用与内核中进程地址空间中的堆区进行交换。大内存则通过mmap()和munmap()进行地址映射。
在这里插入图片描述

其中edata为堆指针,小内存分配时通过brk()和sbrk()系统调用进行移动。
在这里插入图片描述

大内存分配时如200k时分通过mmap()系统调用 找一块空闲的虚拟内存,随后引发缺页异常,由操作系统进行分配。
三、内存碎片
内部碎片:是已经被分配出去(能明确指出属于哪个进程)的内存空间大于请求所需的内存空间,不能被利用的内存空间就是内部碎片。内部碎片是处于区域内部或页面内部的存储块。占有这些区域或页面的进程并不使用这个存储块。而在进程占有这块存储块时,系统无法利用它。直到进程释放它,或进程结束时,系统才有可能利用这个存储块。
外部碎片:外部碎片是指还没有分配出去(不属于任何进程),但是由于大小而无法分配给申请内存空间的新进程的内存空闲块。

四、Ptmalloc
在这里插入图片描述

不管内存是在哪里被分配的,用什么方法分配,用户请求分配的空间在 ptmalloc 中都使 用一个 chunk 来表示。用户调用 free()函数释放掉的内存也并不是立即就归还给操作系统, 相反,它们也会被表示为一个 chunk,ptmalloc 使用特定的数据结构来管理这些空闲的 chunk。
ptmalloc 在给用户分配的空间的前后加上了一些控制信息,用这样的方法来记录分配的
信息,以便完成分配和释放工作。
在这里插入图片描述

用户 free 掉的内存并不是都会马上归还给系统,ptmalloc 会统一管理 heap 和 mmap 映
射区域中的空闲的 chunk,当用户进行下一次分配请求时,ptmalloc 会首先试图在空闲的 chunk 中挑选一块给用户,这样就避免了频繁的系统调用,降低了内存分配的开销。ptmalloc 将相似大小的 chunk 用双向链表链接起来,这样的一个链表被称为一个 bin。Ptmalloc 一共 维护了 128 个 bin,并使用一个数组来存储这些bin。
在这里插入图片描述

表链接起来,这样的一个链表被称为一个 bin。Ptmalloc 一共 维护了 128 个 bin,并使用一个数组来存储这些bin。
其中:
1.数组中的第一个为 unsorted bin
2.数组中从编号2到编号为64的bin 称为 small bins,同 一个 small bin 中的 chunk 具有相同的大小。相邻的small bins中的chunk大小相差为8B。
3.Small bins 后面的 bin 被称作 large bins。large bins 中的每一个 bin 分别包含了一个给定范围 内的 chunk,其中的 chunk 按大小序排列。相同大小的 chunk 同样按照最近使用顺序排列。
4.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 中继续进行查找和分配过程。

1.小于等于 64 字节:用 pool 算法分配。
2.64 到 512 字节之间:在最佳匹配算法分配和 pool 算法分配中取一种合适的。
3.大于等于 512 字节:用最佳匹配算法分配。
4.大于等于 mmap 分配阈值 (默认值 128KB): 根据设置的 mmap 的分配策略进行分配。
Ptmalloc分配内存具体步骤
1.获取一个未加锁的分配区,如果所有分配区都加了锁,ptmalloc会开辟一个新的分配区。开辟新分配区时,会调用mmap创建一个sub-heap,并设置好top chunk。
2.将用户的请求大小转换为实际需要分配的 chunk 空间大小
3.判断所需分配 chunk的大小是否满足 chunk_size <= max_fast (max_fast 默认为 64B),如果是的话, 则转下一步, 否则跳到第 5 步。
4.首先尝试在 fast bins 中取一个所需大小的 chunk 分配给用户。 如果可以找到, 则分配结束。 否则转到下一步。
5.判断所需大小是否处在 small bins 中, 即判断 chunk_size < 512B 是否成立。 如果chunk大小处在small bins中,则转下一步,否则转到第7步。
6.根据所需分配的 chunk 的大小, 找到具体所在的某个 small bin, 从该 bin 的尾部摘取一个恰好满足大小的chunk。若成功,则分配结束,否则,转到下一步。
7.首先尽可能将fast bins中的chunk合并,并且放入unsorted bin中。如果unsorted bin中只有一个chunk,而且在上次分配的时候使用过,并且说需要分配的属于 small bins,并且 chunk 的大小大于等于需要分配的大小,这种情况下就直接将该 chunk 进行切割,分配结束。否则,将unsorted bin中的chunk放入small bins或者large bins。进入下一步。
8.从large bins分配一块合适的chunk.
9.根据申请空间的大小和mmap分配阈值判断,从top chunk中分配内存还是直接调用mmap分配内存。

释放内存的具体步骤:
1.根据地址对齐找到sub-heap,从sub-heap头部信息找到属于哪个分配区,获取分配区的锁,保证线程安全。
2.判断所需释放的 chunk 是否为 mmaped chunk,如果是,则调用munmap()释放mmaped chunk,解除内存空间映射,该该空间不再有效。
3.判断 chunk 的大小和所处的位置,若 chunk_size <= max_fast,并且 chunk 并不位于heap 的顶部,也就是说并不与 top chunk 相邻,则转到下一步。否则跳到5。
4.将 chunk 放到 fast bins 中, chunk 放入到 fast bins 中时,并不会修改chunk的使用状态位,也并不尝试合并。然后free函数返回。
5.判断前一个 chunk 是否处在使用中,如果前一个块也是空闲块,则合并。并转下一步。
6.判断当前释放 chunk 的下一个块是否为 top thunk,如果是,则转第8步,否则转下一步。
7.判断下一个 chunk 是否处在使用中, 如果下一个 chunk 也是空闲的, 则合并, 并将合并后的 chunk 放到 unsorted bin 中。
8.将chunk与top chunk合并。
9.判断合并后的chunk的大小是否大于FASTBIN_CONSOLIDATION_THRESHOLD(默认
64KB), 如果是的话,则会触发进行 fast bins 的合并操作, fast bins 中的 chunk 将被遍历,并与相邻的空闲 chunk 进行合并,合并后的 chunk 会被放到 unsorted bin 中。fast bins 将变为空, 操作完成之后转下一步。
10.判断top chunk是否大于mmap收缩值,如果大于就将一部分top chunk归还给操作系统。

五、Jemalloc
在这里插入图片描述

Jemalloc中定义了全局钩子函数,分配内存和释放内存时通过强符号将使用jemalloc分配器。
编译完成之后将库和头文件移动到相关位置,然后源文件中加上头文件#include <jemalloc/jemalloc.h>,编译时加上-ljemalloc。
其中jemalloc的配置函数如下,可修改相关配置
在这里插入图片描述
在这里插入图片描述
原理简述:
在这里插入图片描述

1:arena:把内存分成许多不同的小块来分而治之,是jemalloc的核心分配管理区域。
2: chunk。具体进行内存分配的区域,默认大小是4M,chunk以page为单位进行管理,每个chunk的前6个page用于存储后面page的状态,比如是否分配或已经分配
3:bin:用来管理各个不同大小单元的分配,比如最小的Bin管理的是8字节的分配,每个Bin管理的大小都不一样,依次递增。
4:run:每个bin在实际上是通过对它对应的正在运行的Run进行操作来进行分配的,一个run实际上就是chunk里的一块区域,大小是page的整数倍,具体由实际的bin来决定,比如8字节的bin对应的run就只有1个page,可以从里面选取一个8字节的块进行分配。在run的最开头会存储着这个run的信息,比如还有多少个块可供分配。
5:tcache。线程对应的私有缓存空间,默认是使用的。因此在分配内存时首先从tcache中找,miss的情况下才会进入一般的分配流程。

arena和bin的关系:每个arena有个bin数组,每个bin管理不同大小的内存(run通过它的配置去获取相应大小的内存),每个tcahe有一个对应的arena,它本身也有一个bin数组(称为tbin),前面的部分与arena的bin数组是对应的,但它长度更大一些,因为它会缓存一些更大的块;而且它也没有对应的run的概念
chunk与run的关系:chunk默认是4M,而run是在chunk中进行实际分配的操作对象,每次有新的分配请求时一旦tcache无法满足要求,就要通过run进行操作,如果没有对应的run存在就要新建一个,哪怕只分配一个块,比如只申请一个8字节的块,也会生成一个大小为一个page(默认4K)的run;再申请一个16字节的块,又会生成一个大小为4096字节的run。run的具体大小由它对应的bin决定,但一定是page的整数倍。因此实际上每个chunk就被分成了一个个的run。

内存分配:

  1. 如果请求size不大于arena的最小的bin(默认3584字节),那么就通过线程对应的tcache来进行分配。首先确定size的大小属于哪一个tbin,比如2字节的size就属于最小的8字节的tbin,然后查找tbin中有没有缓存的空间,如果有就进行分配,没有则为这个tbin对应的arena的bin分配一个run,然后把这个run里面的部分块的地址依次赋给tcache的对应的bin的avail数组,相当于缓存了一部分的8字节的块,最后从这个availl数组中选取一个地址进行分配;
  2. 如果请求size大于arena的最小的bin,同时不大于tcache能缓存的最大块(默认是32K),也会通过线程对应的tcache来进行分配,但方式不同。首先看tcache对应的tbin里有没有缓存块,如果有就分配,没有就从chunk里直接找一块相应的page整数倍大小的空间进行分配(当这块空间后续释放时,这会进入相应的tcache对应的tbin里);
  3. 如果请求size大于tcache能缓存的最大块,同时不大于chunk大小(默认是4M),具体分配和第2类请求相同,区别只是没有使用tcache;
  4. 如果请求大于chunk大小,直接通过mmap进行分配。

内存回收:
回收流程大体和分配流程类似,有tcache机制的会将回收的块进行缓存,没有tcache机制的直接回收(不大于chunk的将对应的page状态进行修改,回收对应的run;大于chunk的直接munmap)。需要关注的是jemalloc何时会将内存还给操作系统,因为ptmalloc中存在因为使用top_chunk机制而使得内存无法还给操作系统的问题。目前看来,除了大内存直接munmap,jemalloc还有两种机制可以释放内存:

  1. 当释放时发现某个chunk的所有内存都已经为脏(即分配后又回收)就把整个chunk释放;
  2. 当arena中的page分配情况满足一个阈值时。这个阈值的具体含义是该arena中的脏page大小已经达到一个chunk的大小且占到了active page的1/opt_lg_dirty_mult(默认为1/32)。active page的意思是已经正在使用中的run的page,而脏page就是其中已经分配后又回收的page。

Jmalloc小对象也根据size-class,但是它使用了低地址优先的策略,来降低内存碎片化。
Jemalloc大概需要2%的额外开销。(tcmalloc 1%, ptmalloc最少8B)
Jemalloc和tcmalloc类似的线程本地缓存,避免锁的竞争

六、测试结果
1.单线程测试
最大字节数:80000,内循环次数:100,倍增关系10,总次数1000
Ptmalloc:7.562s 实际占用内存:1004
Jemalloc:2.485s 实际占用内存:28680

多线程测试
最大字节数:8000,内循环次数:100,倍增关系10,总线程数100
Ptmalloc:1.699s 实际占用内存:15932
Jemalloc:1.207s 实际占用内存:89964

LINUX操作系统内存框图

在这里插入图片描述

简易内存池实现思路

在这里插入图片描述
创建一个链表,每个链表头部标识此链表内存块的大小,为2的幂次。申请的内存落在某个区间时将此区间的空闲内存块链表返回给用户。其中头部信息保存在返回给用户内存的前面。一次性申请malloc内存时可用互斥锁,单次内存池申请内存时可用自旋锁。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值