Mesh 内存分配器的 mmap 小技巧
最近看了 一篇内存分配器的论文 ,原理很简单,但是里面的数学论证还没看懂,这次先简单写一下原理和用到的 API。
内存分配器是用于封装操作系统提供的底层 API,给应用程序提供动态内存的。内存不断申请释放后,往往会形成内存碎片。当需要申请一段较大的内存时,当前剩余内存总量是够的,但是被当前申请的内存块隔断成一个个小隔间,内存分配器无法给出指定长度的内存。这时就只能向操作系统重新申请,或者对应用程序返回分配失败了。
( 图片来自这里 )
如上图所示,不断申请内存后,内存占用如 Figure 2 所示。应用程序释放部分内存后,形成 Figure 3。前面的两个空闲内存块,就只能用来放小的内存对象了。即使总内存足够,也因为中间内存对象的隔断,无法分配出足够大的内存。
为了避免内存碎片增多,Mesh 提出了内存页合并的想法。在现代的 Linux 系统上,内存页一般以 4K 大小分配,假设页 A 有碎片,页 B 也有碎片,但是页 A 和页 B 的碎片相对于页头的偏移刚好不重叠,那就可以将两者合成一页,腾出完整的一页来分配较大的内存。见下图:
(图来自论文)
这种合并的方法有一个问题,c/c++ 的应用程序可能直接存下了对象的地址,所以内存合并后,原有分配对象的地址不能变。作者用了一个巧妙的方法来解决这个问题:
-
作者用内存文件系统创建了一个内存文件(mkstemp)
-
通过 mmap api 创建一个 1:1 的内存映射,将第一步的文件映射为一块内存(mmap,这里需要设为 MAP_SHARED, 如果设为 MAP_PRIVATE 则重映射后 B 页内容会丢掉)
-
假设算法发现有 A 和 B 两页内存可进行合并,则将 B 中存的对象用 memcpy 拷贝到 A 里去(memcpy 或者普通的赋值操作)
-
通过 mmap api 将 B 页重新映射到 A 页对应的文件偏移上(mmap,这里需要设为 MAP_FIXED,表示新的内存映射就用这个地址开始)
这样应用程序依然可以通过原有的内存地址访问原有的内存对象,而对象实际上已经移动到新的位置了。
上面的步骤 我做了个小实验 验证了一下,的确可行。作者论文里提到修改页表来完成,之前纠结了很久为什么应用程序可以改页表,不是操作系统维护页表的吗?可以改页表不就可以碰物理内存了?才知道原来 mmap 可以这样玩 233333