Linux深入理解内存管理11

Linux深入理解内存管理11(基于Linux6.6)---CMA介绍

一、概述

在使用ARM等嵌入式Linux系统的时候,一个头疼的问题是GPU,Camera,HDMI等都需要预留大量连续内存,对于内核如果申请一块连续的内存空间该怎么处理呢?

  • 首先向到的是利用内核提供的kmalloc申请,尽管kmalloc可以申请连续的内存空间,但是在长时间的测试中,会出现内存空间可能申请失败的情况,无法保证能成功分配。
  • 使用memblock分配器中提供的方法,称为预留内存,但这么预留的内存只能被特定的Device驱动所使用,System不能分配这部分内容,会导致内存浪费。

1. CMA 的背景和需求

在很多情况下,设备驱动程序或硬件设备(如图形处理单元 GPU、视频编码解码器等)需要使用连续的物理内存区域,这样它们才能直接访问这段内存进行高效的数据传输和操作。例如:

  • DMA 操作:很多硬件(如网络接口卡、存储控制器等)通过 DMA 操作与主机内存直接交互,要求内存必须是连续的物理块。
  • 内存映射 I/O(MMIO):一些设备可能需要将一个大的内存区域映射到设备中,这时设备也需要连续的物理内存。
  • 高性能内存操作:如视频流的解码和图形渲染等操作,常常要求内存区域连续以减少访问延迟和提高带宽。

普通的内存分配机制(如伙伴系统)虽然能够高效地分配物理内存,但它分配的内存块并不一定是连续的。而在某些硬件的使用场景下,连续的物理内存是非常重要的。因此,CMA 提供了一种专门的机制来为这些设备和驱动程序提供连续的物理内存区域。

2. CMA 的工作原理

CMA 的基本思想是在系统启动时预留一块区域作为连续内存池,然后通过特殊的内存分配策略,从该内存池中分配连续物理内存。它的实现主要依赖于以下几个关键点:

  • 内存池预留:CMA 会在系统启动时预留一块连续的内存区域。这块内存区域可以在内核配置时指定大小,通常在内存大小较大的系统上,CMA 会预留较大一块区域。
  • 动态内存分配:通过内存池,CMA 允许在运行时动态地分配和释放连续的物理内存区域。这个内存池可以随时被设备驱动请求分配连续内存。
  • 内存对齐:CMA 确保所分配的内存是按照硬件的要求对齐的,这对于某些硬件设备来说是非常重要的。
  • 内存释放:与普通的内存分配方式不同,CMA 需要确保内存分配后不会破坏连续性,因此它的释放方式也有所不同。

3. CMA 的配置与启用

CMA 的启用通常需要在内核配置时进行指定。你可以通过内核配置选项启用 CMA,并设置预留的连续内存池大小。以下是一些相关的配置项:

  • CONFIG_CMA:启用 CMA 支持。
  • CONFIG_CMA_SIZE_MBYTES:指定 CMA 区域的大小(以MB为单位)。

例如,内核启动时可以通过命令行参数设置 CMA 区域的大小:

cma=256M

这将为 CMA 指定 256MB 的连续内存区域。

4. CMA 的 API 接口

CMA 提供了一些接口供设备驱动请求连续内存:

  • dma_alloc_coherent():这是一个常见的 API,用于分配连续的物理内存区域。虽然这个函数通常用于 DMA 操作,但在使用 CMA 时,它会从 CMA 管理的内存池中分配内存。

  • cma_alloc():这是 CMA 内部提供的分配接口,用于从 CMA 管理的内存区域中申请连续的物理内存。

  • cma_release():释放之前通过 CMA 分配的内存,确保内存能够回到 CMA 内存池中。

  • cma_declare_area():用于声明一个 CMA 内存区域,这通常在内核启动时进行。

5. CMA 与 DMA 的关系

DMA(直接内存访问)是硬件和内存之间的一种高效数据传输方式,要求内存区域必须是物理连续的。CMA 是为了解决 DMA 操作中物理内存连续性问题而设计的。

当一个 DMA 设备需要分配连续内存时,它可以通过 CMA 接口从内核的 CMA 区域中请求内存。CMA 确保所分配的内存区域在物理上是连续的,这使得 DMA 操作可以顺利进行。

