linux内存管理之CMA
linux内存管理之CMA
备注:
- Kernel版本:5.4.95
- 使用工具:Source Insight 4.0
文章目录
CMA概述
什么是CMA
CMA,Contiguous Memory Allocator,是内存管理子系统中的一个模块,负责物理地址连续的内存分配,一般我们把这块区域定义为reserved-memory。一般系统会在启动过程中,从整个memory中配置一段连续内存用于CMA,然后内核其他的模块可以通过CMA的接口API进行连续内存的分配。
CMA的核心并不是设计精巧的算法来管理地址连续的内存块,实际上它的底层还是依赖内核伙伴系统这样的内存管理机制,或者说CMA是处于需要连续内存块的其他内核模块(例如DMA mapping framework)和内存管理模块之间的一个中间层模块,主要功能包括:
- 1、解析DTS或者命令行中的参数,确定CMA内存的区域,这样的区域我们定义为CMA area
- 2、提供cma_alloc和cma_release两个接口函数用于分配和释放CMA pages
- 3、记录和跟踪CMA area中各个pages的状态
- 4、调用伙伴系统接口,进行真正的内存分配
早期的Linux内核中没有cma的实现,如果驱动想要申请一个大块的物理连续内存,那么只能通过预留专属内存的形式,然后在驱动中使用ioremap来映射后作为私有内存使用。这样带来的后果就是有一部分内存将被预留出来不能作为系统中的通用内存来使用,比如camera、audio设备,它们在工作时是需要大块连续内存进行DMA操作的,而当这些设备不工作时,预留的内存也无法被其他模块所使用。
如何使得操作系统能够充分的利用物理内存呢?比如当一些设备需要使用大块连续物理内存时,可以比较容易的申请到,而当这些设备不工作时,这些内存又可以当做普通的内存那样被系统其他模块申请使用。引入CMA就是为了解决这个问题的,定义为cma区域的内存,也是由操作系统来管理的,当一个驱动模块想要申请大块连续内存时,通过内存管理子系统把CMA区域的内存进行迁移,空出连续内存给驱动使用;而当驱动模块释放这块连续内存后,它又被归还给操作系统管理,可以给其他申请者分配使用。
综上所述,问题本质上是整机内存利用率与 CMA 业务性能之间的平衡。那么,CMA如果解决上述问题呢?它采取了折中的办法:
- 开机时,系统预留出 CMA 区域。
- 在 CMA 业务不使用时,允许其他业务有条件地使用 CMA 区域,条件是申请页面的属性必须是可迁移的。
- 当 CMA 业务使用时,把其他业务的页面迁移出 CMA 区域,以满足 CMA 业务的需求。
CMA 数据结构
cma页面类型
MIGRATE_CMA 为cma内存区域专属的内存类型,相关定义如下:
// 源码:include/linux/mmzone.h
enum migratetype {
//无法移动和检索的类型,用于内核分配的页面,I/O缓冲区,内核堆栈等
MIGRATE_UNMOVABLE,
//当需要大的连续内存时,通过移动当前使用的页面来尽可能防止碎片,用于分配用户内存
MIGRATE_MOVABLE,
//当没有可用内存时使用此类型
MIGRATE_RECLAIMABLE,
MIGRATE_PCPTYPES, /* the number of types on the pcp lists */
//减少原子分配请求无法进行高阶页面分配的可能,内核会提前准备一个页面块
MIGRATE_HIGHATOMIC = MIGRATE_PCPTYPES,
#ifdef CONFIG_CMA
/*
* MIGRATE_CMA migration type is designed to mimic the way
* ZONE_MOVABLE works. Only movable pages can be allocated
* from MIGRATE_CMA pageblocks and page allocator never
* implicitly change migration type of MIGRATE_CMA pageblock.
*
* The way to use it is to change migratetype of a range of
* pageblocks to MIGRATE_CMA which can be done by
* __free_pageblock_cma() function. What is important though
* is that a range of pageblocks must be aligned to
* MAX_ORDER_NR_PAGES should biggest page be bigger then
* a single pageblock.
*/
//页面类型由CMA内存分配器单独管理
MIGRATE_CMA,
#endif
#ifdef CONFIG_MEMORY_ISOLATION
//内核会暂时更改为这种类型,以迁移使用中的系列活动页面
MIGRATE_ISOLATE, /* can't allocate from here */
#endif
MIGRATE_TYPES
};
cma定义
// 源码:mm/cma.h
struct cma {
unsigned long base_pfn; //CMA区域物理地址的起始页帧号;
unsigned long count; //CMA区域总体的页数;
//位图中每个bit描述的物理页面的order值,其中页面数为2^order值;
unsigned long *bitmap; //描述页的分配情况;
//指明该 CMA 区域的 bitmap 中,每个 bit 代表 的 page 数量
unsigned int order_per_bit; /* Order of pages represented by one bit */
struct mutex lock;
#ifdef CONFIG_CMA_DEBUGFS
struct hlist_head mem_head;
spinlock_t mem_head_lock;
#endif
//CMA 区域的名字
const char *name;
};
extern struct cma cma_areas[MAX_CMA_AREAS];
extern unsigned cma_area_count;
cma模块使用bitmap来管理其内存的分配,0表示free,1表示已经分配。
具体内存管理的单位和struct cma中的order_per_bit成员相关,如果order_per_bit等于0,表示按照一个一个page来分配和释放,如果order_per_bit等于1,表示按照2个page组成的block来分配和释放,以此类推。
struct cma中的bitmap成员就是管理该cma area内存的bit map。
count成员说明了该cma area内存有多少个page。它和order_per_bit一起决定了bitmap指针指向内存的大小。
base_pfn定义了该CMA area的起始page frame number,base_pfn和count一起定义了该CMA area在内存在的位置。
如下图:
cma内存域的创建
一开始,CMA area的概念是全局的,通过内核配置参数和命令行参数,内核可以定位到Global CMA area在内存中的起始地址和大小(注:这里的Global的意思是针对所有的driver而言的)。并在初始化的时候,调用dma_contiguous_reserve函数,将指定的memory region保留给Global CMA area使用。人性是贪婪的,驱动亦然,很快,有些驱动想吃独食,不愿意和其他驱动共享CMA,因此出现两种CMA area:Global CMA area给大家共享,而Per Device CMA可以给指定的一个或者几个驱动使用。这时候,命令行参数不是那么合适了,因此引入了device tree中的reserved memory node的概念。当然,为了兼容,内核仍然支持CMA的command line参数。
命令行参数建立cma
通过命令行参数也可以建立cma area。我们可以通过 “cma=nn[MG]@[start[MG][-end[MG]]]” 这样命令行参数来指明Global CMA area在整个物理内存中的位置。在初始化过程中,内核会解析这些命令行参数,获取CMA area的位置(起始地址,大小),并调用cma_declare_contiguous接口函数向CMA模块进行注册(当然,和device tree传参类似,最终也是调用cma_init_reserved_mem接口函数)。除了命令行参数,通过内核配置(CMA_SIZE_MBYTES和CMA_SIZE_PERCENTAGE)也可以确定CMA area的参数。
device_tree建立cma
按照CMA的使用范围,它也可以分为两种类型,一种是通用的CMA区域(Global CMA area),该区域是给整个系统分配使用的,另一种是专用的CMA区域(Per Device CMA),这种是专门为单个模块定义的,定义它的目的是不太希望和其他模块共享该区域,我们可以在dts中定义不同的CMA区域,每个区域实际上就是一个reserved memory,对于共享的CMA。
device_tree建立global cma area
// 源码:arch/arm/boot/dts/sun4i-a10.dtsi
reserved-memory {
#address-cells = <1>;
#size-cells = <1>;
ranges;
/* Address must be kept in the lower 256 MiBs of DRAM for VE. */
default-pool {
compatible = "shared-dma-pool";
size = <0x6000000>;
alloc-ranges = <0x40000000 0x10000000>;
reusable;
linux,cma-default;
};
};
对于CMA区域的dts配置来说,有三个关键点:
- 第一点,一定要包含有reusable,表示当前的内存区域除了被dma使用之外,还可以被内存管理子系统reuse。
- 第二点,不能包含有no-map属性,该属性表示是否需要创建页表映射,对于通用的内存,必须要创建映射才可以使用,而CMA是可以作为通用内存进行分配使用的,因此必须要创建页表映射。
- 第三点,对于共享的CMA区域,需要配置上linux,cma-default属性,标志着它是共享的CMA。
device_tree建立per device area
//源码:arch/arm/boot/dts/stm32mp157a-dk1.dts
reserved-memory {
#address-cells = <1>;
#size-cells = <1>;
ranges;
.......
gpu_reserved: gpu@d4000000 {
reg = <0xd4000000 0x4000000>;
no-map;
};
};
先在reserved memory中定义专用的