首发于术道经纬
Linux中的Memory Compaction [二] - CMA

Linux中的Memory Compaction [二] - CMA

Linux中的Memory Compaction [一]

CMA机制

在数据方向是从外设到内存的DMA传输中,如果外设本身不支持scatter-gather形式的DMA,或者CPU不具备使用虚拟地址作为目标地址的IOMMU/SMMU机制(通常较高端的芯片才具有),那么则要求DMA的目标地址必须在物理内存上是连续的。

传统的做法是在内核启动时,通过传递"mem="的内核参数,预留一部分内存,但这种预留的内存只能作为DMA传输的“私藏”,即便之后DMA并没有真正使用这部分内存,也不能挪作他用,造成浪费。

应用对内存的需求变化是很难预测的,但是如果等到DMA设备真正产生需求时再分配,由于内存碎片等诸多原因,又可能很难申请到一块较大的连续的物理内存。

一个更灵活的做法是基于memory compaction实现的CMA(Contiguous Memory Allocator)机制(对应的内核选项为"CONFIG_CMA")。虽然名字是叫"allocator",但CMA存在的意义不光是“分配”,其目标还包括提高内存的利用率。

CMA也会预留一块内存区域,但在DMA设备不使用这段内存的时候,它也可以被OS的其他模块所使用。而在DMA设备真正需要的时候,可以对其他模块使用的page frame做migration操作,以腾出CMA区域中的空间。也就是说,DMA设备对CMA区域有优先使用权,属于primary client,而其他模块的页面则是secondary client

Reserved Area Technique

同上文的示例有所不同的是,需要先在CMA以外的内存中分配空闲的page frame,然后把CMA里的部分page的内容拷贝过去,最后释放掉CMA里对应的page。

数据结构

一个CMA区域在内核中用struct cma结构体表示(定义在"/mm/cma.h"),多个CMA区域由"cma_areas[]"数组管理和索引:

struct cma {
    unsigned long  base_pfn;
    unsigned long  count;
    unsigned long *bitmap;
    unsigned int   order_per_bit; /* Order of pages represented by one bit */
    ...
}
struct cma cma_areas[MAX_CMA_AREAS];

其中,"base_pfn"指定了这个CMA区域的起始物理页面的地址,"count"为page frame的数量。页面空闲与否,由"bitmap"进行位图标记,但标记的最小粒度不是一个page frame,而是"order_per_bit"。

图片来源于http://jake.dothome.co.kr/wp-content/uploads/2016/03/cma-1.png
启动和配置

在内存分配机制从启动时的memblock/bootmem切换到运行时的buddy系统之前,就需要完成CMA的预留和初始化。

图片来源于zhuhui@xiaomi.com

一个CMA区域的属性在device tree中设置,包括其范围大小、对齐要求等,启动时可通过"cma="内核参数指定希望使用的CMA区域。

CMA的类型

既然要做migration/compaction操作,那么页面就应该是movable的(可以自由流动),按理它使用"MIGRATE_MOVABLE"的类型就可以了,但内核又为CMA单独定义了一个"MIGRATE_CMA"的类型。

这是因为当"MIGRATE_UNMOVABLE"类型的内存耗光后,可以fallback到"MIGRATE_MOVABLE"的内存区域去分配,这将使后者变得不再“纯粹”,就像流动的河水有一部分变成了冰块一样。

而CMA区域必须保证在任何时刻,其区域内的page frame都是movable类型的(就像不冻港一样),这样DMA设备的请求才能得到满足,因此它不允许像普通的"MIGRATE_MOVABLE"那样被其他内存区域fallback。

CMA的应用

一个CMA区域可以被多个device drivers共享,也可以单独指定给一个。要使用CMA区域,driver必须至少注册了一个device,如果没有,可以使用fake device。在Linux内核的实现中,CMA属于DMA子系统的一部分。

由于CMA进行的是page层面的操作,而且CMA分配器与DMA子系统已经完全集成,因此使用DMA设备的驱动程序无需直接调用CMA的API接口,而是使用像dma_alloc_coherent()这样的DMA相关函数就可以了。因为底层的CMA操作涉及到内存分配、拷贝和页表修改,可能造成睡眠等待,所以不能在atomic上下文中使用。

存在的问题

在实际应用中,CMA的使用效果其实并不太好,因为分配新的page frame,复制页面内容,加上更改PTE的操作,开销很大,这会给作为primary client的DMA设备的内存申请造成很大的延迟(甚至fail)。

要从源头上解决这个问题,应该尽量避免申请high order的内存,比如使用"VMAP_STACK"机制,就不再要求内核stack的物理内存连续。

Linux中有一个宏"PAGE_ALLOC_COSTLY_ORDER",默认值为3,它的意思是:当一次内存申请小于或等于 2^{3}=8 个pages时,通常是容易得到满足的,而大于8个就是比较"costly"的操作。这也是在提醒开发者,最好不要一次申请超过8个连续的page frames。

CMA的改进 - GCMA

从另一方面,也可以基于现有的CMA机制进行一些调整和改进。因为这些开销主要是由需要迁出CMA区域的页面造成的,从某个角度说,这些movable的页面作为seconday client并不十分理想。

那什么页面做seconday client的开销更小呢?能够想到的就是那些即将被discard的页面,包括在内存回收中,已经完成writeback的page cache和完成swap out的anonymous pages。这就是由韩国首尔大学的Seong Jae Park等人设计的GCMA(Guaranteed Contiguous Memory Allocator)方案,"Guaranteed"意思是更能确保primary client的分配。

GCMA workflow

但是内存回收本身也会造成延迟,于是对于seconday client,GCMA方案采用write-through的模式,比如正在swap out的页面,将同时写入外部的swap device和内存中的GCMA保留区域。不过,由于涉及到I/O操作,性能依然受到影响。

因此,GCMA方案推荐使用压缩的内存交换区域(比如Android使用的zram,或者专为GCMA定制的Dmem)。经过这些优化和平衡,GCMA最终在benchmark测试中取得了一个颇为不错的结果(具体实现的代码在此)。

GCMA overall architecture

Linux中的Memory Compaction [三] - THP


参考:


原创文章,转载请注明出处。

发布于 08-25

文章被以下专栏收录