6. CMA 的优点

  • 提高性能:连续的物理内存分配对于许多硬件设备的性能至关重要,CMA 提供了高效的内存管理机制,避免了内存碎片和不连续问题。
  • 避免内存碎片:CMA 通过内存池的方式来集中管理连续内存,避免了普通内存管理方式下的内存碎片问题。
  • 简化设备驱动开发:CMA 提供了统一的 API,使得设备驱动不需要关注底层内存分配的细节,可以专注于实现硬件功能。

7. CMA 的局限性

  • 内存池限制:CMA 使用预留内存池来提供连续内存,这个池的大小是有限的。如果系统内存池过小,或者分配请求过多,可能会出现内存不足的情况。
  • 内存碎片问题:虽然 CMA 能够有效减少内存碎片,但如果系统长期运行,内存池中可能仍然会出现碎片,影响内存分配的效率。
  • 对硬件要求高:某些硬件可能要求更高的内存连续性要求,这时候 CMA 的效果可能受到限制。

8. CMA 的实际应用场景

CMA 主要应用于以下几种场景:

  • 视频处理:如视频编码、解码和显示等操作需要大量连续内存来存储帧缓冲区。
  • 图形渲染:GPU 等图形硬件通常需要连续内存来存储渲染数据。
  • 网络数据传输:某些网络设备需要连续内存来存储传输缓冲区。
  • 存储设备:某些存储设备(如 SSD 控制器)可能需要连续内存用于存储缓存或映射。

二、CMA简介

2.1、什么是CMA内存分配技术

CMA,Contiguous Memory Allocator,是一种用于申请大量的,并且物理上连续的内存块的方法。是linux kernel内存管理系统的扩展功能,目的在于解决需要预留大量连续内存导致运行内存紧张的问题。连续内存分配器(CMA - Contiguous Memory Allocator)是一个框架,允许建立一个平台无关的配置,用于连续内存的管理。然后,设备所需内存都根据该配置进行分配。

在 Linux 内核中,物理内存的管理通常是通过“页面”(page)来进行的,这意味着内存可以不连续地分布在不同的物理地址上。然而,一些硬件设备,尤其是需要使用 DMA(Direct Memory Access,直接内存访问)的设备,要求其内存缓冲区必须是连续的物理内存,否则它们就无法高效地进行数据传输和处理。

例如,某些图形处理单元(GPU)、网络设备、视频编码解码器等,要求内存区域是物理连续的。这些设备无法处理碎片化的内存,只能在物理上相邻的内存区域进行数据交换。

一般系统在启动过程中,从整个Memory中配置一段连续内存用于CMA,然后内核其他的模块就可以通过CMA的接口API进行连续的内存分配。其功能主要包括

  • 解析DTS或者命令行参数,确定CMA内存的区域,也就是定义的CMA eara
  • 提供cma_alloc和cma_release两个接口函数用于分配和释放CMA
  • 记录和跟踪CMA area的各个page状态
  • 调用伙伴系统接口,进行真正的内存分配
  • CMA的初始化必须在buddy系统工作之前和memblock分配器初始化完成之后

2.2、为什么需要CMA技术

1. 硬件对连续内存的需求

一些硬件设备,特别是涉及直接内存访问(DMA)的设备,需要将数据从内存直接传输到设备,或者反之,传输过程中不希望经过 CPU。为了确保这种高效的数据交换,DMA 设备要求内存区域在物理上是连续的。如果内存碎片化,设备可能无法访问需要的连续内存区域,导致数据传输失败或性能下降。

  • GPU:图形处理单元(GPU)需要连续内存来存储图形数据,进行图像渲染和处理。
  • 视频编码解码器:视频解码器或编码器需要连续内存来存储视频帧数据。
  • 网卡:一些网络设备使用 DMA 直接将网络数据包存储在连续内存中。
  • 存储控制器:存储设备(如 SSD 控制器)通常也需要连续的内存来处理数据缓冲。

这些硬件设备通过 DMA 操作能够直接访问内存中的数据,而 DMA 的高效执行往往依赖于物理内存的连续性。

2. 内存碎片化问题

