在Linux内存管理体系中,连续大块物理内存的分配始终是核心难点,频繁的内存分配与释放极易造成物理内存碎片化,导致摄像头、硬件音视频编解码器等外设,以及DMA传输等关键场景所需的连续内存无法被高效获取,进而影响设备正常运行与系统性能发挥。在此背景下,连续内存分配器(CMA,Contiguous Memory Allocator)应运而生,成为解决这一痛点的关键组件。
CMA并非简单的内存预留工具,其核心价值在于平衡内存复用与连续分配需求——系统启动时预留一段连续物理内存,设备驱动闲置时,该区域可被伙伴系统用于分配可移动页面,提升内存利用率;当设备需要连续内存时,系统通过页面迁移腾出预留区域,保障需求高效满足。本文将从CMA的设计初衷出发,深入拆解其内存预留、页面迁移、分配释放等核心机制,解析其与伙伴系统、DMA子系统的协同逻辑,带你读懂Linux连续内存分配的底层逻辑与核心原理。
一、CMA 机制深度剖析
1.1 什么是CMA 机制
CMA 机制,全称 Contiguous Memory Allocator,即连续内存分配器,它是 Linux 内核内存管理系统的一个重要扩展 ,主要作用是解决内存碎片化导致的大块连续内存分配难题。在了解 CMA 机制之前,我们需要先明白为什么大块连续物理内存如此重要。
在计算机系统中,有些设备和应用对内存的访问方式比较特殊,它们需要内存的物理地址是连续的。比如一些硬件设备,像摄像头、硬件音视频编解码器等,由于它们没有 IOMMU(Input/Output Memory Management Unit,输入 / 输出内存管理单元),不支持分散的内存访问(scatter-gather),所以只能操作连续的物理内存空间才能正常工作。另外,对于一些对内存访问性能要求极高的应用,连续的物理内存可以提高缓存命中率,从而加快数据的读写速度,提升应用的整体性能。
然而,随着系统的运行,内存的使用变得越来越复杂,内存碎片化问题逐渐凸显。内存碎片化可以分为内部碎片化和外部碎片化。内部碎片化是指分配给进程的内存块中,实际使用的部分小于分配的大小,造成了内存块内部的浪费;外部碎片化则是指内存中存在许多小块的空闲内存,但由于它们不连续,无法满足大块内存的分配需求。
在传统的内存分配机制下,比如伙伴系统(buddy system),虽然它可以通过分配高 order 的页面来提供连续的内存空间,但随着内存的不断分配和释放,内存碎片化现象会越来越严重。这会导致高 order 内存分配耗时越来越长,甚至在需要分配较大的连续内存时,常常因为找不到足够大的连续空闲内存块而失败。以 ARM64 架构为例,伙伴系统中最高 order 是 10,单次支持分配的最大内存大小是 4MB,如果业务需要分配超过 4MB 的连续物理内存,伙伴系统就无法满足需求了。
这时候,CMA 机制就应运而生。CMA 机制的核心思想是预先保留一段连续的物理内存,专门用于满足那些对大块连续内存有需求的设备或应用。在系统启动时,CMA 会从整机内存中划分出一块内存区域作为预留。当其他普通的内存分配请求到来时,如果满足一定条件(比如申请的页面属性是可迁移的),这块预留的 CMA 区域内存也可以被暂时使用。而当有需要大块连续内存的业务出现时,系统会将之前占用 CMA 区域的内存页面迁移出去,从而确保能够为该业务提供连续的内存空间 。这样一来,既保证了有足够的连续内存可供特殊需求使用,又在一定程度上提高了内存的整体利用率,有效解决了内存碎片化导致的大块连续内存分配难题。
1.2 CMA的工作原理
(1)内存预留机制:CMA 的工作从系统启动那一刻就开始布局了。在系统启动阶段,CMA 会根据预先设定的规则,从系统的物理内存中精心挑选并预留出一块特定的物理内存区域。这个过程就好比在一个大仓库里,专门划出一块区域,贴上 “CMA 专用” 的标签,这块区域未来将肩负起满足连续大块内存需求的重任 。
通常,CMA 预留内存区域的大小可以通过多种方式来确定,比如在设备树(Device Tree)中进行配置,或者通过内核启动参数来指定。以设备树配置为例,开发人员可以在设备树文件里明确设置 CMA 区域的大小、起始地址等关键信息。假设我们有一个嵌入式系统,在设备树中这样定义 CMA 区域:
reserved-memory {#address-cells = <1>;#size-cells = <1>; ranges; cma_region: cma@0 { compatible = "shared-dma-pool"; reusable; size = <0x10000000>; // 预留256MB内存,0x10000000换算成十进制就是256 * 1024 * 1024 alignment = <0x100000>; // 地址对齐为1MB,0x100000换算成十进制就是1 * 1024 * 1024 };};
这样,系统启动时就会按照这个配置,预留出 256MB 的连续物理内存作为 CMA 区域。
在初始状态下,这块被预留的 CMA 内存区域虽然已经被标记为特殊用途,但它实际上还没有被真正占用。它就像一个空的 “仓库”,静静地等待着被使用。不过,它和普通的空闲内存又有所不同,它有着特殊的 “身份标识”,系统知道这是 CMA 预留区域,后续会按照 CMA 的规则来对它进行管理和使用 。
(2)内存迁移机制:当系统中有设备或应用请求大块连续物理内存时,CMA 机制会启动内存迁移过程,以确保能够满足这些请求。内存迁移是 CMA 机制的核心操作之一,它的作用就像是在仓库中,将原本放置在特定区域(CMA 区域)的货物搬运到其他地方,从而腾出一块连续的空间来存放新的大型货物。
内存迁移的触发条件主要是当普通内存分配无法满足大块连续内存请求时,且此时 CMA 区域内存在被其他普通进程占用。例如,当一个视频编码模块需要申请一块较大的连续内存来存储编码数据,而系统的普通内存由于碎片化无法提供足够大的连续内存块时,CMA 机制就会介入。
具体的内存迁移过程涉及到多个步骤和内核组件的协同工作:
- 页面选择与隔离:CMA 首先会根据分配请求确定需要迁移的页面范围。然后,将涉及该页面范围的 pageblock 从 buddy 系统中隔离出来。在 Linux 内存管理中,pageblock 是一个物理上连续的内存区域,包含多个页面。CMA 通过将 pageblock 的迁移类型由MIGRATE_CMA变更为MIGRATE_ISOLATE来实现隔离,因为 buddy 系统不会从MIGRATE_ISOLATE迁移类型的 pageblock 分配页面 。这一步就像是在仓库中,先确定要搬运哪些货物,然后将存放这些货物的区域标记为特殊区域,暂时不允许其他常规操作。
- 页面迁移:在隔离 pageblock 后,CMA 会对分配范围内已被占用的页面进行迁移处理。这涉及到将页面中的数据从原内存位置复制到新的内存位置。内核会利用内存复制函数(如memcpy等)来完成这个操作。在迁移过程中,需要确保数据的完整性和一致性,同时要处理好页面的映射关系,保证进程对内存的访问不受影响。例如,对于一个正在运行的进程,其部分内存页面位于 CMA 区域,当这些页面需要迁移时,内核会先将页面数据复制到新的内存位置,然后更新进程的页表,使其指向新的内存地址 。这个过程就像是将仓库中的货物小心地搬运到新的存储位置,并更新货物存放位置的记录。
- 内存分配:当页面迁移完成后,CMA 区域内就会出现一块连续的空闲内存空间,此时 CMA 就可以将这块连续内存分配给请求者。分配过程会返回一个指向连续内存起始地址的指针,请求者可以通过这个指针来访问和使用这块内存 。就好比在仓库中腾出了一块连续的空地后,将这块空地分配给需要存放大型货物的客户。
- 迁移后处理:内存分配完成后,CMA 还需要对迁移过程进行一些后续处理。例如,更新 CMA 区域的使用状态信息,包括已使用内存大小、空闲内存大小等;同时,将迁移后的 pageblock 重新标记为MIGRATE_CMA迁移类型,以便在后续的内存分配中,这些 pageblock 可以再次被用于可迁移页面的分配 。这一步就像是在仓库中完成货物搬运和分配后,更新仓库的库存记录,并将特殊标记的区域恢复为正常的可使用状态,以便后续的仓库管理操作。
1.3 CMA 数据结构
在 Linux 内核中,使用struct cma结构体来描述一个 CMA 区域,其定义如下:
struct cma { unsigned long base_pfn; // CMA区域物理地址的起始页帧号 unsigned long count; // CMA区域总体的页数 unsigned long *bitmap; // 位图,用于描述页的分配情况 unsigned int order_per_bit; // 位图中每个bit描述的物理页面的order值,其中页面数为2^order值 struct mutex lock; // 互斥锁,用于保护对CMA区域的访问#ifdef CONFIG_CMA_DEBUGFS struct hlist_head mem_head; // 用于调试文件系统的链表头 spinlock_t mem_head_lock; // 保护链表的自旋锁#endif const char *name; // CMA区域的名称};
base_pfn表示 CMA 区域物理地址的起始页帧号,通过这个值可以确定 CMA 区域在物理内存中的起始位置 。count则表示 CMA 区域总体的页数,它和base_pfn一起定义了 CMA 区域在内存中的范围。bitmap是一个位图,用于描述 CMA 区域内页面的分配情况,每一位对应一个或多个物理页面,0 表示该页面空闲,1 表示已分配 。order_per_bit决定了位图中每个 bit 所代表的物理页面数量,即页面数为 2 的order_per_bit次幂 。
例如,如果order_per_bit等于 0,表示每个 bit 对应 1 个物理页面;如果order_per_bit等于 1,表示每个 bit 对应 2 个物理页面。lock是一个互斥锁,用于保护对 CMA 区域的并发访问,确保在多线程或多核环境下对 CMA 区域的操作是安全的 。在启用了 CMA 调试文件系统(CONFIG_CMA_DEBUGFS)的情况下,mem_head和mem_head_lock用于在调试文件系统中展示 CMA 区域的内存使用情况等信息 。name是 CMA 区域的名称,方便在系统中标识和管理不同的 CMA 区域。
1.4 CMA 初始化流程
CMA 区域的创建主要有两种方式:通过设备树(dts 的 reserved memory)和通过命令行参数。
(1)通过设备树创建 CMA 区域:在设备树中,可以定义一个 reserved - memory 节点来描述 CMA 区域,示例如下:
reserved - memory {#address - cells = <2>;#size - cells = <2>; ranges; cma_region: cma@10000000 { compatible = "shared - dma - pool"; reusable; reg = <0x0 0x10000000 0x0 0x8000000>; };};
其中,compatible属性必须为"shared - dma - pool",表示这是一个用于共享 DMA 内存池的 CMA 区域;reusable属性表示该 CMA 内存可被伙伴系统使用;reg属性定义了 CMA 区域的起始地址和大小 。在系统启动过程中,会解析设备树,当解析到compatible为"shared - dma - pool"的节点时,会调用rmem_cma_setup函数来创建和初始化 CMA 区域 。
(2)通过命令行参数创建 CMA 区域:可以在启动内核时通过命令行参数来指定 CMA 区域的大小和位置,格式为cma=nn[MG]@[start[MG][-end[MG]]] 。例如,cma=64M@0x38000000表示在物理地址0x38000000处预留一块大小为 64MB 的 CMA 区域 。系统在启动时会解析这些参数,并通过dma_contiguous_reserve等函数来进行内存预留和 CMA 区域信息设置 。
在系统初始化过程中,CMA 的初始化调用流程如下:start_kernel() -> setup_arch() -> dma_contiguous_reserve() 。dma_contiguous_reserve()函数会读取来自命令行或设备树的 CMA 相关信息,然后通过dma_contiguous_reserve_area()进行内存预留和cma_areas内存信息设置 。接着,通过core_initcall(cma_init_reserved_areas)将cma_init_reserved_areas函数注册到系统初始化流程中 。cma_init_reserved_areas函数会遍历cma_areas数组,调用cma_activate_area()函数对每个 CMA 区域进行初始化 。在cma_activate_area函数中,会申请位图内存,对 CMA 区域内的页面进行检验和初始化,确保页面合法且处于同一内存管理区 。
二、CMA 核心工作原理
2.1 内存预留策略
CMA 的内存预留是其实现连续内存分配的基础。在系统启动阶段,CMA 会根据预先设定的配置来确定预留内存区域的大小和位置。这个配置过程可以通过多种方式实现,最常见的是通过内核编译选项或者设备树(Device Tree)进行设置。
以内核编译选项为例,在编译内核时,可以通过配置选项如CONFIG_CMA_SIZE_MBYTES来指定 CMA 区域的大小 。假设我们将其设置为 128MB,这就意味着系统在启动时会从物理内存中划出 128MB 的连续空间作为 CMA 区域。在实际的内核代码中,相关的初始化函数会读取这些配置信息,并据此进行内存预留操作。例如,在dma_contiguous_reserve函数中,会根据内核编译选项或者从设备树中解析出的信息,计算出 CMA 区域的起始地址和大小,然后通过memblock机制将这部分内存标记为预留状态,从而确保这部分内存不会被其他常规的内存分配机制所使用 。
而通过设备树进行配置则更加灵活,它允许在设备硬件描述中精确地定义 CMA 区域的各种参数。设备树是一种描述硬件设备信息的数据结构,它以树形结构组织,包含了设备的各种属性和节点信息。在设备树中,可以通过类似于下面的代码片段来定义 CMA 区域:
reserved - memory {#address - cells = <2>;#size - cells = <2>; ranges; cma_region: cma@100000000 { compatible = "shared - dma - pool"; reg = <0x0 0x100000000 0x0 0x8000000>; reusable; };};
在这段代码中,reg属性指定了 CMA 区域的起始地址(0x100000000)和大小(0x8000000,即 128MB) ,compatible属性表明这是一个用于共享 DMA 内存池的区域,reusable属性则表示当 CMA 区域未被使用时,其内存可以被其他可移动页面占用。
预留内存的意义重大,它为后续的连续内存分配提供了一个可靠的内存池。在系统运行过程中,当其他内存分配机制由于内存碎片化而无法提供连续的大块内存时,CMA 区域就成为了满足这些需求的关键。例如,在视频编码应用中,编码算法通常需要大量的连续内存来存储和处理视频帧数据 。如果没有 CMA 预留内存,由于内存碎片化,可能无法及时分配到足够大的连续内存块,导致视频编码过程卡顿甚至失败。而有了 CMA 预留内存,视频编码应用就可以从 CMA 区域获取连续内存,保证编码过程的顺利进行。
2.2 内存迁移机制
当系统中有设备或应用请求大块连续内存,而 CMA 区域中已被其他可移动页面占用时,内存迁移机制就开始发挥作用。CMA 的内存迁移机制是其实现高效连续内存分配的关键技术之一,它通过将已分配的可移动页面从 CMA 区域迁移到其他内存位置,从而腾出连续的内存空间来满足新的内存请求。
内存迁移的过程涉及多个步骤和复杂的技术细节。首先,CMA 会确定需要迁移的页面范围。这通常是根据当前 CMA 区域的使用情况以及新的内存请求大小来计算的。例如,如果一个设备请求 16MB 的连续内存,而 CMA 区域中当前有一些可移动页面占用了部分空间,CMA 会通过其内部的位图(bitmap)等数据结构来标识出哪些页面需要被迁移,以腾出 16MB 的连续空间。
接下来,CMA 会与内核的页面迁移机制协同工作,将这些需要迁移的页面移动到其他合适的内存位置。在 Linux 内核中,页面迁移主要通过migrate_pages函数来实现 。这个函数会将源页面的数据复制到目标页面,然后更新相关的页表项,确保系统能够正确地访问迁移后的页面。在 CMA 的内存迁移过程中,会调用migrate_pages函数,将 CMA 区域中需要迁移的可移动页面复制到其他内存区域,同时更新页面的相关元数据,如页面的迁移类型等 。
在迁移过程中,还需要考虑一些技术细节和潜在问题。例如,页面的一致性问题。由于内存迁移涉及到数据的复制,在复制过程中需要确保数据的一致性,避免出现数据丢失或损坏的情况。为了解决这个问题,内核在页面迁移过程中会采用一些同步机制,如使用锁来保护页面数据的访问 。同时,还需要处理页表的更新和内存映射的调整,以确保系统在页面迁移后能够正确地寻址和访问内存。例如,当一个页面从 CMA 区域迁移到其他内存区域后,需要更新该页面在各级页表中的映射关系,使得 CPU 在访问该页面时能够正确地找到其新的物理地址。
2.3 内存分配与释放流程
CMA 提供了一系列的函数接口来实现内存的分配和释放操作,这些接口是开发者与 CMA 交互的主要方式。在 CMA 中,主要的内存分配函数是dma_alloc_from_contiguous,而内存释放函数则是dma_free_from_contiguous。dma_alloc_from_contiguous函数用于从 CMA 区域分配连续内存,其函数原型通常如下:
void *dma_alloc_from_contiguous(struct device *dev, size_t size, dma_addr_t *dma_handle, gfp_t gfp_mask);
其中,dev参数指定了分配内存的设备,size参数表示需要分配的内存大小,dma_handle用于返回分配内存的物理地址,gfp_mask则是分配标志,用于指定内存分配的一些属性,如是否允许睡眠等 。在实际使用中,开发者可以通过调用这个函数来从 CMA 区域获取连续内存。例如:
#include <linux/device.h>#include <linux/dma-mapping.h>struct device *dev = get_device(); // 获取设备指针size_t size = 1024 * 1024; // 1MB内存dma_addr_t dma_handle;void *buffer = dma_alloc_from_contiguous(dev, size, &dma_handle, GFP_KERNEL);if (buffer) { // 内存分配成功,进行后续操作 // ...} else { // 内存分配失败,处理错误 // ...}
在这段代码中,首先获取了一个设备指针dev,然后定义了需要分配的内存大小为 1MB。接着调用dma_alloc_from_contiguous函数从 CMA 区域分配内存,并将返回的内存指针存储在buffer变量中。如果分配成功,可以在后续代码中对buffer进行操作;如果分配失败,则需要处理相应的错误情况。
内存释放的过程相对简单,通过调用dma_free_from_contiguous函数来实现:
voiddma_free_from_contiguous(struct device *dev, size_t size, void *vaddr, dma_addr_t dma_handle);
其中,dev、size和dma_handle参数与分配函数中的含义相同,vaddr是之前分配内存时返回的虚拟地址。例如,在上述分配内存的示例中,当不再需要使用分配的内存时,可以通过以下代码释放内存:
dma_free_from_contiguous(dev, size, buffer, dma_handle);
在使用 CMA 的内存分配和释放接口时,需要注意一些事项。首先,分配和释放操作必须配对使用,确保每一次分配的内存最终都能被正确释放,以避免内存泄漏。其次,在分配内存时,要根据实际需求合理设置gfp_mask标志。例如,如果在中断处理程序中分配内存,由于中断处理程序不能睡眠,所以需要设置GFP_ATOMIC标志,而在一般的进程上下文中,可以根据情况设置GFP_KERNEL等标志 。此外,还需要注意内存对齐的问题,某些设备对内存对齐有特定的要求,在分配内存时需要确保分配的内存地址满足这些要求,否则可能会导致设备访问内存出错。
三、CMA 机制的配置与使用方法
3.1内核参数配置
通过内核启动参数来配置 CMA 是一种较为便捷的方式,它允许用户在系统启动时快速设置 CMA 区域的相关参数。在 Linux 系统中,常见的内核启动参数格式为cma=size[@start-end]。例如,cma=256M@3G-4G,其中256M表示预留的 CMA 内存大小为 256MB,3G-4G则指定了该 CMA 区域在物理内存中的起始地址为 3GB,结束地址为 4GB 。这种方式的优点在于简单直接,用户只需在系统启动加载器(如 GRUB 等)的配置文件中修改bootargs参数,添加或调整cma相关配置,然后重启系统即可生效。
对于一些对系统内存布局有明确规划,且不需要频繁更改 CMA 配置的场景,内核参数配置方式非常适用。比如在一个基于 ARM 架构的嵌入式系统中,已知系统在 3GB 到 4GB 之间的内存区域访问性能较为稳定,且该系统中的摄像头模块需要大块连续内存,就可以通过这种方式在该区域预留 256MB 的 CMA 内存供摄像头模块使用 。
3.2设备树配置方式
在支持设备树的硬件平台上,利用设备树配置 CMA 区域提供了更灵活和详细的配置选项。设备树是一种描述硬件设备信息的数据结构,它可以准确地定义硬件设备的属性和连接关系。在设备树中,CMA 区域通常在reserved-memory节点下进行定义。下面是一个完整的设备树配置 CMA 区域的示例:
reserved-memory {#address-cells = <2>;#size-cells = <2>; ranges; cma_region: cma@80000000 { compatible = "shared-dma-pool"; reusable; linux,cma-default; reg = <0x0 0x80000000 0x0 0x40000000>; };};
- #address-cells和#size-cells用于定义地址和大小的表示方式。这里<2>表示地址和大小都使用两个单元来表示,这是一种符合设备树规范的表示方法,在不同的硬件平台和内核版本中,其值可能会根据实际情况有所变化 。
- compatible属性设置为"shared-dma-pool",表明这是一个共享的 DMA 内存池,这是 CMA 区域在设备树中的标准标识,用于告知内核该区域是用于连续内存分配的特殊区域 。
- reusable属性非常重要,它表示该 CMA 区域在未被大块连续内存请求占用时,可以被系统其他部分复用,大大提高了内存的利用率。例如,在系统启动后的一段时间内,如果没有设备需要大块连续内存,该 CMA 区域内的内存可以被普通的进程分配使用 。
- linux,cma-default属性将此 CMA 区域标记为系统默认的 CMA 区域。当系统中有多个 CMA 区域时,通过这种方式可以指定一个默认区域,方便系统在进行内存分配时优先考虑该区域 。
- reg属性定义了 CMA 区域的具体信息,<0x0 0x80000000 0x0 0x40000000>中,前两个值0x0 0x80000000表示起始地址(这里是 0x80000000,即 2GB),后两个值0x0 0x40000000表示大小(这里是 0x40000000,即 1GB) 。通过这种方式,可以精确地指定 CMA 区域在内存中的位置和大小。
设备树配置方式适用于需要根据硬件平台的具体特性进行定制化配置的场景,特别是在嵌入式系统开发中,不同的硬件板卡可能有不同的内存布局和需求,使用设备树配置 CMA 区域可以更好地适应这些变化。
3.3代码中使用 CMA 内存
在内核驱动开发中,当需要使用 CMA 内存时,通常会借助 DMA API 来进行内存分配。以一个简单的字符设备驱动为例,假设该设备需要使用 CMA 内存进行数据传输,以下是相关的代码示例及流程说明:
#include <linux/module.h>#include <linux/kernel.h>#include <linux/fs.h>#include <linux/cdev.h>#include <linux/dma-mapping.h>#include <linux/dma-contiguous.h>#define DEVICE_NAME "my_cma_device"#define BUFFER_SIZE 4096 // 假设需要分配4KB的CMA内存static dev_t dev_num;static struct cdev cdev;static struct class *class;static struct device *device;static void __exit my_cma_device_exit(void) { // 释放设备相关资源 device_destroy(class, dev_num); class_destroy(class); cdev_del(&cdev); unregister_chrdev_region(dev_num, 1);}static int __init my_cma_device_init(void) { int ret; void *cma_buffer; dma_addr_t dma_handle; // 分配设备号 ret = alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME); if (ret < 0) { printk(KERN_ERR "Failed to allocate device number\n"); return ret; } // 初始化字符设备 cdev_init(&cdev, NULL); cdev.owner = THIS_MODULE; ret = cdev_add(&cdev, dev_num, 1); if (ret < 0) { printk(KERN_ERR "Failed to add cdev\n"); unregister_chrdev_region(dev_num, 1); return ret; } // 创建类和设备节点 class = class_create(THIS_MODULE, DEVICE_NAME); if (IS_ERR(class)) { ret = PTR_ERR(class); printk(KERN_ERR "Failed to create class\n"); cdev_del(&cdev); unregister_chrdev_region(dev_num, 1); return ret; } device = device_create(class, NULL, dev_num, NULL, DEVICE_NAME); if (IS_ERR(device)) { ret = PTR_ERR(device); printk(KERN_ERR "Failed to create device\n"); class_destroy(class); cdev_del(&cdev); unregister_chrdev_region(dev_num, 1); return ret; } // 使用DMA API分配CMA内存 cma_buffer = dma_alloc_coherent(&device->dev, BUFFER_SIZE, &dma_handle, GFP_KERNEL); if (!cma_buffer) { printk(KERN_ERR "Failed to allocate CMA memory\n"); device_destroy(class, dev_num); class_destroy(class); cdev_del(&cdev); unregister_chrdev_region(dev_num, 1); return -ENOMEM; } // 在这里可以使用分配到的CMA内存进行数据操作,例如: // memset(cma_buffer, 0, BUFFER_SIZE); // 释放CMA内存 dma_free_coherent(&device->dev, BUFFER_SIZE, cma_buffer, dma_handle); return 0;}module_init(my_cma_device_init);module_exit(my_cma_device_exit);MODULE_LICENSE("GPL");MODULE_AUTHOR("Your Name");MODULE_DESCRIPTION("A simple CMA device driver example");
- 包含必要的头文件:#include <linux/dma-mapping.h>和#include <linux/dma-contiguous.h>提供了使用 DMA API 和 CMA 相关的函数和数据结构定义。
- 设备初始化部分:通过alloc_chrdev_region分配设备号,cdev_init和cdev_add初始化并添加字符设备,class_create和device_create创建设备类和设备节点,这些是字符设备驱动开发的基本步骤 。
- CMA 内存分配:使用dma_alloc_coherent函数来分配 CMA 内存。该函数的第一个参数为设备结构体指针&device->dev,表示要为该设备分配内存;第二个参数BUFFER_SIZE指定分配的内存大小为 4KB;第三个参数&dma_handle用于返回分配内存对应的 DMA 地址,这个地址将用于设备与内存之间的数据传输;第四个参数GFP_KERNEL是内存分配标志,表示在进程上下文中进行内存分配,允许睡眠 。如果分配失败,函数会返回NULL,并进行相应的错误处理,释放之前分配的设备资源。
- 内存使用与释放:在成功分配 CMA 内存后,可以对cma_buffer指向的内存进行数据操作,如填充数据等。在驱动不再需要该内存时,通过dma_free_coherent函数释放分配的 CMA 内存,传入的参数与分配时一致,确保内存被正确释放,避免内存泄漏 。
通过上述代码示例,可以清晰地看到在内核驱动中如何使用 DMA API 来分配和释放 CMA 内存,以满足设备对大块连续内存的需求。在实际应用中,根据不同的设备和业务需求,可能需要对内存分配的大小、分配标志以及数据操作逻辑进行相应的调整 。
四、CMA 调试技巧
4.1前期准备:开启 CMA 调试功能
要调试 CMA,首先需要开启内核的调试开关,具体操作是开启两个关键的内核配置:CONFIG_CMA_DEBUG 和 CONFIG_CMA_DEBUGFS 。
CONFIG_CMA_DEBUG 就像是一个隐藏的调试按钮,当我们把它打开(设置为 y)时,内核就会激活一系列的调试代码和功能,这些代码和功能就像是为我们调试 CMA 量身定制的工具,能帮助我们深入了解 CMA 的运行情况 。而 CONFIG_CMA_DEBUGFS 则是开启了一个特殊的文件系统接口 —— 调试文件系统(debugfs),它就像是一个通往 CMA 内部信息的大门,通过这个接口,我们可以方便地获取和修改 CMA 的各种调试信息 。
具体的配置方法也不难,如果你是使用 make menuconfig 来配置内核的话 ,可以按照以下步骤操作:首先,在配置界面中找到 “Memory Management options” ,进入这个选项后,再找到 “Contiguous Memory Allocator (CMA)” 相关的配置项,在这里,你就能看到 CONFIG_CMA_DEBUG 和 CONFIG_CMA_DEBUGFS 这两个选项了,把它们都设置为 y 。如果你是使用其他方式配置内核,比如直接修改.config 文件,那就找到对应的配置项,把它们的值改为 y 就可以了 。
内核配置实操代码示例如下:
# 1. 进入内核配置界面(通用方法)make menuconfig# 2. 直接修改.config 文件(快速配置,适合熟悉内核配置的开发者)# 打开.config 文件,添加/修改以下两行echo "CONFIG_CMA_DEBUG=y" >> .configecho "CONFIG_CMA_DEBUGFS=y" >> .config# 3. 保存配置并重新编译内核(编译命令根据自身编译环境调整)make -j$(nproc)make modules_installmake install# 4. 重启系统,验证调试功能是否开启(查看 debugfs 挂载情况)mount | grep debugfs# 若输出类似 "debugfs on /sys/kernel/debug type debugfs (rw,relatime)",说明开启成功
4.2 核心调试工具:利用 debugfs 查看 CMA 状态
当我们成功打开了 CONFIG_CMA_DEBUG 和 CONFIG_CMA_DEBUGFS 这两个开关后,就可以在 /sys/kernel/debug/cma 目录下找到一系列非常有用的调试文件 。这些文件就像是一个个小侦探,能帮我们获取 CMA 区域的各种关键信息,快速定位问题。
(1)alloc 文件:用于控制 CMA 内存的分配与释放,通过向该文件写入数值,可直接操作 CMA 内存的分配数量(单位:内存页),用于排查 CMA 内存分配是否正常。CMA 内存分配调试示例:
# 1. 查看系统中所有 CMA 区域(确认 CMA 区域名称,避免操作错误)ls /sys/kernel/debug/cma/# 输出示例:cma-0 (单 CMA 区域)、cma-1 cma-2 (多 CMA 区域)# 2. 分配 CMA 内存(以 cma-0 为例,分配 1024 个内存页,每页默认 4KB,即 4MB)echo 1024 > /sys/kernel/debug/cma/cma-0/alloc# 3. 查看分配后的 CMA 使用情况(验证是否分配成功)cat /sys/kernel/debug/cma/cma-0/used# 输出示例:4096 (单位:KB,即 4MB,与申请的 1024*4KB 一致)# 4. 释放已分配的 CMA 内存(释放所有已分配的 CMA 内存)echo 0 > /sys/kernel/debug/cma/cma-0/alloc# 5. 再次查看使用情况,确认释放成功cat /sys/kernel/debug/cma/cma-0/used# 输出示例:0
(2)base_pfn 文件:记录了 CMA 区域的起始物理页帧号(Page Frame Number,PFN) 。PFN 就像是内存的 “门牌号”,通过它我们可以准确地定位到 CMA 区域在物理内存中的起始位置 。知道了起始位置,我们就能更好地理解 CMA 区域在内存中的布局和使用情况 。比如,我们可以通过这个起始位置,结合 CMA 区域的大小,计算出 CMA 区域在内存中的具体范围 。
查看 CMA 区域物理地址范围示例:
# 1. 查看 CMA 区域起始页帧号(PFN)cat /sys/kernel/debug/cma/cma-0/base_pfn# 输出示例:1048576 (不同系统输出不同,仅为示例)# 2. 查看 CMA 区域总大小(单位:页)cat /sys/kernel/debug/cma/cma-0/size# 输出示例:262144 (即 262144 页,每页 4KB,总大小 1GB)# 3. 计算 CMA 区域物理地址范围(物理地址 = PFN * 页大小,默认页大小 4KB = 4096 字节)# 起始物理地址:1048576 * 4096 = 0x40000000(十六进制)# 结束物理地址:(1048576 + 262144) * 4096 = 0x50000000(十六进制)# 命令行快速计算(需安装 bc 工具)base_pfn=$(cat /sys/kernel/debug/cma/cma-0/base_pfn)size=$(cat /sys/kernel/debug/cma/cma-0/size)page_size=4096start_addr=$(echo "$base_pfn * $page_size" | bc)end_addr=$(echo "($base_pfn + $size) * $page_size" | bc)echo "CMA 物理地址范围:0x$(printf '%x' $start_addr) - 0x$(printf '%x' $end_addr)"
(3) bitmap 文件:这是一个非常强大的调试工具,它就像是一个内存使用情况的 “监视器” 。这个文件以位图的形式记录了 CMA 区域中每一个内存页的使用状态 。每一位(bit)对应一个内存页,如果该位的值为 1,表示对应的内存页正在被使用;如果为 0,则表示该内存页处于空闲状态 。通过查看这个文件,我们可以直观地了解 CMA 区域中哪些内存页被占用了,哪些是空闲的,从而判断内存分配和释放是否正常,也能快速排查内存碎片化问题 。
查看 CMA 内存页使用状态代码示例如下:
# 1. 查看 CMA 区域 bitmap(原始二进制,可读性较差)cat /sys/kernel/debug/cma/cma-0/bitmap# 2. 格式化查看 bitmap(转换为十六进制,便于分析,推荐)hexdump -C /sys/kernel/debug/cma/cma-0/bitmap# 输出示例(每两位十六进制对应 8 个内存页的状态):# 00000000 00 00 00 00 0f f0 00 00 00 00 00 00 00 00 00 00 |................|# 解析:0f f0 表示 16 个内存页中,中间 8 个页(bit4-bit11)被占用(值为 1),其余空闲(值为 0)# 3. 结合 alloc 命令,观察 bitmap 变化(验证内存分配/释放与状态同步)# 第一步:分配 8 个内存页echo 8 > /sys/kernel/debug/cma/cma-0/alloc# 第二步:查看 bitmap 变化hexdump -C /sys/kernel/debug/cma/cma-0/bitmap# 第三步:释放内存echo 0 > /sys/kernel/debug/cma/cma-0/alloc# 第四步:再次查看 bitmap,确认恢复空闲hexdump -C /sys/kernel/debug/cma/cma-0/bitmap
4.3常见问题及解决思路(实操性极强)
在使用 CMA 的过程中,我们可能会遇到各种各样的问题,下面就来列举一些常见的问题,并提供相应的解决思路和调试步骤,帮你快速解决问题。
问题一:CMA 内存分配失败
当出现 CMA 内存分配失败时,首先要检查的就是 CMA 区域的大小是否足够 。如果 CMA 区域本身就很小,而我们申请的内存量又比较大,那么就很容易导致分配失败 。可以通过查看 /sys/kernel/debug/cma/cma-0/size 文件来了解 CMA 区域的总大小,再结合 /sys/kernel/debug/cma/cma-0/used 文件查看当前已经被使用的内存大小,判断是否还有足够的空闲内存可供分配 。比如,如果 CMA 区域总大小为 1024MB,而 used 文件显示已经使用了 900MB,那么当我们申请 200MB 内存时,就很可能会失败 。
另外,内存碎片化也可能导致 CMA 内存分配失败 。虽然 CMA 的目的就是解决内存碎片化问题,但在某些情况下,还是可能会出现碎片化的情况 。这时,可以通过查看 bitmap 文件,分析内存页的使用情况,看看是否存在大量零散的空闲内存页 。如果存在,可以尝试手动触发内存规整操作,在 Linux 系统中,可以通过向 /proc/sys/vm/compact_memory 文件写入 1 来触发内存规整操作,命令为 “echo 1 > /proc/sys/vm/compact_memory” 。内存规整操作会将零散的空闲内存页整理成连续的内存块,从而满足 CMA 内存分配的需求 。
解决 CMA 内存分配失败示例如下:
# 1. 排查分配失败原因:查看 CMA 总大小和已使用大小cat /sys/kernel/debug/cma/cma-0/size # 查看总大小(单位:页)cat /sys/kernel/debug/cma/cma-0/used # 查看已使用大小(单位:KB)# 2. 查看内存碎片化情况(通过 bitmap 分析)hexdump -C /sys/kernel/debug/cma/cma-0/bitmap# 3. 手动触发内存规整,整理零散空闲内存echo 1 > /proc/sys/vm/compact_memory# 4. 再次尝试分配内存(以申请 1024 个内存页为例)echo 1024 > /sys/kernel/debug/cma/cma-0/alloc# 无报错即分配成功,验证使用情况cat /sys/kernel/debug/cma/cma-0/used
问题二:CMA 内存迁移异常
在 CMA 进行内存迁移的过程中,可能会出现迁移异常的情况 。一种可能的原因是某些内存页上的数据无法正常迁移 。这可能是因为这些数据被一些特殊的进程或设备锁定了,导致无法移动 。可以通过查看内核日志(比如使用 dmesg 命令)来获取更多关于迁移异常的信息,看看是否有相关的错误提示 。比如,日志中可能会显示 “cma: migrate_pages failed, page 0x40001000 is locked”,这表示物理地址为 0x40001000 的内存页被锁定,无法进行迁移 。
如果发现是某些进程或设备锁定了内存页,可以尝试找出这些进程或设备,并解除它们对内存页的锁定 。比如,有些驱动程序可能会锁定内存页,我们可以检查相关的驱动代码,看看是否有优化的空间,或者尝试更新驱动程序 。如果是测试进程锁定了内存页,可以直接终止该进程,解除锁定 。
排查 CMA 迁移异常示例如下:
# 1. 查看内核日志,定位迁移异常信息(筛选 CMA 相关日志)dmesg | grep -i "cma" | grep -i "migrate"# 输出示例(迁移失败日志):# [1234.567890] cma: migrate_pages failed, page 0x40001000 is locked# 2. 查找锁定内存页的进程(根据日志中的物理地址,转换为虚拟地址,排查进程)# 安装 debug 工具(以 Ubuntu 为例)apt install linux-tools-common linux-tools-$(uname -r)# 查看所有锁定内存页的进程ps -eLo pid,cmd | grep -i "mlock"# 或使用 pagemap 工具,根据物理地址查找进程(需 root 权限)grep -l 0x40001000 /proc/*/pagemap# 3. 解除内存页锁定(若为测试进程,可直接终止)# 终止锁定进程(示例,根据实际 pid 调整)kill -9 1234# 或在代码中解除内存锁定(C 语言示例,驱动/应用层通用)#include <sys/mman.h>int main() { void *addr = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0); mlock(addr, 4096); // 锁定内存页(模拟异常场景) munlock(addr, 4096); // 解除锁定(解决迁移异常) return 0;}# 4. 重启 CMA 相关服务(或重启系统),验证迁移是否正常echo 1 > /proc/sys/vm/compact_memory # 触发内存规整echo 1024 > /sys/kernel/debug/cma/cma-0/alloc # 尝试分配,验证是否正常
另一种可能的原因是内存迁移算法本身出现了问题 。虽然 Linux 内核的内存迁移算法经过了大量的测试和优化,但在某些特殊情况下,还是可能会出现异常 。这时,可以尝试更新内核版本,看看是否能解决问题 。新的内核版本通常会修复一些已知的内存管理问题,包括内存迁移方面的问题 。如果更新内核版本后问题仍然存在,那就需要深入分析内核源码,找出具体的问题所在,这可能需要一定的内核开发经验和技能 。