CMA(连续的内存分配)与dma_alloc_writecombine异常现象和分析1
cma,全称(contiguous memory allocation),在内存初始化时预留一块连续内存,可以在内存碎片化严重时通过调用dma_alloc_contiguous接口并且gfp指定为__GFP_DIRECT_RECLAIM从预留的那块连续内存中分配大块连续内存。
原来 dma_alloc_coherent 在 arm 平台上会禁止页表项中的 C (Cacheable) 域以及 B (Bufferable)域。
而 dma_alloc_writecombine 只禁止 C (Cacheable) 域。
什么是CMA
Linux内核的连续内存分配器 (CMA)——避免预留大块内存。嵌入式Linux系统的时候,GPU,Camera,HDMI等都需要预留大量连续内存,随着内核的运行,内核中的物理内存越来越趋向于碎片化,但是某些特定的设备在使用时用到的 DMA 需要大量的连续物理内存但是一般的做法又必须先预留着。通过这套机制,我们可以做到不预留内存,这些内存平时是可用的,只有当需要的时候才被分配给Camera,HDMI等设备。
在使用LCD显示功能驱动时遇到不断打开和关闭显示设备,出现dma_alloc_writecombine 申请内存失败的情况。理论上程序应该允许申请失败,失败后再重新申请能获取到空间即可。 尽管dma_alloc_writecombine 函数和dma_free_writecombine 成对的调用,实际申请内存空间的物理地址不断的在累加,并不是完全复用释放的内存空间地址。 内存释放相关后台程序周期有一定关系,内存释放并不是实时的。不过程序测试过程中比较奇怪的事,很多分配的空间好像长时间没有释放,导致空间不足感觉不太应该。
通过错误信息上图中2部分信息,可以获知是在arm_dma_alloc 函数获取DMA内存失败导致异常。该函数在内核/Linux/arch/arm/mm/dma-mapping.c。 对于DMA使用内存,在我们LINUX 系统下的DTS是这样定义的:
图中dts的linux,cma 定义的保留内存区域是预留给DMA专用的内存区域,配置只设置了size没有像其下面vpu_mem定义使用了reg,系统会使用内存的最高端的对应size大小区域用于做DMA申请大段连续内存。其他关键字的定义和说明如下:
- compatible(可选):通常情况下,保留内存并不需要 compatible 属性,因为保留内存并不需要相应的驱动程序来处理它,一个特殊的情况就是上面提到的 CMA 保留内存,CMA 内存在作为保留内存的同时还需要可以被 buddy 子系统管理,需要做一些特殊的设置,因此需要编写相应的驱动程序,使用 compatible 属性来关联相应的驱动程序,并在内核的启动阶段调用,完成 CMA 保留内存的初始化。 CMA 内存对应的 compatible 属性为:shared-dma-pool,这是内核规定的一个固定值。 当然,在一些特殊情况下,如果保留内存需要驱动程序进行一些特殊处理,也可以提供自定义的 compatible 属性以在内核启动阶段执行相应的驱动程序,这都是很灵活的。
- no-map(可选):在 32 位的系统上,由于内核线性地址的限制,物理内存被分为两个部分:高端内存和低端内存,低端内存会被直接线性映射到内核地址空间中,而高端内存暂时不会进行映射,只有在需要使用到的时候再动态地映射到内核中。而在 64 位系统中,由于内核线性地址完全足够,就会直接将所有物理内存映射到内核虚拟地址中。 默认情况下,32 位系统地低端内存和 64 位系统所有物理内存都将会被映射到内核中,而且通过简单地线性转换就可以通过物理地址找到对应地虚拟地址,执行内存访问。 如果在保留内存处设置了 no-map 属性,这部分保留内存将不会建立虚拟地址到物理地址的映射,也就是即使获取了这部分保留内存,也是不能直接访问的,而是需要使用者自己建立页表映射,通常直接使用 ioremap 来重新建立映射再访问,为保留内存添加 no-map 属性可以提供更高的安全性和灵活性,毕竟只有在特定设备建立映射之后才能访问,同时在建立页表映射时可以根据不同的业务场景自行指定该片内存的缓存策略,话说回来,没有添加 no-map 属性的保留内存同样也可以在高端内存区重新建立映射,只是这种情况很少见。 既然讲到这儿,就多说两句底层实现吧,内核初始化阶段使用的是 memblock 内存分配器,对于 32 位系统,memblock 只会管理低端内存,memblock 负责收集系统的物理内存信息,主要是通过设备树。memblock.memory 节点用于保存 memory 节点提供的物理内存信息,对于已经被分配掉的或者需要保留的内存区间将会被额外地添加到 memblock.reserved 中(memblock.reserved 中的内存段依旧保存在 memblock.memory 中),同时,memblock.memory 中所有的物理内存都会建立虚拟映射,在内存初始化的后期阶段,memblock.memory 除去 memblock.reserved 中的所有内存页面都会被移交给 buddy 子系统。 再回过头看保留内存的设置,默认的保留内存会被添加到 memblock.reserved 中,表示不会被移交给 buddy 子系统,但是依旧会被 memblock 管理,进行一些初始化设置,对于设置了 no-map 属性的保留内存,memblock 会将这部分内存从 memblock.memory 中删除,就好像物理上并没有提供这片内存一样,后续的内存管理自然不会管理到这片内存。而对于指定位 CMA 类型的保留内存,和其他保留内存不一样的是,它还是会被移交给 buddy 子系统,只是将这片内存标记为 MIGRATE_CMA,表示这是 CMA 内存,只有用户在申请 MOVABLE 类型的内存时才能使用这部分内存,因为当特定设备需要使用这部分内存的时候需要把原本占用该内存的内容移出去,达到使用时再分配的目的。
- reusable(可选):保留内存中指定该属性表示这片内存可以被 buddy 子系统利用,CMA 就属于这种,也可以自定义其它的框架来实现保留内存的重复利用。对于一个保留内存节点,不能同时指定 no-map 和 reusable 属性。 如果 cma 节点指定了 linux,cma-default 属性,内核在分配 cma 内存时会将这片内存当成默认的 cma 分配池使用,执行内存申请时如果没有指定对应的 cma 就使用默认 cma pool。 如果用作 dma 的保留内存指定了 linux,dma-default 属性,内核在分配 dma 内存时将会默认使用该片内存作为 dma 分配池。
内存子系统的一些细节可以点击该链接去了解更多
预留内存访问
通常访问指定物理地址的内存方式有很多种:
- memremap / ioramp 方式将其映射
- phy_to_vir 线性映射虚拟地址
- mmap方式将物理地址映射到用户空间
通过实际测试, memblock_reserve保留的内存段,可以采用memremap ,phy_to_vir 方式将物理地址映射出并使用。
问题原因:
由于我们没有开启DMA使用CMA,导致一直在申请NORMAL内存的空间。导致内存碎片化严重,由于内存回收机制效率较低长时间后无法申请到有效的大内存作为显示。使用CMA作为显示内存和DMA使用,可以避免碎片化的问题有效解决了相关问题。
解决方法开启DMA使用CMA
在内核中有CMA的相关配置,可以打开CMA debug 运行程序能获取更多的相关信息。
DMA使用CMA的设置,不开启设置DMA将不能使用CMA内存。在dma_alloc_coherent 调用时使用或上__GFP_HIGH参数可以避免使用Normal 空间。 CMA 内存相关介绍
注意Normal 空间最大申请内存的大小通过这个参数控制。
Kernel Features
extern void *
dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle,
gfp_t flag);
extern void
dma_free_coherent(struct device *dev, size_t size, void *cpu_addr,
dma_addr_t dma_handle);
/*
* kernel module helper for testing CMA
*
* Licensed under GPLv2 or later.
*/
#include <linux/module.h>
#include <linux/device.h>
#include <linux/fs.h>
#include <linux/miscdevice.h>
#include <linux/dma-mapping.h>
#define CMA_NUM 10
static struct device *cma_dev;
static dma_addr_t dma_phys[CMA_NUM];
static void *dma_virt[CMA_NUM];
/* any read request will free coherent memory, eg.
* cat /dev/cma_test
*/
static ssize_t
cma_test_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
int i;
for (i = 0; i < CMA_NUM; i++) {
if (dma_virt[i]) {
dma_free_coherent(cma_dev, (i + 1) * SZ_1M, dma_virt[i], dma_phys[i]);
_dev_info(cma_dev, "free virt: %p phys: %p\n", dma_virt[i], (void *)dma_phys[i]);
dma_virt[i] = NULL;
break;
}
}
return 0;
}
/*
* any write request will alloc coherent memory, eg.
* echo 0 > /dev/cma_test
*/
static ssize_t
cma_test_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
int i;
int ret;
for (i = 0; i < CMA_NUM; i++) {
if (!dma_virt[i]) {
dma_virt[i] = dma_alloc_coherent(cma_dev, (i + 1) * SZ_1M, &dma_phys[i], GFP_KERNEL);
if (dma_virt[i]) {
void *p;
/* touch every page in the allocated memory */
for (p = dma_virt[i]; p < dma_virt[i] + (i + 1) * SZ_1M; p += PAGE_SIZE)
*(u32 *)p = 0;
_dev_info(cma_dev, "alloc virt: %p phys: %p\n", dma_virt[i], (void *)dma_phys[i]);
} else {
dev_err(cma_dev, "no mem in CMA area\n");
ret = -ENOMEM;
}
break;
}
}
return count;
}
static const struct file_operations cma_test_fops = {
.owner = THIS_MODULE,
.read = cma_test_read,
.write = cma_test_write,
};
static struct miscdevice cma_test_misc = {
.name = "cma_test",
.fops = &cma_test_fops,
};
static int __init cma_test_init(void)
{
int ret = 0;
ret = misc_register(&cma_test_misc);
if (unlikely(ret)) {
pr_err("failed to register cma test misc device!\n");
return ret;
}
cma_dev = cma_test_misc.this_device;
cma_dev->coherent_dma_mask = ~0;
_dev_info(cma_dev, "registered.\n");
return ret;
}
module_init(cma_test_init);
static void __exit cma_test_exit(void)
{
misc_deregister(&cma_test_misc);
}
module_exit(cma_test_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Barry Song <21cnbao@gmail.com>");
MODULE_DESCRIPTION("kernel module to help the test of CMA");
MODULE_ALIAS("CMA test");
# echo 0 > /dev/cma_test
# cat /dev/cma_test