Linux 内核通常使用页面(page)作为最小的内存分配单位。内存会被分割成若干页,而这些页面可以是非连续的。这意味着,尽管系统中可能有足够的空闲内存,但由于内存的碎片化,无法提供足够大的连续物理内存块

  • 碎片化:随着系统的运行,内存不断地被分配和释放,导致内存中可能有许多小的空闲块,但这些块可能无法满足硬件设备需要的连续内存块。传统的内存分配方式(如伙伴系统)虽然有效,但无法保证内存分配时内存的连续性。
  • 性能问题:对于需要连续内存的设备,如果无法分配到足够大的连续内存块,设备的 DMA 操作可能会失败,导致性能下降,甚至系统崩溃。

CMA 技术通过集中管理一块物理内存区域,专门提供连续内存,减少了内存碎片化带来的问题。

3. 避免内存碎片的累积

长期运行的系统中,内存碎片化会逐渐加剧,尤其是在设备频繁请求和释放内存时。传统的内存分配器(如伙伴系统)在处理连续内存分配时,可能无法有效地解决这一问题。CMA 通过提供一个单独的内存池,在内核启动时就为连续内存分配预留出一定的空间,避免了长时间运行中产生的内存碎片化问题。

4. 简化设备驱动开发

没有 CMA 的情况下,设备驱动程序通常需要自己管理内存的连续性。例如,驱动程序需要手动在内存中查找足够大的连续区域,或者使用一些复杂的技术来避免内存碎片。这不仅增加了开发复杂性,也可能导致程序错误。

使用 CMA 技术,设备驱动程序可以通过简单的 API 请求连续内存,而不需要关注内存是否连续。CMA 内核模块会自动从预留的内存池中为设备分配连续内存,简化了驱动开发流程。

5. 内存共享和内存映射

对于需要频繁进行内存映射或共享的设备(例如在图形处理或网络设备中),将内存映射为连续的物理内存块可以提高数据交换的效率。如果内存是碎片化的,数据传输的效率会受到很大影响。

CMA 提供了一种机制,确保设备在需要时能够获得合适大小的连续内存块,从而提高内存映射和共享内存的效率。

6. 动态分配和释放

CMA 不仅可以在系统启动时为连续内存区域分配内存,还允许动态的内存分配和释放。通过这种方式,设备可以根据需要动态请求和释放连续内存,进一步提高内存利用率。动态分配减少了内存资源的浪费,并确保在硬件设备不再需要时释放内存。

7. 满足特定硬件的对齐要求

许多硬件设备对内存的对齐有严格的要求。CMA 能够确保所分配的内存符合设备对对齐的要求,避免因对齐错误而导致硬件无法正常工作或出现性能瓶颈。

2.3、设计思路

2.3.1、CMA 的设计目标

  1. 提供物理连续内存:许多硬件设备(如 GPU、视频解码器、网卡等)要求访问的内存必须是连续的,即在物理内存地址空间中没有中断。这是因为 DMA(直接内存访问)操作通常需要连续的内存块。

  2. 减少内存碎片化:内存碎片化会导致无法为硬件设备分配大块连续内存,影响系统的稳定性和性能。CMA 旨在集中管理内存池,减少碎片化问题。

  3. 支持动态内存分配:CMA 必须支持内存的动态分配和释放,以应对设备在运行过程中不断变化的内存需求。

  4. 高效内存利用:CMA 通过提供一个大范围的内存池来提高内存的利用率,避免内存资源的浪费,特别是在长时间运行或资源紧张的系统中。

