重要:本系列文章内容摘自
基于ARM64架构的Linux4.x内核一书,作者余华兵。系列文章主要用于记录Linux内核的大部分机制及参数的总结说明
在系统长时间运行后,内存可能碎片化,很难找到连续的物理页,连续内存分配器(Contiguous Memory Allocator,CMA)使得这种情况下分配大的连续内存块成为可能。
嵌入式系统中的许多设备不支持分散聚集和I/O映射,需要连续的大内存块。例如手机上1300万像素的摄像头,一个像素占用3字节,拍摄一张照片需要大约37MB内存。在系统长时间运行后,内存可能碎片化,很难找到连续的物理页,页分配器和块分配器很可能无法分配这么大的连续内存块。
一种解决方案是为设备保留一块大的内存区域,缺点是:当设备驱动不使用的时候(大多数时间手机摄像头是空闲的),内核的其他模块不能使用这块内存。
连续内存分配器试图解决这个问题,保留一块大的内存区域,当设备驱动不使用的时候,内核的其他模块可以使用,当然有要求:只有申请可移动类型的页时可以借用;当设备驱动需要使用的时候,把已经分配的页迁移到其他地方,形成物理地址连续的大内存块。
编译内核时需要开启以下配置宏:
(1)配置宏CONFIG_CMA,启用连续内存分配器。
(2)配置宏CONFIG_CMA_AREAS,指定CMA区域的最大数量,默认值是7。
(3)配置宏CONFIG_DMA_CMA,启用允许设备驱动分配内存的连续内存分配器。
CMA区域分为全局CMA区域和设备私有CMA区域。全局CMA区域是由所有设备驱动共享的,设备私有CMA区域由指定的一个或多个设备驱动使用。
配置CMA区域有3种方法:
(1)通过内核参数“cma”配置全局CMA区域的大小。
使用内核参数“cma=nn[MG]@[start[MG][-end[MG]]]”设置全局CMA区域的大小和物理地址范围。
(2)通过配置宏配置全局CMA区域的大小。
首先选择指定大小的方式:CONFIG_CMA_SIZE_SEL_MBYTES表示指定兆字节数,CONFIG_CMA_SIZE_SEL_PERCENTAGE表示指定物理内存容量的百分比,默认使用指定兆字节数的方式。
如果选择指定兆字节数的方式,那么通过配置宏CONFIG_CMA_SIZE_MBYTES配置大小。如果配置为0,表示禁止CMA,但是可以传递内核参数“cma=nn[MG]”以启用CMA。
如果选择指定物理内存容量的百分比的方式,那么通过配置宏CONFIG_CMA_SIZE_PERCENTAGE指定百分比。如果配置为0,表示禁用连续内存分配器,但是可以传递内核参数“cma=nn[MG]”以启用连续内存分配器。
(3)通过设备树源文件的节点“/reserved-memory”配置CMA区域,如果子节点的属性“compatible”的值是“shared-dma-pool”,表示全局CMA区域,否则表示设备私有CMA区域。
例如配置3个CMA区域:
1)全局CMA区域,节点名称是“linux,cma”,大小是64MB。
2)帧缓冲设备专用的CMA区域,节点名称是“framebuffer@78000000”,大小是8MB。
3)多媒体处理专用的CMA区域,节点名称是“multimedia-memory@77000000”,大小是64MB。
设备树源文件如下:
/ {
#address-cells = <1>;
#size-cells = <1>;
memory {
reg = <0x40000000 0x40000000>;
};
reserved-memory {
#address-cells = <1>;
#size-cells = <1>;
ranges;
/* 全局的自动配置的连续分配区域 */
linux,cma {
compatible = "shared-dma-pool";
reusable;
size = <0x4000000>;
alignment = <0x2000>;
linux,cma-default;
};
display_reserved: framebuffer@78000000 {
reg = <0x78000000 0x800000>;
};
multimedia_reserved: multimedia@77000000 {
compatible = "acme,multimedia-memory";
reg = <0x77000000 0x4000000>;
};
};
/* ... */
fb0: video@12300000 {
memory-region = <&display_reserved>;
/* ... */
};
scaler: scaler@12500000 {
memory-region = <&multimedia_reserved>;
/* ... */
};
codec: codec@12600000 {
memory-region = <&multimedia_reserved>;
/* ... */
};
};
连续内存分配器是DMA映射框架的辅助框架,设备驱动程序不能直接使用连续内存分配器,软件层次如下所示:
(1)连续内存分配器是在页分配器的基础上实现的,提供的接口cma_alloc用来从CMA区域分配页,接口cma_release用来释放从CMA区域分配的页。
(2)在连续内存分配器的基础上实现了DMA映射框架专用的连续内存分配器,简称DMA专用连续内存分配器,提供的接口dma_alloc_from_contiguous用来从CMA区域分配页,接口dma_release_from_contiguous用来释放从CMA区域分配的页。
(3)DMA映射框架从DMA专用连续内存分配器分配或释放页,为设备驱动程序提供的接口dma_alloc_coherent和dma_alloc_noncoherent用来分配内存,接口dma_free_coherent和dma_free_noncoherent用来释放内存。
(4)设备驱动程序调用DMA映射框架提供的函数来分配或释放内存。
1.数据结构
内核定义了结构体cma来描述一个CMA区域的信息,其代码如下:
mm/cma.h
struct cma {
unsigned long base_pfn;
unsigned long count;
unsigned long *bitmap;
unsigned int order_per_bit;
struct mutex lock;
const char *name;
};
(1)成员base_pfn是CMA区域的起始页帧号。
(2)成员count是页数。
(3)成员bitmap是位图,每个位描述对应的页的分配状态,0表示空闲,1表示已分配。
(4)成员order_per_bit指示位图中的每个位描述的物理页的阶数,目前取值为0,表示每个位描述一页。
可以配置多个CMA区域,内核定义了一个数组用来管理CMA区域,全局变量cma_area_count存放配置的CMA区域的数量:
mm/cma.c
struct cma cma_areas[MAX_CMA_AREAS];
unsigned cma_area_count;
页分配器为CMA区域的物理页定义了迁移类型MIGRATE_CMA:
include/linux/mmzone.h
enum migratetype {
…
#ifdef CONFIG_CMA
MIGRATE_CMA,
#endif
…
};
2.创建CMA区域
内存管理子系统初始化时,解析设备树二进制文件得到物理内存的布局,使用memblock保存布局信息,memblock的memory类型保存内存块的物理地址范围,reserved类型保存保留内存块的物理地址范围,CMA区域属于保留内存块。
首先解析设备树二进制文件中的节点“memory”,把内存块添加到memblock的memory类型。
如果通过设备树源文件配置CMA区域,创建CMA区域的执行流程如下所示:
(1)解析设备树二进制文件中的节点“reserved-memory”,把保留内存块添加到memblock的reserved类型。
(2)函数__reserved_mem_init_node调用注册的所有保留内存初始化函数,保留内存初始化函数是使用宏RESERVEDMEM_OF_DECLARE定义的,放在“__reservedmem_of_table”节里面,其中全局CMA区域的初始化函数是rmem_cma_setup。
(3)函数rmem_cma_setup从数组cma_areas分配一个数组项,保存CMA区域的起始页帧号和页数。如果指定了属性“linux,cma-default”,那么这个CMA区域是默认的CMA区域,设置全局变量dma_contiguous_default_area指向这个CMA区域。
如果通过内核参数或配置宏配置全局CMA区域,创建CMA区域的执行流程如下所示,函数 dma_contiguous_reserve 负责创建全局CMA区域:
3.把CMA区域释放给页分配器
memblock是内核初始化的时候使用的内存分配器,内核初始化完以后使用伙伴分配器管理物理页。内核初始化完成的时候,把空闲的内存释放给伙伴分配器,不会把保留的内存释放给伙伴分配器。CMA区域属于保留的内存,但是我们需要把CMA区域的物理页交给伙伴分配器管理。
连续内存分配器注册了初始化函数cma_init_reserved_areas:
mm/cma.c
core_initcall(cma_init_reserved_areas);
函数cma_init_reserved_areas负责把所有CMA区域的物理页释放给伙伴分配器,执行流程如下所示。针对每个CMA区域,先把页块的迁移类型设置为MIGRATE_CMA,然后调用函数__free_pages,把页块释放给伙伴分配器:
4.从CMA区域借用页
当设备驱动程序不使用CMA区域的时候,内核的其他模块可以借用CMA区域的物理页,页分配器只允许可移动类型从CMA类型借用物理页。
如下所示,页分配器分配物理页的时候,执行流程如下:
(1)从指定迁移类型分配页。
(2)如果分配失败,从备用迁移类型借用物理页。
5.从CMA区域分配内存
当设备驱动程序需要使用CMA区域的时候,如果CMA区域中的物理页已经被页分配器分配出去,需要把物理页迁移到其他地方。
函数cma_alloc负责从CMA区域分配内存,执行流程如下所示:
(1)在CMA区域的位图中查找一个足够大的空闲页块。
(2)在位图中把物理页的分配状态设置为已分配。
(3)调用函数alloc_contig_range,把页分配器已分配出去的物理页迁移到其他地方。
(4)如果迁移失败,回到第1步,查找下一个足够大的空闲页块并尝试分配,直到分配成功或者尝试完所有空闲页块。
函数alloc_contig_range的执行流程如下:
(1)调用函数start_isolate_page_range,把物理页的迁移类型设置为隔离类型(MIGRATE_ISOLATE),隔离物理页,防止页分配器把空闲页分配出去。
(2)调用函数__alloc_contig_migrate_range,处理页分配器已分配出去的物理页。
(3)调用函数isolate_freepages_range处理空闲页,把空闲页从页分配器的空闲链表中删除。
(4)调用函数undo_isolate_page_range,撤销对物理页的隔离,把物理页的迁移类型设置为CMA类型。
6.释放CMA区域的内存
函数cma_release负责释放CMA区域的内存,执行流程如下所示:
(1)检查物理页是否属于CMA区域。
(2)把物理页释放给页分配器。
(3)在CMA区域的位图中把物理页的分配状态设置为空闲。