linux之 内存管理(5)-CMA 连续内存管理器
一、CMA机制出现的原因
在linux驱动开发过程中经常需要使用到连续大块物理内存,尤其是DMA设备。而实际在系统经过长时间的允许之后,物理内存会出现比较严重的碎片化现象,虽然通过内存规整,内存回收等手动可以清理出一段连续的物理内存,但是并不能保证一定能够申请较大连续物理块。最初连续申请较大块物理内存,一般都是只在DMA场景中使用,因此内核专门把物理内存划分出ZONE_DMA专门用于DMA内存申请(当然划分DMA_ZONE还有其他原因,在较早DMA中由于DMA寻址地址限制 只能将一定范围的物理内存),用于解决DMA申请连续物理内存问题。但是随着各种设备驱动出现,对连续物理内存需求也越来越大。因此将所有连续物理内存都做预留出来,很显然如果划分预留出较多内存专门给连续物理内存使用,对内存使用存在较为严重浪费情况,也支持长期发展需求。
为了解决上述问题,三星公司的Michal Nazarewicz 于2010年提出CMA(contiguous memory allocator)The Contiguous Memory Allocator [LWN.net],用于解决连续物理内存申请问题。
二、内核中 预留大块内存的方法
主要有三种方法:限制内存总空间方式预留内存、memblock和CMA。预留内存对于模块使用是比较方便,但是保留的内存内核是不管理也不可用的,完全由用户决定怎么使用,这样如果用户使用不充分会造成内存的浪费。memblock是内核启动时,使用的预留内存的方法,当内核启动完成就不能再用对应的接口了。最后CMA(Contiguous Memory Allocation)可以比较灵活的使用大块内存。本文尝试了一种在内核模块中使用CMA机制分配和使用大块内存的方法。
2.1预留内存方法
直接修改系统grub启动参数,将系统总内存限制在指定大小,使得其余内存不可见。然后对不可见内存进行申请保留。
[root@localhost ~]# cat /proc/cmdline
BOOT_IMAGE=/vmlinuz-3.10.0-123.el7.x86_64 root=UUID=79704805-e306-420b-827a-52849e1376c1 ro vconsole.keymap=us crashkernel=auto vconsole.font=latarcyrheb-sun16 rhgb quiet LANG=en_US.UTF-8 mem=2G
如上mem=2G ,是限制了可用物理内存为2G,那么我们可以对[2-4G]内的内存进行申请,映射和使用。
在iomem和meminfo里可以看到系统可用总内存已经减少2G左右。
2.1.1 预留内存访问
对预留部分内存访问的驱动代码如下,从0x40000000开始,申请512MB(0x20000000)内存。
static char data[] = "123456\n";
static void* addr;
static int __init ram_reserve_init(void)
{
if (!request_mem_region(0x40000000, 0x20000000, "reserve test")) //请求不可见的内存段权限(即:检查你申请的资源是否可用,如果可用,则将其标志为被使用。非必须)
{
printk("request_mem_region fail\n");
return - EBUSY;
}
addr = memremap(0x40000000, 0x20000000, MEMREMAP_WB); //映射内存空间
memcpy(addr, data, sizeof(str)); //拷贝测试数据
printk( "%s: %s\n " , __func__, ( char * )addr);//读出内存数据
return 0;
}
static void __exit ram_reserve_exit(void)
{
iounmap(addr);
release_mem_region(RESERVE_PHY, RESERVE_SIZE);
pr_info(KBUILD_MODNAME ": unloaded.\n");
}
module_init(ram_reserve_init);
module_exit(ram_reserve_exit);
request_mem_region() 检查你申请的资源是否可用,如果可用,则将其标志为被使用,如果该内存区已经被标记,会返回报错,该调用可以忽略,建议加上。加载驱动后,通过/proc/iomem 可以查看到reserve test段的内存分配信息
该方式有个缺点就是预留的内存是ram空间的尾部,边界不确定性,可能无法准确预留出指定大小的内存。
通过实际测试,不可见内存段保留方式,不能通过phy_to_vir 方式进行映射,因为phy_to_vir 只限在normal区的内存,不可见的内存段在系统认为不在normal区中,但可以采用memremap访问 。
2.2 memblock方式预留内存
2.2.1 memblock 内存管理
mmeblock是内存的一种管理机制,主要管理这两种内存:一种是系统可用部分的物理内存(usable),也就是/proc/meminfo里看到的总内存都是提供给系统使用的;另一种是用户预留部分的内存(reserved),用户自己特殊使用,这部分在系统总内存里看不到,本文主要讲解该部分内存怎么预留。
memblock 结构体维护着上述两种内存, 其中成员 memory 维护着可用物理内存区域;成员 reserved 维护预留的内存区域。
struct memblock {
bool bottom_up; /* is bottom up direction? */
phys_addr_t current_limit;
struct memblock_type memory; //维护着可用物理内存区域
struct memblock_type reserved; //维护着预留的内存区域
#ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP
struct memblock_type physmem;
#endif
};
cat /proc/iomem |grep 'System RAM’的 io内存分布里也能看到系统可用部分的物理内存的分布情况。4段System RAM加起来就是总的可用物理内存。
[root@localhost ~]# cat /proc/iomem |grep 'System RAM'
00001000-0009ebff : System RAM
00100000-bfecffff : System RAM
bff00000-bfffffff : System RAM
100000000-13fffffff : System RAM
2.2.2 memblock 方式预留内存方法
方法很简单,通过memblock_reserve() 将一段物理内存放在预留区里,在setup.c中setup_arch启函数中添加,内核补丁如下(基于:linux3.10.0-123.):
diff -uNrp a/arch/x86/kernel/setup.c b/arch/x86/kernel/setup.c
--- a/arch/x86/kernel/setup.c 2021-01-29 23:09:08.443072526 -0800
+++ b/arch/x86/kernel/setup.c 2021-01-29 23:31:53.521307672 -0800
@@ -907,6 +907,8 @@ static void rh_check_supported(void)
void __init setup_arch(char **cmdline_p)
{
+
+ struct memblock_region *reg;
memblock_reserve(__pa_symbol(_text),
(unsigned long)__bss_stop - (unsigned long)_text);
@@ -1035,6 +1037,13 @@ void __init setup_arch(char **cmdline_p)
* again from within noexec_setup() during parsing early parameters
* to honor the respective command line option.
*/
+
+ memblock_reserve(0x100000000, 0x2000000);
+ pr_info("Scan equal region:\n");
+ for_each_memblock(reserved, reg) //遍历打印reserved所有分区
+ pr_info("Region [%llx -- %llx]\n", (u64)(reg->base),
+ (u64)(reg->base + reg->size));
+
x86_configure_nx();
parse_early_param();
memblock_reserve(0x100000000, 0x2000000);函数完成了对ram起始地址0x100000000开始保留32MB(0x2000000)内存。reserved链表管理着预留区域的内存,memblock_reserve会将预留内存段插入链表新节点里(如果出现reserve之间地址相互覆盖的,reserver会将它们合并成一个内存区,即一个节点)。for_each_memblock()可以获取每个预留区内存信息并打印。
该方式是否成功预留出内存,可以在dmesg中查看打印,如果要查看memblock调试打印信息,可以在grub中加入:memblock=debug。
有一个实际的问题: 这块预留的内存是否显示在 /proc/iomem
2.2.3 预留内存访问
通常访问指定物理地址的内存方式有很多种,如:
memremap / ioramp 方式将其映射
phy_to_vir 线性映射虚拟地址
mmap方式将物理地址映射到用户空间
通过实际测试, memblock_reserve保留的内存段,可以采用memremap ,phy_to_vir 方式将物理地址映射出并使用。
三、CMA 预留大块内存
3.1 CMA 与内核内存分配接口的关系
要理解CMA内存分配和内核中常用的slab内存分配或以page的方式分配的关系,可以参考下面的图:
对上图的注释:
1、kmalloc 申请内存,源码中,有两个分支:一个是申请小于8K物理内存时,是调用SLAB的接口;一个是申请大于8K物理内存时,调用buddy 系统的接口 alloc_pages.
2、ION 是安卓系统中,对预留大块内存使用的框架,在内核中有补丁;
重点在这:内核中CMA机制是为设备DMA需要大块连续的内存设计的,所以在驱动模块中使用CMA并不是直接用到的,而是通过DMA API间接使用的。
在实际项目开发中,调用cma_alloc 函数接口申请内存,申请不成功;使用 dma_alloc_coherent 申请连续32M的物理内存是可以的。
下面展示一下,dma_alloc_coherent 调用到 cma 的调用栈:
通过call_trace 可以看出:dma_alloc_coherent() ->dma_alloc_attrs()->dma_direct_alloc()->__dma_direct_alloc_pages()->_alloc_pages()
dma 申请内存:
先介绍完,CMA预留内存的方法,在介绍CMA与DMA的关系。
3.2 预留CMA的三种方式
3.2.1 使用Kbuild的宏配置设置CMA区域
CONFIG_CMA_SIZE_MBYTES=32 // 这是内核Kbuild中的默认是 32M, 我在实际项目开发中,是修改32 为128,为驱动预留128M
3.2.2 启动参数设置CMA
cma=nn[MG]@[start[MG][-end[MG]]] [ARM,X86,KNL]
Sets the size of kernel global memory area for
contiguous memory allocations and optionally the
placement constraint by the physical address range of
memory allocations. A value of 0 disables CMA
altogether. For more information, see
include/linux/dma-contiguous.h
需要从内存物理地址5G开始预留1G的连续内存用于CMA,则可以在内核启动选项中添加如下参数:
cma=1G@5G
如果需要检查CMA是否预留成功,可以执行下面的命令:
# cat /proc/meminfo | grep Cma
CmaTotal: 1048576 kB
CmaFree: 1048576 kB
3.2.3 设备树中,配置CMA区域
dts中关于CMA区域的描述如下:
resmem: reserved-memory {
#address-cells = <2>;
#size-cells = <2>;
ranges;
linux,cma {
compatible = "shared-dma-pool";
reusable;
size = <0 0x3c000000>;
alloc-ranges = <0 0x40000000 0 0xC0000000>;
linux,cma-default;
};
}
在内核启动的dtb解析阶段,需要对这块CMA区域进行范围合法性和reusable属性检查【必须是reusable属性,不能是no-map,no-map用于专有驱动的io remap】
这三者如果同时使用,内核预留内核的优先级: dts > 启动参数 > kbuild 参数
注意:配置 reserved_memory 的区别,不是给CMA使用的mem
/ {
#address-cells = <1>;
#size-cells = <1>;
memory {
reg = <0x40000000 0x40000000>;
};
reserved-memory {
#address-cells = <1>;
#size-cells = <1>;
ranges;
display_reserved: framebuffer@78000000 {
reg = <0x78000000 0x800000>;
};
}
fb0: video@12300000 {
memory-region = <&display_reserved>;
};
}
四、CMA 区域 构建
4.1 通过 CONFIG_CMA_SIZE_MBYTES 和启动参数 cma 构建CMA区域
内核启动时,如何分析CONFIG_CMA_SIZE_MBYTES 和cma 参数?
注意:若是CONFIG_CMA_SIZE_MBYTES没有使能或者配置为0,那么此刻CMA是没有功能的,所以,这个宏算是一个CMA子系统的小开关。只不过,CMA还能通过cmdline动态配置CMA的参数来启动CMA,cmdline的优先级高于Kbuild,内核启动到某一时刻,会先判断cmdline,若是cmdline没有设置cma信息,才会根据Kbuild来构建CMA。
代码如下:
setup_arch
->bootmem_init
->dma_contiguous_reserve
->dma_contiguous_reserve_area
->cma_declare_contiguous
->cma_declare_contiguous_nid
这里解释一下arm64_dma_phys_limit 物理地址大小的设置:
如果设置了DMA32区域,那么dma32(32bit寻址设备限制)有以下几种情况:
- 若是物理内存小于4G,那么所有的物理内存都将在DMA32 zone以下
- 若是物理内存大于4G,那么低于4G属于DMA32以下,剩下的是zone normal
- CONFIG_CMA_ALIGNMENT=8
CMA的块管理的基本单位大小按照PAGE_SIZE^8对齐,也就是说,最小也必须有1MiB。
接着分析:dma_contiguous_reserve(arm64_dma_phys_limit);
先判断size_cmdline 不为 -1, 就使用 cma 参数,否则使用 size_bytes,就是CONFIG_CMA_SIZE_MBYTES 设置的大小。
问题:size_bytes 大小是多少?
答案:32M,代码如下:
1、CONFIG_CMA_SIZE_SEL_MBYTES=y
表征CMA内存块的粒度单位为MBytes
2、CONFIG_CMA_SIZE_SEL_PERCENTAGE
按照可用物理内存百分比进行分配,比如我们设定CONFIG_CMA_SIZE_PERCENTAGE = 20,那么表示将可用物理内存的百分之20做CMA。
3、CONFIG_CMA_SIZE_SEL_MIN
对比CONFIG_CMA_SIZE_MBYTES与CONFIG_CMA_SIZE_SEL_PERCENTAGE的内存大小,选择最小者作为CMA内存块大小
4、CONFIG_CMA_SIZE_SEL_MAX
对比CONFIG_CMA_SIZE_MBYTES与CONFIG_CMA_SIZE_SEL_PERCENTAGE的内存大小,选择最大者作为CMA内存块大小
接着分析:dma_contiguous_reserve_area
"reserved" 是 /proc/iomem 显示的 reserved 内存。
接着分析:cma_declare_contiguous()-> cma_declare_contiguous_nid()
接着分析:cma_init_reserved_mem()
cma_areas 的定义:
CONFIG_CMA_AREAS 是设置 cma_areas 数组的大小;系统默认值是19 ,我们自己可以设置。
上面只是构建CMA区域,加入buddy 系统,第五节再分析。
4.2 dts 构建CMA区域
resmem: reserved-memory {
#address-cells = <2>;
#size-cells = <2>;
ranges;
linux,cma {
compatible = "shared-dma-pool";
reusable;
size = <0 0x3c000000>;
alloc-ranges = <0 0x40000000 0 0xC0000000>;
linux,cma-default;
};
}
在dtb
解析过程中,会调用到rmem_cma_setup
函数:
从搜索中,发现 rmem_dma_setup 函数也会调用
后面测试一下,rmem_dma_setup函数会不会被调用。
RESERVEDMEM_OF_DECLARE(cma, "shared-dma-pool", rmem_cma_setup);
五、CMA添加到buddy 系统
在创建完CMA区域后,该内存区域成了保留区域,如果单纯给驱动使用,显然会造成内存的浪费,因此内存管理模块会将CMA区域添加到Buddy System中,用于可移动页面的分配和管理。CMA区域是通过cma_init_reserved_areas接口来添加到Buddy System中的。
// mm/cma.c
core_initcall(cma_init_reserved_areas);
core_initcall
宏将cma_init_reserved_areas
函数放置到特定的段中,在系统启动的时候会调用到该函数。
5.2 CMA 的分配和释放
源码路径: mm/cma.c cma_alloc() 函数
源码路径: kernel/dma/contiguous.c dma_alloc_contiguous()函数
dma_alloc_contiguous()函数会调用 cma_alloc();
前面展示的call_trace :dma_alloc_coherent() ->dma_alloc_attrs()->dma_direct_alloc()->__dma_direct_alloc_pages()->_alloc_pages() ; 调用栈打通了。
内核中,还有其他模块调用 cam_alloc() 申请内存:巨页表
CMA分配,入口函数为cma_alloc
:
CMA释放,入口函数为cma_release
:函数比较简单,直接贴上代码:
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;
}
六、CMA原理
6.1 DMA申请物理内存的各个分支
dma_alloc_attrs 代码解析:
默认情况下,在大多数情况下,内核使用CMA作为DMA缓冲区分配的后端。最右侧的黄色部分,需要开机前就要配置好。
6.2 cma 的数据结构
内核定义了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
值;
来一张图就会清晰明了:
七、CMA 利弊
7.1.优点
- 精心设计,即使在内存碎片情况下也可用于大型连续内存分配。
- CMA中的页面可以由伙伴系统共享,而不是保留池共享
- 可以是特定于设备的CMA区域,仅由该设备使用并与系统共享
- 无需重新编译内核即可轻松配置它的启动地址和大小
7.2.缺点
- 需要迁移页面时分配过程变慢
- 容易被系统内存分配破坏。当系统内存不足时,客户可能会遇到cma_alloc故障,这会在前台应用程序需要图形缓冲区进行渲染而RVC希望缓冲区用于CAR反向时导致不良的用户体验。
- 当cma_alloc()需要迁移某些页面时仍可能出现死锁,而这些页面仍在刷新到存储中(当FUSE文件系统在回写路径中有一页时,某些客户已经遇到了死锁,而cma_alloc希望迁移它)。
7.3 为什么要摆脱CMA
关键是为DMA内存关键的分配路径(例如GPU图形缓冲区和摄像机/ VPU预览/记录缓冲区)保留内存,以防止分配失败而导致良好的用户体验,分配失败会导致黑屏,预览卡死等
八、如何摆脱CMA
要摆脱CMA,基本思想是在DMA分配中切断CMA方式,转向相干池(原子池)。请注意,连贯池只能由DMA分配API使用,它不会与系统伙伴共享。
8.1 启用连贯池
在命令行中添加“ coherent_pool = <size>”,Coherent池实际上是从系统默认CMA分配的,因此CMA size> coherent_pool。
此大小没有参考,因为从系统到系统以及用例到用例各有不同:
- DMA的最大消耗者是GPU,其使用情况可以通过gmem_info工具进行监控。在典型的用例下监视gmem_info,并确定GPU所需的内存。
- 检查DMA的第二个使用者:ISI /相机,取决于V4l2 reqbuf的大小和数量
- 检查VPU,取决于多媒体框架
- 加上alsa snd,USB,fec使用
必须通过测试验证大小,以确保系统稳定。
8.2 DMA分配技巧
修改至arch / arm64 / mm / dma-mapping.c,在__dma_alloc()函数中删除gfpflags_allow_blocking检查:
diff --git a/arch/arm64/mm/dma-mapping.c b/arch/arm64/mm/dma-mapping.c
index 7015d3e..ef30b46 100644
--- a/arch/arm64/mm/dma-mapping.c
+++ b/arch/arm64/mm/dma-mapping.c
@@ -147,7 +147,7 @@ static void *__dma_alloc(struct device *dev, size_t size,
size = PAGE_ALIGN(size);
- if (!coherent && !gfpflags_allow_blocking(flags)) {
+ if (!coherent) { // && !gfpflags_allow_blocking(flags)) {
struct page *page = NULL;
8.3 离子分配器(没有使用过,参考网友的分享)
在Android和Yocto版本中,ION分配器(Android临时驱动程序)都用于VPU缓冲区。它默认进入ION CMA堆。这意味着ION对连续内存的请求直接发送给CMA。为了避免CMA,我们可以在ION中使用分割堆栈而不是CMA堆栈:
安卓:
启用CARVEOUT堆,禁用CMA堆:
CONFIG_ION = y
CONFIG_ION_SYSTEM_HEAP = y
-CONFIG_ION_CMA_HEAP = y
+ CONFIG_ION_CARVEOUT_HEAP = y
+ CONFIG_ION_CMA_HEAP = n
在dts中调整分割保留的堆基地址和大小:
/ {
reserved-memory {
#address-cells = <2>;
#size-cells = <2>;
ranges;
carveout_region: imx_ion@0 {
compatible = "imx-ion-pool";
reg = <0x0 0xf8000000 0 0x8000000>;
};
};
};
linux :
内核-请参阅i.MX8QM的随附补丁。与Linux几乎相同,但需要对ION分离堆驱动程序进行修补。
Gstreamer-应用以下补丁从分配中分配:
yocto / build-8qm / tmp / work / aarch64-mx8-poky-linux / gstreamer1.0-plugins-base / 1.14.4.imx-r0 / git:
diff --git a/gst-libs/gst/allocators/gstionmemory.c b/gst-libs/gst/allocators/gstionmemory.c
index 1218c4a..12e403d 100644
--- a/gst-libs/gst/allocators/gstionmemory.c
+++ b/gst-libs/gst/allocators/gstionmemory.c
@@ -227,7 +227,8 @@ gst_ion_alloc_alloc (GstAllocator * allocator, gsize size,
}
for (gint i=0; i<heapCnt; i++) {
- if (ihd[i].type == ION_HEAP_TYPE_DMA) {
+ if (ihd[i].type == ION_HEAP_TYPE_DMA ||
+ ihd[i].type == ION_HEAP_TYPE_CARVEOUT) {
heap_mask |= 1 << ihd[i].heap_id;
}
}