2.3.2、CMA 设计的关键思路

  1. 内存池的划分: CMA 通过预留一块专用的内存池(称为“CMA 区域”)来存放需要连续内存的区域。这块内存池在内核启动时就被划定,并且是供设备使用的。这一设计避免了内存碎片化,使得当硬件设备请求连续内存时,系统可以从这个池中快速分配。

  2. 内存区域管理: CMA 管理的内存区域是由多个内存页组成的,并且它们是物理连续的。内存池本身在物理上是一个连续的大区域,但在逻辑上,CMA 会将这块区域分割成多个小的块,这些小块可以根据需求进行动态分配。

    在 CMA 中,每个分配请求会从这个内存池中查找一个足够大的连续内存块来满足请求。如果当前内存池中没有足够的空间,CMA 可以选择从系统的其他区域进行“紧急回收”或者将内存页合并(通过内核的其他内存管理机制)。

  3. 请求和释放接口: 为了使设备能够高效地请求连续内存,CMA 提供了简单的 API 接口。设备驱动通过这些接口请求一定大小的连续内存,并在使用完毕后释放。内存的释放并不立即回到 CMA 区域,而是等待一个合适的时机,这样可以减少内存池的频繁碎片化。

    • cma_alloc():请求分配一块连续的内存区域。
    • cma_free():释放先前分配的连续内存区域。
    • cma_declare_area():声明内存池的范围,用于启动时设置。
  4. 内存回收机制: CMA 的内存池并非一成不变,内存的分配和释放可能会导致内存池的碎片化。为了解决这一问题,CMA 通过定期的内存回收机制来尽量合并碎片,并保持内存池的连贯性。系统可以根据内存需求的变化动态扩展或收缩 CMA 区域。

    同时,CMA 支持延迟回收(delayed reclamation),即内存页在被释放后,并不会立即被系统回收,而是等待下一次合适的内存请求。这样,内存池中的碎片可以被合并到一起,确保未来分配时能够满足需求。

  5. 兼容性与灵活性: CMA 的设计考虑了不同硬件平台和内存需求的差异。为了最大程度地减少系统碎片化,CMA 的内存池是通过内核启动时的配置进行划定的,并且可以被动态调整。这意味着,在不同的硬件架构上(如 ARM、x86、MIPS 等),CMA 的工作方式可以根据内存布局和需求进行定制。

  6. 内存对齐和页大小: 在进行内存分配时,CMA 会确保内存地址对齐到硬件要求的粒度。通常 DMA 操作要求内存页的对齐要求比常规内存分配更严格,因此 CMA 会在内存分配时确保对齐方式符合设备的要求。

    此外,CMA 的内存池通常是以页面(page)为单位来进行管理的,这样能够与内核的内存管理机制保持一致,并在需要时进行大块内存的切割和合并。

  7. 预留内存区域: 一些内核中必须保证不被其他部分占用的内存区域(如早期初始化阶段的内存,或者一些特殊硬件的内存区域)会被预留给 CMA 使用,这样可以避免在这些内存区域上发生内存碎片化。CMA 会提前声明这些区域并进行管理。

  8. 动态扩展: 在一些特殊场景下,CMA 还支持动态扩展内存池的功能。也就是说,CMA 可以在运行时根据硬件的需求动态地从其他空闲区域分配内存,保证系统在内存压力较大的情况下也能够继续提供连续的内存块。

2.3.3、CMA 工作流程示例

  1. 设备请求内存: 当一个硬件设备需要连续的内存时,它会调用 CMA 提供的 API(如 cma_alloc())请求内存。CMA 会检查是否能够从内存池中分配出足够大小的连续内存块。

  2. 内存分配: 如果内存池中存在满足要求的连续内存,CMA 就会将这块内存分配给设备。如果没有足够的连续内存,CMA 可能会启动内存回收机制,合并碎片,或者将请求转发到其他内存区域。

  3. 设备使用内存: 设备获得连续内存后,可以进行 DMA 操作、数据传输等操作,确保性能和稳定性。

  4. 设备释放内存: 使用完毕后,设备会通过 cma_free() 等接口将连续内存释放回 CMA 内存池。

  5. 内存回收: CMA 会定期进行内存回收,合并碎片,确保内存池中的内存可以最大程度地被重用。

因此

CMA主要设计目的是提供一个以下功能:

可以分配连续的大的内存空间

  • 防止reseve方式的内存浪费。
  • (1) 支持Migration功能,所以即使是被某个驱动设备Reserve的区域,在驱动没有使用的时候System可以对该段内存进行分配使用。
  • (2) 在System使用这段内存的时候,如果驱动要求分配这个预留的内存,System memory就会被Migration到其他内存区域,之后这段内存被分配给驱动设备。
  • 驱动设备间的内存共享(通过CMA被Reserve的内存会通过CMA进行管理,所以可以驱动设备间共享该段,比如FIMC可以共享MFC预留的内存区域等)。

