【前言】
有时候对一个词记忆特别深刻的时候,那一定是你被这个词伤害过,我就是被CMA伤害过。。。
那是一个冬天,高通的面试官问我,知道什么是CMA吗,我心头翻涌出无数浪花,脑子也和进了水一样,CMA,我草,这是个啥,China Meteorological Administration 中国气象局?China Meat Association 中国肉类协会。
我心想,高通绝对不会知道我中华文化之深厚,Christian Management Association 基督教管理委员会,他一定想问我是否有信仰,那我就说 merry christmas 或者哈利路亚,这不就表明了我还是了解欧美文化的,结果面试官说:下一个。
我做错了什么。。。
1. 啥是CMA?
CMA: Contiguous Memory Allocator:说人话,linux连续的内存分配模块。
CMA是处于需要连续内存块的其他内核模块(例如DMA mapping framework)和内存管理模块之间的一个中间层模块,主要功能包括:
1、确定CMA内存的区域: 解析DTS或者命令行中的参数。
2、提供cma_alloc和cma_release两个接口函数用于分配和释放CMA pages。
3、记录和跟踪CMA area中各个pages的状态。
4、调用伙伴系统接口,进行真正的内存分配。
2. 为啥需要CMA?
盲猜:应该是有需求,有需求就会有市场!
是谁需要连续的物理内存?连linux强大的虚拟内存都无法满足?还让linux费心专门做了一个功能!我猜想这个需求一定很重要,难道是哈利路亚!
手机中能用到这么大的空间的器件是什么呢?
【猜想】
第一个能想到的是display和camera,也只有这两个一个长满手机正面,一个充满手机背面的器件可能才有这么大的需求吧?
【验证】
1300W像素的手机,占用内存:1300W * 3B = 26MB,我去还真是个内存大户,小米的1亿像素得多少,即使用到图像压缩技术,大实在是大!
buddy系统你一次性只能最大分配4M,太少了吧。。。而且还会频繁调用buddy系统,这样碎片产生就不可避免。
Page size大于4K的page统称为“huge page”,camera我需要一个26MB的huge page,而且还要连续,如果采用static分配,那么就会这块空间的浪费,其他人用不了,连续的空间哪有那么多。。
【CMA诞生】Michal Nazarewicz 麦克金发哥提出
对于CMA 内存,当前驱动没有分配使用的时候,这些memory可以内核的被其他的模块使用(当然有一定的要求),而当驱动分配CMA内存后,那些被其他模块使用的内存需要吐出来,形成物理地址连续的大块内存,给具体的驱动来使用。
一句话描述:不是内存的生产者,只是内存的搬运工,解析和管理内存配置。
3. 菜鸡我实现的CMA?
1. 预留专属空间:RAM中预留一片专属的空间给CM(连续内存)器件使用,首先很方便的管理,其次可以和其他正常使用内存的user进行区分。此外,设置一个宏动态配置这片区域的size。
2. 灵活的接口:其他user来调用CMA的时,只需统一的接口,不需要关心其中细节,就可以正常使用CMA内存了。一般做common层。
3. 内存分配和回收算法:
- 当空间请求时:因为是动态分配,当然面临的问题是如何把连续的空间分配给需要的人,因为不同的人需要的空间大小不同,所以要设计内存分配算法。
- 当空间释放时:我们又需要内存的回收算法,让这些空间继续可以被其他人使用,而且还需要碎片少,连续性强。
- 当空间不足的时:我们又需要将不常使用的内存分配给及时调用的user。
4. 大神实现的CMA?
大神实现的CMA:
1. Reserved Area(memory type与reserve type对应)
- Golbal CMA area(cmd line):初始化的时候,调用dma_contiguous_reserve函数,将指定的memory region保留给Global CMA area使用
- per device CMA area:Reserved memory node(DTS)。
2. 设计CMA数据结构:
struct cma cma_areas[MAX_CMA_AREAS];
struct cma {
unsigned long base_pfn; //CMA区域物理地址的起始页帧号
unsigned long count; //该cma area内存有多少个page
unsigned long *bitmap; //0:表示内存free,1表示内存已经分配,位图,用于描述页的分配情况
unsigned int order_per_bit;//0:1个page分配和释放 1:按2个page组成block来分配和释放
struct mutex lock;
};
PS:
1. base_pfn + count :该CMA area在内存在的位置
2. order_per_bit + count: bitmap指针指向内存的大小
3. MAX_CMA_AREAS: 1 + CONFIG_CMA_AREAS
3. Global CMA area创建:
1.初始化定义:RESERVEDMEM_OF_DECLARE(cma, "shared-dma-pool", rmem_cma_setup;
2.内核配置:CMA_SIZE_MBYTES和CMA_SIZE_PERCENTAGE
3.命令行参数:cma=nn[MG]@[start[MG][-end[MG]]]
Per device CMA area操作:
1.DTS定义:调用:cma_init_reserved_mem。reserved memory节点必须有reusable属性,不能有no-map的属性。no-map属性的往往是专用于某个设备驱动,在驱动中会进行io remap,如果OS已经对这段地址进行了mapping,而驱动又一次mapping,这样就有不同的虚拟地址mapping到同一个物理地址上去。
reserved-memory {
/* global autoconfigured region for contiguous allocations */
linux,cma {
compatible = "shared-dma-pool";
reusable;
size = <0 0x28000000>;
alloc-ranges = <0 0xa0000000 0 0x40000000>;
linux,cma-default;
};
};
linux,cma {
compatible = "xxx";
reusable; //默认属性
size = <0x00800000>; /* 8M */ //通常size为8M的倍数
alloc-ranges = <0x69000000 0x00800000>; //起始地址和长度
linux,cma-default; //默认使用这段区域
};
2. 注册过程和各自具体的驱动相关,都会用到dma_declare_contiguous。
4. 内存管理子系统进行初始化,CMA如何生效:
a. memblock确定系统的内存布局:memory type与reserved type的划分。
setup_arch--->
setup_machine_fdt--->
early_init_dt_scan--->
early_init_dt_scan_nodes--->
memblock_add
b. reserve block的建立与初始化:
DTS优先级高于命令行:
//built dts
setup_arch--->
arm64_memblock_init--->
early_init_fdt_scan_reserved_mem--->
__fdt_scan_reserved_mem--->
memblock_reserve
//init dts
setup_arch--->
arm64_memblock_init--->
early_init_fdt_scan_reserved_mem--->
fdt_init_reserved_mem--->
__reserved_mem_init_node
//golbal area CMD line
setup_arch--->
arm64_memblock_init--->
dma_contiguous_reserve
5. CMA的两种调用方法:
一种是CMA area是固定位置的,即参数给出了确定的起始地址和大小,这种情况比较简单,直接调用memblock_reserve就OK了。
另外一种情况是动态分配的,这时候,需要调用memblock的内存分配接口memblock_alloc_range来为CMA area分配内存。
6. free memory与reserved memory的伙伴系统处理:
// free memory handle
start_kernel--->
mm_init--->
mem_init--->
free_all_bootmem--->
free_low_memory_core_early--->
__free_memory_core
//reserved memory不会进入buddy system
//CMA初始化:在初始化的过程中,CMA area的内存会全部导入伙伴系统
为了避免这块reserved的内存在不用时候的浪费,
内存管理模块会将CMA区域添加到Buddy System中,用于可移动页面的分配和管理。
core_initcall(cma_init_reserved_areas);
7. CAM工作流程:
伙伴系统中页属性的概念Migrate type:MIGRATE_CMA,只有可移动的页面可以从MIGRATE_CMA的pageblock中分配。non-movable的内存是不可以从CMA区域分配的,保证了之后需要迁移时,可以将movable内存搬走。
如果MIGRATE_MOVABLE 页框链表没有页框后,将第一个访问MIGRATE_CMA 页框链表,在MIGRATE_CMA上分配物理页框。从上面可以看出,只有MIGRATE_MOVABLE才会fallback MIGRATE_CMA链表,分配CMA区域内存。
CMA分配
CMA是通过cma_alloc分配的。cma_alloc->alloc_contig_range(..., MIGRATE_CMA,...),向刚才释放给buddy system的MIGRATE_CMA类型页面,重新“收集”过来。
__init cma_activate_area(struct cma *cma)
--> cma->bitmap = kzalloc(bitmap_size, GFP_KERNEL); ----分配内存
--> zone = page_zone(pfn_to_page(pfn)); ---找到page对应的memory zone
1. CMA area有一个bitmap来管理各个page的状态,这里bitmap_size给出了bitmap需要多少的内存。i变量表示该CMA area有多少个pageblock。
2. 遍历该CMA area中的所有的pageblock。
3. 获得所需pageblock:确保CMA area中的所有page都是在一个memory zone内,同时累加了pfn,从而得到下一个pageblock的初始page frame number。
4. 导入伙伴系统进行管理:将该pageblock导入到伙伴系统,并且将migrate type设定为MIGRATE_CMA。
5. 申请内存:cma_alloc用来从指定的CMA area上分配count个连续的page frame,按照align对齐,从bitmap上搜索free page的过程,一旦搜索到,就调用alloc_contig_range向伙伴系统申请内存。
6. 释放内存:分配连续内存的逆过程,除了bitmap的操作之外,最重要的就是调用free_contig_range,将指定的pages返回伙伴系统。
PS:
需要注意的是,CMA内存分配过程是一个比较“重”的操作,可能涉及页面迁移、页面回收等操作,因此不适合用于atomic context。因为CMA在迁移的过程中需要等待当前页面中的数据回写到磁盘之后,才会进一步的规整为连续内存供gpu/display使用,从而出现卡顿的现象。
#define CMA_NAME "video_cma@69000000"
struct page *cma_page;
static struct device *cma_dev;
np = of_find_node_by_path("/reserved-memory/video_cma@69000000");
struct cma *cma = find_cma_by_name(CMA_NAME);
cma_dev->cma_area = cma;
cma_page = dma_alloc_from_contiguous(cma_dev, alloc->count, 8, GFP_KERNEL);
alloc->vaddr = (unsigned long)page_address(alloc->cma_page);
ret = dma_release_from_contiguous(cma_dev, alloc->cma_page, alloc->count);
//Linux cmd
cat /proc/meminfo
CmaTotal: 16384 kB
CmaFree: 14592 kB
total used free shared buff/cache available
Mem: 31911 5546 560 1177 25805 24729
Swap: 2047 628 1419
在内存不断使用的过程中,会产生碎片,日益使用对内存消耗大的设备比如camera,video,将很难分配到足够大的内存。cma的技术提前分配一块专用内存,在不用的时候释放到伙伴系统中,在使用的时候通过页迁移腾出这块内存。
cma也是一种内存调节技术之一。