当前位置: 首页 > article >正文

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寻址设备限制)有以下几种情况:

  1. 若是物理内存小于4G,那么所有的物理内存都将在DMA32 zone以下
  2. 若是物理内存大于4G,那么低于4G属于DMA32以下,剩下的是zone normal
  3. 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;
         }
   }


http://www.kler.cn/a/612439.html

相关文章:

  • 行业大数据实验报告 通过聚类算法实现睡眠健康群体的精准智能划分
  • 如何在 HTML 中使用<dialog>标签创建模态对话框,有哪些交互特性
  • Spring AOP:面向切面编程的探索之旅
  • PTA 1105-链表合并(C++)
  • SpringMVC实战——转发和重定向及实际场景
  • Dify实现自然语言生成SQL并执行
  • 高级数据结构01BST树
  • 如何使用VS中的Android Game Development Extension (AGDE) 来查看安卓 Logcat 日志
  • ‌JVM 内存模型(JDK8+)
  • 如何使用Python爬虫按关键字搜索1688商品?
  • 测谎仪策略思路
  • linux 安装open webui
  • 第二十章:类型属性的重载_《C++ Templates》notes
  • 【商城实战(80)】从0到1剖析:区块链如何重塑商城生态
  • Lisp语言的数据库交互
  • WPF 依赖项属性
  • 使用django的DRF业务逻辑应该放在序列化器类还是模型类
  • 前端空白/红幕报错 undefined
  • JavaScript性能优化实战手册:从V8引擎到React的毫秒级性能革命
  • <track>标签在<video>或<audio>元素中的作用,如何利用它实现字幕功能?