三、数据结构

内核定义了struct cma结构,用于管理一个CMA区域,此外还定义了全局的cma数组,如下:

mm/cma.h

struct cma {
	unsigned long   base_pfn;
	unsigned long   count;
	unsigned long   *bitmap;
	unsigned int order_per_bit; /* Order of pages represented by one bit */
	spinlock_t	lock;
#ifdef CONFIG_CMA_DEBUGFS
	struct hlist_head mem_head;
	spinlock_t mem_head_lock;
	struct debugfs_u32_array dfs_bitmap;
#endif
	char name[CMA_MAX_NAME];
#ifdef CONFIG_CMA_SYSFS
	/* the number of CMA page successful allocations */
	atomic64_t nr_pages_succeeded;
	/* the number of CMA page allocation failures */
	atomic64_t nr_pages_failed;
	/* kobject requires dynamic object */
	struct cma_kobject *cma_kobj;
#endif
	bool reserve_pages_on_error;
};
  • 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 )个CAM erea,代码如下:

mm/cma.h

/*
 * There is always at least global CMA area and a few optional
 * areas configured in kernel .config.
 */
#ifdef CONFIG_CMA_AREAS
#define MAX_CMA_AREAS	(1 + CONFIG_CMA_AREAS)
#endif
extern struct cma cma_areas[MAX_CMA_AREAS];

四、流程分析

4.1、CMA区域创建

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的:

kernel/dma/contiguous.c

