2023-06-11
原文作者:奇小葩 原文地址:https://blog.csdn.net/u012489236/category_9614673.html

在我们使用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内存分配技术?有机顶盒有什么作用

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];

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的服务。

5. 参考文档

http://www.wowotech.net/memory_management/cma.html
https://www.lizenghai.com/archives/44215.html

阅读全文