背景
-
Read the fucking source code!
--By 鲁迅 -
A picture is worth a thousand words.
--By 高尔基
说明:
- Kernel版本:4.14
- ARM64处理器,Contex-A53,双核
- 使用工具:Source Insight 3.5, Visio
1. 概述
在我们使用ARM等嵌入式Linux系统的时候,一个头疼的问题是GPU,Camera,HDMI等都需要预留大量连续内存,对于内核如果申请一块连续的内存空间该怎么处理呢?
首先向到的是利用内核提供的kmalloc申请,尽管kmalloc可以申请连续的内存空间,但是在长时间的测试中,会出现内存空间可能申请失败的情况,无法保证能成功分配。
使用memblock分配器中提供的方法,称为预留内存,但这么预留的内存只能被特定的Device驱动所使用,System不能分配这部分内容,会导致内存浪费
因此,内核设计者设计了CMA,即连续物理内存管理,本章主要是针对CMA进行原理和代码流程梳理。
1. CMA简介
1.1 什么是CMA内存分配技术
CMA,Contiguous Memory Allocator,是一种用于申请大量的,并且物理上连续的内存块的方法。是linux kernel内存管理系统的扩展功能,目的在于解决需要预留大量连续内存导致运行内存紧张的问题。连续内存分配器(CMA - Contiguous Memory Allocator)是一个框架,允许建立一个平台无关的配置,用于连续内存的管理。然后,设备所需内存都根据该配置进行分配。
在LWN上可以追溯到2011年6月。原理虽简单,但是其实现起来却相当复杂,因为需要许多子系统之间相互协作。一般系统在启动过程中,从整个Memory中配置一段连续内存用于CMA,然后内核其他的模块就可以通过CMA的接口API进行连续的内存分配。其功能主要包括
解析DTS或者命令行参数,确定CMA内存的区域,也就是定义的CMA eara
提供cma_alloc和cma_release两个接口函数用于分配和释放CMA
记录和跟踪CMA area的各个page状态
调用伙伴系统接口,进行真正的内存分配
CMA的初始化必须在buddy系统工作之前和memblock分配器初始化完成之后
1.2 为什么需要CMA技术
在嵌入式设备中,现状很多外设(摄像头,硬件视频编码,GPU等),都需要较大的内存缓冲区,例如对于现在一个200W的像素的高清摄像机,就需要一个超过6M的内存缓冲区,如我们现在更高清的8K,就需要更大的缓冲区。而内核提供的kmalloc内存分配机制对于这么大的内存,由于因为内存碎片问题,会导致分配不到足够的内存空间。[详细的应用场景见文档什么是CMA内存分配技术?有机顶盒有什么作用
Contiguous Memory Allocator, CMA
,连续内存分配器,用于分配连续的大块内存。CMA分配器
,会Reserve一片物理内存区域:
- 设备驱动不用时,内存管理系统将该区域用于分配和管理可移动类型页面;
- 设备驱动使用时,用于连续内存分配,此时已经分配的页面需要进行迁移;
此外,CMA分配器
还可以与DMA子系统
集成在一起,使用DMA的设备驱动程序无需使用单独的CMA API
。
2. 数据结构
内核定义了struct cma
结构,用于管理一个CMA区域
,此外还定义了全局的cma数组
,如下:
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 mutex lock;
#ifdef CONFIG_CMA_DEBUGFS
struct hlist_head mem_head;
spinlock_t mem_head_lock;
#endif
const char *name;
};
extern struct cma cma_areas[MAX_CMA_AREAS];
extern unsigned cma_area_count;
-
base_pfn
:CMA区域物理地址的起始页帧号; -
count
:CMA区域总体的页数; -
*bitmap
:位图,用于描述页的分配情况; -
order_per_bit
:位图中每个bit
描述的物理页面的order
值,其中页面数为2^order
值;
来一张图就会清晰明了:
3. 流程分析
3.1 CMA区域创建
3.1.1 方式一 根据dts来配置
之前的文章也都分析过,物理内存的描述放置在dts
中,最终会在系统启动过程中,对dtb
文件进行解析,从而完成内存信息注册。
CMA
的内存在dts
中的描述示例如下图:
在dtb
解析过程中,会调用到rmem_cma_setup
函数:
RESERVEDMEM_OF_DECLARE(cma, "shared-dma-pool", rmem_cma_setup);
3.1.2 方式二 根据参数或宏配置
可以通过内核参数或配置宏,来进行CMA区域的创建,最终会调用到cma_declare_contiguous
函数,如下图:
3.2 CMA添加到Buddy System
在创建完CMA区域
后,该内存区域成了保留区域,如果单纯给驱动使用,显然会造成内存的浪费,因此内存管理模块会将CMA区域
添加到Buddy System
中,用于可移动页面的分配和管理。CMA区域
是通过cma_init_reserved_areas
接口来添加到Buddy System
中的。
core_initcall(cma_init_reserved_areas);
core_initcall
宏将cma_init_reserved_areas
函数放置到特定的段中,在系统启动的时候会调用到该函数。
3.3 CMA分配/释放
- CMA分配,入口函数为
cma_alloc
:
- CMA释放,入口函数为
cma_release
:
函数比较简单,直接贴上代码
/**
* cma_release() - release allocated pages
* @cma: Contiguous memory region for which the allocation is performed.
* @pages: Allocated pages.
* @count: Number of allocated pages.
*
* This function releases memory allocated by alloc_cma().
* It returns false when provided pages do not belong to contiguous area and
* true otherwise.
*/
bool cma_release(struct cma *cma, const struct page *pages, unsigned int count)
{
unsigned long pfn;
if (!cma || !pages)
return false;
pr_debug("%s(page %p)\n", __func__, (void *)pages);
pfn = page_to_pfn(pages);
if (pfn < cma->base_pfn || pfn >= cma->base_pfn + cma->count)
return false;
VM_BUG_ON(pfn + count > cma->base_pfn + cma->count);
free_contig_range(pfn, count);
cma_clear_bitmap(cma, pfn, count);
trace_cma_release(pfn, pages, count);
return true;
}
3.4 DMA使用
代码参考driver/base/dma-contiguous.c
,主要包括的接口有:
/**
* dma_alloc_from_contiguous() - allocate pages from contiguous area
* @dev: Pointer to device for which the allocation is performed.
* @count: Requested number of pages.
* @align: Requested alignment of pages (in PAGE_SIZE order).
* @gfp_mask: GFP flags to use for this allocation.
*
* This function allocates memory buffer for specified device. It uses
* device specific contiguous memory area if available or the default
* global one. Requires architecture specific dev_get_cma_area() helper
* function.
*/
struct page *dma_alloc_from_contiguous(struct device *dev, size_t count,
unsigned int align, gfp_t gfp_mask);
/**
* dma_release_from_contiguous() - release allocated pages
* @dev: Pointer to device for which the pages were allocated.
* @pages: Allocated pages.
* @count: Number of allocated pages.
*
* This function releases memory allocated by dma_alloc_from_contiguous().
* It returns false when provided pages do not belong to contiguous area and
* true otherwise.
*/
bool dma_release_from_contiguous(struct device *dev, struct page *pages,
int count);
在上述的接口中,实际调用的就是cma_alloc/cma_release
接口来实现的。
整体来看,CMA分配器还是比较简单易懂,也不再深入分析。
4.后记
内存管理的分析先告一段落,后续可能还会针对某些模块进一步的研究与完善。
内存管理子系统,极其复杂,盘根错节,很容易就懵圈了,尽管费了不少心力,也只能说略知皮毛。
1.3 设计思路
根据git的合入记录,CMA(Contiguous Memory Allocator,连续内存分配器)是在内核3.5的版本引入,由三星的工程师开发实现的,用于DMA映射框架下提升连续大块内存的申请。
对于这种连续的大块区域,内核提供了resvered保留空间,但是这个空间会导致本来就有限的内存空间浪费。CMA主要设计目的是提供一个以下功能:
可以分配连续的大的内存空间
防止reseve方式的内存浪费
(1) 支持Migration功能,所以即使是被某个驱动设备Reserve的区域,在驱动没有使用的时候System可以对该段内存进行分配使用
(2) 在System使用这段内存的时候,如果驱动要求分配这个预留的内存,System memory就会被Migration到其他内存区域,之后这段内存被分配给驱动设备
驱动设备间的内存共享(通过CMA被Reserve的内存会通过CMA进行管理,所以可以驱动设备间共享该段,比如FIMC可以共享MFC预留的内存区域等)
2. 数据结构
内核定义了struct cma结构,用于管理一个CMA区域,此外还定义了全局的cma数组,如下:
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 mutex lock;
#ifdef CONFIG_CMA_DEBUGFS
struct hlist_head mem_head;
spinlock_t mem_head_lock;
#endif
}
base_pfn : CMA区域物理地址的起始页帧号
count : CMA区域的总页数
*bitmap : 位图,用于描述页的分配情况,0表示free,1表示已经分配
order_per_bit : 位图中每个Bit描述的物理页面的order值,其中页面数为2^order值。如果为0,表示按照一个一个page来分配和释放;如果是1,表示按照2个page的组成的block来分配和释放,依次类推。
对于内核,CMA模块定义了若干(MAX_CMA_AREAS = 7)个CAM erea,代码如下
struct cma cma_areas[MAX_CMA_AREAS];
1
3. 流程分析
3.1 CMA区域创建
3.1.1. DTS创建
物理内存的描述放置在dts中,最终会在系统启动过程中,对dtb文件进行解析,从而完成内存信息注册。
CMA的内存在dts中的描述如下
reserved-memory {
#address-cells = <1>;
#size-cells = <1>;
ranges; linux,cma {
compatible = "shared-dma-pool";
reusable;
size = <0x14000000>;
linux,cma-default;
};
};
device tree中可以包含reserved-memory node,在该节点的child node中,可以定义各种保留内存的信息。compatible属性是shared-dma-pool的那个节点是专门用于建立 global CMA area的。
RESERVEDMEM_OF_DECLARE(cma, "shared-dma-pool", rmem_cma_setup);
static int __init rmem_cma_setup(struct reserved_mem *rmem)
{
phys_addr_t align = PAGE_SIZE << max(MAX_ORDER - 1, pageblock_order);
phys_addr_t mask = align - 1;
unsigned long node = rmem->fdt_node;
struct cma *cma;
int err; if (!of_get_flat_dt_prop(node, "reusable", NULL) || --------------(1)
of_get_flat_dt_prop(node, "no-map", NULL))
return -EINVAL; if ((rmem->base & mask) || (rmem->size & mask)) {
pr_err("Reserved memory: incorrect alignment of CMA region\n");
return -EINVAL;
} err = cma_init_reserved_mem(rmem->base, rmem->size, 0, &cma); ---------------(2)
if (err) {
pr_err("Reserved memory: unable to setup CMA region\n");
return err;
}
/* Architecture specific contiguous memory fixup. */
dma_contiguous_early_fixup(rmem->base, rmem->size); if (of_get_flat_dt_prop(node, "linux,cma-default", NULL)) ---------------(3)
dma_contiguous_set_default(cma); rmem->ops = &rmem_cma_ops;
rmem->priv = cma; pr_info("Reserved memory: created CMA memory pool at %pa, size %ld MiB\n",
&rmem->base, (unsigned long)rmem->size / SZ_1M); return 0;
}
1.CMA对应的reserved memory节点必须有reusable属性,不能有no-map的属性,然后就是base和size的检查
(1) 对于reusable属性,其有reserved memory这样的属性,当驱动程序不使用这些内存的时候,OS可以使用这些内存;而当驱动程序从这个CMA area分配memory的时候,OS可以释放这些内存,让驱动可以使用它。
(2) no-map属性与地址映射有关,如果没有no-map属性,那么OS就会为这段memory创建地址映射,象其他普通内存一样。但是对于no-map属性,往往是专用
2.用dtb中解析出来的地址信息来初始化CMA
(1). 首先使用memblock_is_region_reserved判断分配给CMA区域的内存释放已经被预留了
(2). alignment检查
(3). 从全局CMA数组中获取CMA实例,初始化各个字段
3.如果dts指定了linux,cma-default,则将dma_contiguous_set_default指向这个CMA区域
3.1.2. 据参数或宏配置
可以通过内核参数或配置宏,来进行CMA区域的创建,例如cma=64M,在初始化过程中,内核会解析这些命令行参数,获取CMA area的位置(起始地址,大小),并调用cma_declare_contiguous接口函数向CMA模块进行注册(当然,和device tree传参类似,最终也是调用cma_init_reserved_mem接口函数)。除了命令行参数,通过内核配置(CMA_SIZE_MBYTES和CMA_SIZE_PERCENTAGE)也可以确定CMA area的参数。
static int __init early_cma(char *p)
{
pr_debug("%s(%s)\n", __func__, p);
size_cmdline = memparse(p, &p);
if (*p != '@')
return 0;
base_cmdline = memparse(p + 1, &p);
if (*p != '-') {
limit_cmdline = base_cmdline + size_cmdline;
return 0;
}
limit_cmdline = memparse(p + 1, &p); return 0;
}
early_param("cma", early_cma);
解析的cmdline参数后,会通过前面一张Memblock章节中arm_memblock_init会调用dma_contiguous_reserve(arm_dma_limit)进行初始化,最终会调用cma_declare_contiguous来进行初始化。如果使用的是device tree,则应该不会使用cma_declare_contiguous来进行初始化。而对于该接口主要完成以下工作
通过memblock_end_of_DRAM计算物理内存的末端地址,防止越界
如果使用固定地址,直接保留这段区域memblock_reserve,否则通过membloc分配器进行区域分配
cma_init_reserved_mem从全局CMA数组中获取一个实例,初始化操作
3.2 CMA初始化
在创建完CMA区域后,该内存区域成了保留区域图,如果单纯的给驱动使用,显然会超成内存的浪费,因此内存模块会将该CMA区域添加到Buddy System中,可用于页面的分配和管理。
内存管理子系统进行初始化的时候,首先是通过memblock掌握全局的,它确定了整个系统的内存布局。哪些是memory是Memory type,而哪些memory block是reserved type,memblock分配器中有相关的介绍。memblock始终是初始化阶段的内存管理模块,最终我们还是要转向伙伴系统。free memory被释放到伙伴系统中,而reserved memory不会进入伙伴系统,对于CMA area,我们之前说过,最终被由伙伴系统管理,因此,在初始化的过程中,CMA area的内存会全部导入伙伴系统(方便其他应用可以通过伙伴系统分配内存)。具体代码/drivers/base/dma-contiguous.c代码文件中,可以找到其初始化函数cma_init_reserved_areas(),其通过core_initcall()注册到系统初始化中。
static int __init cma_init_reserved_areas(void)
{
int i; for (i = 0; i < cma_area_count; i++) {
int ret = cma_activate_area(&cma_areas[i]); if (ret)
return ret;
} return 0;
}
其主要是通过遍历cma_ereas的CMA管理区信息,调用cma_activate_area将各个区进行初始化
static int __init cma_activate_area(struct cma *cma)
{
int bitmap_size = BITS_TO_LONGS(cma_bitmap_maxno(cma)) * sizeof(long);
unsigned long base_pfn = cma->base_pfn, pfn = base_pfn;
unsigned i = cma->count >> pageblock_order;
struct zone *zone; cma->bitmap = kzalloc(bitmap_size, GFP_KERNEL); -------------------(1)
if (!cma->bitmap)
return -ENOMEM; WARN_ON_ONCE(!pfn_valid(pfn));
zone = page_zone(pfn_to_page(pfn)); do { -------------------(2)
unsigned j; base_pfn = pfn;
for (j = pageblock_nr_pages; j; --j, pfn++) { -------------------(3)
WARN_ON_ONCE(!pfn_valid(pfn));
/*
* alloc_contig_range requires the pfn range
* specified to be in the same zone. Make this
* simple by forcing the entire CMA resv range
* to be in the same zone.
*/
if (page_zone(pfn_to_page(pfn)) != zone)
goto err;
}
init_cma_reserved_pageblock(pfn_to_page(base_pfn)); -------------------(4)
} while (--i); mutex_init(&cma->lock);
#ifdef CONFIG_CMA_DEBUGFS
INIT_HLIST_HEAD(&cma->mem_head);
spin_lock_init(&cma->mem_head_lock);
#endif return 0;
err:
kfree(cma->bitmap);
cma->count = 0;
return -EINVAL;
}
1.CMA eara有一个bitmap来管理各个page的状态,这里的bitmap_size给出了Bitmap需要多少内存,i变量表示该CMA eara有多少个pageblock
2.遍历该CMA area中的所有的pageblock
3.确保CMA area中的所有page都是在一个memory zone内,同时累加了pfn,从而得到下一个pageblock的初始page frame number
4.最终调用init_cma_reserved_pageblock,以pageblock为单位进行处理,设置migrate type为MIGRATE_CMA;将页面添加到伙伴系统中;调正zone管理的页面总数
3.3 分配和释放内存
cma_alloc用来从指定的CMA area上分配count个连续的page frame,按照align对齐
extern struct page *cma_alloc(struct cma *cma, size_t count, unsigned int align);
cma_release用来释放分配count个连续的page frame
extern bool cma_release(struct cma *cma, const struct page *pages, unsigned int count);
linux内核对于体系架构的DMA实现通用的架构实现,用以下方式分配CMA内存
struct page *dma_alloc_from_contiguous(struct device *dev, int count, unsigned int align);
第一个参数是需要为之分配内存的设备。第二个参数指定了分配的页数(不是字节或阶)。第三个参数是页阶的校正。这个参数使分配缓存时物理地址按照2^align页对齐。为了防止碎片,这里至少输入0。值得注意的是有一个Kconfig选项(CONFIG_CMA_ALIGNMENT)指定了可以接受的最大对齐数值。默认值是8,即256页对齐。
要释放申请的空间,调用如下方法:
bool dma_release_from_congiguous(struct device *dev, struct page *pages, int count);
dev和count参数和前面的一致,pages是dma_alloc_from_contiguous()方法返回的指针。如果传入这个方法的区域不是来自CMA,该方法会返回false。否则它将返回true。这消除了更上层方法跟踪分配来自CMA或其他方法的需要。
注意dma_alloc_from_congiguous()不能用在需要原子请求的上下文中。因为它执行了一些“沉重”的如页移植、直接回收等需要一定时间的操作。正因为如此,为了使dma_alloc_coherent()及其友元函数工作正常,相关体系需要在原子请求下有不同的方法来分配内存。最简单的解决方案是在启动时预留一部分内存专门用于原子请求的分配。这其实就是ARM的做法。现存的体系大都已经有了特殊的路径来解决原子分配。
4. 总结
本章了解了CMA的背景和整个处理流程,其主要的功能是,当驱动没有分配使用的时候,这些memory可以被内核的其他模块使用;而当驱动分配CMA内存后,那些被其他模块使用的内存需要释放出来,形成物理地址连续的大块内存。对于CMA的接口层,驱动程序并不会调用CMA的模块接口,而是通过DMA framework层来使用CMA的服务。
我们说buddy容易碎,但DMA通常只能操作一段物理上连续的内存,因此我们应该保证系统有足量的连续内存以使DMA正常工作。
1. 预分配一块内存
可以在系统启动时就预留出部分内存给DMA专用,这通常要在bootmem的阶段做,使这部分内存和buddy系统分离。并且需要提供申请释放内存的API给每个有需求的device。可以借用bigphysarea来完成。这种做法的缺点是这块连续内存永远不能给其他地方用(即使有没有被使用),可能被浪费,并且需要额外的物理内存管理。但这种预分配的思想在很多场合是最省力也很常用的。
2. IOMMU
如果device的DMA支持IOMMU(MMU for I/O),也就是DMA内部有自己的MMU,就相当于MMU之于CPU(将virtual address映射到physical address),IOMMU可以将device address映射到physical address。这样就不再需要物理地址连续了。IOMMU相比普通DMA访存要耗时且耗电,不太常见。
3. CMA
处理DMA中的碎片有一个利器,CMA(连续内存分配器,在kernel v3.5-rc1正式被引入)。和磁盘碎片整理类似,这个技术也是将内存碎片整理,整合成连续的内存。因为对一个虚拟地址,它可以在不同时间映射到不同的物理地址,只要内容不变就行,对程序员是透明的。
上面讲了,物理地址映射关系对于CPU来讲是透明的,因此可以说虚拟内存是可移动(movable)的,但内核的内存一般不移动,应用程序一般就可以。应用程序在申请内存的时候可以标记我的这块内存是__GFP_MOVABLE的,让CMA认为可搬移。
CMA的原理就是标记一段连续内存,这段内存平时可以作为movable的页面使用。那么应用程序在申请内存时如果打上movable的标记,就可以从这段连续内存里申请。当然,这段内存慢慢就碎了。当一个设备的DMA需要连续内存的时候,CMA就可以发挥作用了:比如设备想申请16MB连续内存,CMA就会从其他内存区域申请16MB,这16MB可能是碎的,然后将自己区域中已经被分出去的16MB的页面一一搬移到新申请的16MB页面中,这时CMA原来标记的内存就空出来了。CMA还要做一件事情,就是去修改被搬离的页所属的哪些进程的页表,这样才能让用户程序在毫无知觉的情况下继续正常运行。
注意,CMA的API是封装到DMA里面,所以你不能直接调用CMA接口,DMA的底层才用CMA(当然DMA也可以不用CMA机制,如果你的CPU不带CMA就更不用说了)。如果你的系统支持CMA,dma_alloc_coherence()内部就可能用CMA实现。
dts里面可指定哪部分内存是可CMA的。可以是全局的CMA pool,也可以为某个特定设备指定CMA pool。具体填法见内核源码中的 Documentation\devicetree\bindings\reserved-memory\reserved-memory.txt。