static int __init rmem_cma_setup(struct reserved_mem *rmem)
{
	unsigned long node = rmem->fdt_node;
	bool default_cma = of_get_flat_dt_prop(node, "linux,cma-default", NULL);
	struct cma *cma;
	int err;

	if (size_cmdline != -1 && default_cma) {
		pr_info("Reserved memory: bypass %s node, using cmdline CMA params instead\n",
			rmem->name);
		return -EBUSY;
	}

	if (!of_get_flat_dt_prop(node, "reusable", NULL) ||---解析1
	    of_get_flat_dt_prop(node, "no-map", NULL))
		return -EINVAL;

	if (!IS_ALIGNED(rmem->base | rmem->size, CMA_MIN_ALIGNMENT_BYTES)) {
		pr_err("Reserved memory: incorrect alignment of CMA region\n");
		return -EINVAL;
	}

	err = cma_init_reserved_mem(rmem->base, rmem->size, 0, rmem->name, &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 (default_cma)---解析3
		dma_contiguous_default_area = 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;
}
RESERVEDMEM_OF_DECLARE(cma, "shared-dma-pool", rmem_cma_setup);

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区域。

2. 据参数或宏配置

可以通过内核参数或配置宏,来进行CMA区域的创建,例如cma=64M,在初始化过程中,内核会解析这些命令行参数,获取CMA area的位置(起始地址,大小),并调用cma_declare_contiguous接口函数向CMA模块进行注册(当然,和device tree传参类似,最终也是调用cma_init_reserved_mem接口函数)。除了命令行参数,通过内核配置(CMA_SIZE_MBYTES和CMA_SIZE_PERCENTAGE)也可以确定CMA area的参数:

kernel/dma/contiguous.c

static int __init early_cma(char *p)
{
	if (!p) {
		pr_err("Config string not provided\n");
		return -EINVAL;
	}

	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数组中获取一个实例,初始化操作。

4.2、CMA初始化

在创建完CMA区域后,该内存区域成了保留区域图,如果单纯的给驱动使用,显然会超成内存的浪费,因此内存模块会将该CMA区域添加到Buddy System中,可用于页面的分配和管理。

内存管理子系统进行初始化的时候,首先是通过memblock掌握全局的,它确定了整个系统的内存布局。哪些是memory是Memory type,而哪些memory block是reserved type,memblock分配器中有相关的介绍。memblock始终是初始化阶段的内存管理模块,最终我们还是要转向伙伴系统。free memory被释放到伙伴系统中,而reserved memory不会进入伙伴系统,对于CMA area,之前说过,最终被由伙伴系统管理,因此,在初始化的过程中,CMA area的内存会全部导入伙伴系统(方便其他应用可以通过伙伴系统分配内存)。
mm/cma.c

static int __init cma_init_reserved_areas(void)
{
	int i;

	for (i = 0; i < cma_area_count; i++)
		cma_activate_area(&cma_areas[i]);

	return 0;
}
core_initcall(cma_init_reserved_areas);

其主要是通过遍历cma_ereas的CMA管理区信息,调用cma_activate_area将各个区进行初始化:

mm/cma.c

static void __init cma_activate_area(struct cma *cma)
{
	unsigned long base_pfn = cma->base_pfn, pfn;
	struct zone *zone;

	cma->bitmap = bitmap_zalloc(cma_bitmap_maxno(cma), GFP_KERNEL);---解析1
	if (!cma->bitmap)
		goto out_error;

	/*
	 * alloc_contig_range() requires the pfn range specified to be in the
	 * same zone. Simplify by forcing the entire CMA resv range to be in the
	 * same zone.
	 */
	WARN_ON_ONCE(!pfn_valid(base_pfn));
	zone = page_zone(pfn_to_page(base_pfn));
	for (pfn = base_pfn + 1; pfn < base_pfn + cma->count; pfn++) {
		WARN_ON_ONCE(!pfn_valid(pfn));
		if (page_zone(pfn_to_page(pfn)) != zone)
			goto not_in_zone;
	}

	for (pfn = base_pfn; pfn < base_pfn + cma->count;---解析2
	     pfn += pageblock_nr_pages)
		init_cma_reserved_pageblock(pfn_to_page(pfn));---解析3

	spin_lock_init(&cma->lock);

#ifdef CONFIG_CMA_DEBUGFS
	INIT_HLIST_HEAD(&cma->mem_head);
	spin_lock_init(&cma->mem_head_lock);
#endif

	return;

not_in_zone:
	bitmap_free(cma->bitmap);
out_error:
	/* Expose all pages to the buddy, they are useless for CMA. */
	if (!cma->reserve_pages_on_error) {
		for (pfn = base_pfn; pfn < base_pfn + cma->count; pfn++)
			free_reserved_page(pfn_to_page(pfn));
	}
	totalcma_pages -= cma->count;
	cma->count = 0;
	pr_err("CMA area %s could not be activated\n", cma->name);
	return;
}
  1. CMA eara有一个bitmap来管理各个page的状态,这里的bitmap_size给出了Bitmap需要多少内存,i变量表示该CMA eara有多少个pageblock
  2. 确保CMA area中的所有page都是在一个memory zone内,同时累加了pfn,从而得到下一个pageblock的初始page frame number
  3. 最终调用init_cma_reserved_pageblock,以pageblock为单位进行处理,设置migrate type为MIGRATE_CMA;将页面添加到伙伴系统中;调正zone管理的页面总数

4.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的做法。现存的体系大都已经有了特殊的路径来解决原子分配。

五、总结

CMA其主要的功能是,当驱动没有分配使用的时候,这些memory可以被内核的其他模块使用;而当驱动分配CMA内存后,那些被其他模块使用的内存需要释放出来,形成物理地址连续的大块内存。对于CMA的接口层,驱动程序并不会调用CMA的模块接口,而是通过DMA framework层来使用CMA的服务。

CMA(Contiguous Memory Allocator)是Linux内核中处理物理连续内存分配的一种机制,旨在为那些需要连续物理内存的硬件设备(如通过DMA进行直接内存访问的设备)提供一个统一、高效的内存管理方案。CMA的设计核心在于:避免内存碎片化,使硬件设备能够在内存受限的情况下高效地申请并使用连续内存。

在Linux内核中,CMA并不仅仅是一个简单的内存分配器,它结合了内存区域的管理、碎片回收、设备需求响应等复杂的操作流程。下面我们将详细探讨CMA的整个处理流程。

1. CMA内存池的定义与初始化

CMA内存池是系统中一个专门用于连续内存分配的区域,它通常由内核在启动阶段进行初始化。CMA内存池的大小是由内核配置决定的,可以通过修改内核配置或者启动参数来调整。CMA内存池的内存区域会被声明为物理连续的,这一内存池会被分配给需要连续内存的硬件设备使用。

CMA内存区域的定义通常通过内核的cma_declare_area()函数来实现,通常在arch/arm64/kernel/setup.c或者arch/x86/kernel/setup.c等平台相关代码中会做这样的初始化。

cma_declare_area("cma", &cma_area, CMA_SIZE);

在这里,CMA_SIZE是CMA区域的大小,通常这块内存是被内核“预留”的,供设备通过CMA分配连续内存。

2. 设备请求内存

当一个硬件设备(如GPU、网卡、音视频编解码器等)需要进行DMA操作时,它通常需要连续的物理内存块。为了满足这种需求,设备驱动程序会调用cma_alloc()函数来请求CMA分配一块连续的内存。

cma_alloc()函数

cma_alloc()是CMA提供的内存分配接口,它会向CMA内存池请求内存。当调用cma_alloc()时,CMA会:

  • 检查当前CMA内存池中是否有足够大的连续内存块来满足请求;
  • 如果有足够的内存块,直接分配给设备;
  • 如果没有足够的内存,CMA会尝试通过合并碎片、回收内存等手段来提供请求的内存。

cma_alloc()的流程

  1. 内存池检查:检查CMA内存池中是否有足够大的连续内存块。如果有,直接分配。
  2. 内存回收:如果没有足够的内存,CMA会触发内存回收机制(比如合并空闲块、紧急回收等)。
  3. 分配内存:如果回收机制成功找到合适的内存块,CMA会将这块内存分配给设备。
  4. 返回分配结果:成功分配内存后,返回物理内存地址供设备使用。

3. 设备内存的使用

一旦设备通过cma_alloc()获取到连续内存,它可以使用这块内存进行DMA操作,进行数据传输等任务。CMA提供的内存是物理连续的,这对于大多数设备的DMA操作是至关重要的。

4. 设备释放内存

当设备使用完这块连续内存后,它需要将内存释放回CMA内存池。设备驱动程序会调用cma_free()函数释放内存。

cma_free()函数

cma_free()函数的作用是将之前分配的内存返回给CMA内存池。需要注意的是,内存的释放并不意味着会立即归还内存池,而是根据内存回收策略进行延迟回收。CMA通过内存的延迟回收来避免内存池的频繁碎片化。

cma_free(cma_area, addr, size);

此时,内存释放后,CMA会尝试将这块内存块标记为空闲,并等待下一次内存回收时将其重新合并到内存池中。

5. 内存碎片回收机制

内存碎片化是CMA中一个非常重要的设计问题。由于CMA内存池的内存是按页进行管理的(通常是4KB一页),所以在设备频繁分配和释放内存的过程中,CMA内存池很容易出现内存碎片化问题。

为了解决这个问题,CMA使用了一些内存碎片回收机制,如:

  • 内存合并:CMA会定期尝试合并空闲的内存块。合并后的内存块会形成一个更大的连续内存块,供设备使用。
  • 紧急回收:当内存池中碎片过多,无法满足新的内存请求时,CMA可能会执行紧急回收操作,尽量回收一些不活跃的内存区域来提供给新的请求。

6. 内存对齐

内存对齐对于DMA操作至关重要。CMA分配的内存通常会遵循特定的对齐要求,以保证硬件设备能够正确访问内存。例如,某些设备可能要求内存按16字节对齐,CMA在分配内存时会确保内存地址的对齐。

7. 内存池的动态扩展

CMA允许在运行时动态扩展内存池。比如,在内存需求增加时,CMA可以根据需要从空闲的内存区域中扩展内存池,以确保设备能够在内存压力较大的情况下继续正常工作。

8. 内存池的管理

内存池是通过一种称为“内存区域管理”的机制来维护的。内存池中的内存区域会分成多个物理连续的小块,每块内存的大小和对齐方式都会根据具体的需求来调整。

内存区域的标记

CMA通过标记内存区域的使用情况来管理整个内存池。每块内存区域都可能有不同的标记状态,如“已分配”、“空闲”或者“合并中”等。这些标记帮助CMA确定内存是否可以重新分配。

9. CMA的关闭与卸载

当系统关闭或设备驱动卸载时,CMA会清理相关的内存池,释放所有分配的内存,确保内存资源的合理回收。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值