[Linux]学习笔记系列 -- [drivers][dma]coherent


title: coherent

categories:

  • linux
  • drivers
  • dma
    tags:
  • linux
  • kernel
  • dma
    abbrlink: 5a79feca
    date: 2025-10-03 09:01:49

https://github.com/wdfk-prog/linux-study

文章目录

kernel/dma DMA引擎(DMA Engine)与DMA映射(DMA Mapping)框架

历史与背景

这项技术是为了解决什么特定问题而诞生的?

kernel/dma 目录下的代码是为了解决一个计算机体系结构中的核心问题:如何让外围设备(Peripherals)高效地与主内存(Main Memory)进行数据交换,而无需中央处理器(CPU)的持续干预

在没有DMA(Direct Memory Access,直接内存访问)的系统中,当一个设备(如网卡)需要发送数据时,CPU必须亲自逐字节或逐字地从内存中读取数据,然后再写入到网卡的寄存器中。这个过程称为PIO(Programmed I/O)。对于大量数据的传输,这会完全占用CPU,使其无法执行其他计算任务,极大地降低了系统效率。

DMA技术引入了一个专门的硬件单元------DMA控制器(DMAC)。CPU只需配置好DMAC(告诉它源地址、目标地址、传输长度),然后就可以去做其他事情了。DMAC会接管总线控制权,直接在设备和内存之间搬运数据,传输完成后再通过中断通知CPU。

kernel/dma 子系统就是为了在Linux内核中管理和抽象这种硬件能力而诞生的,它主要解决了两大问题:

  1. 内存地址一致性 (DMA Mapping) :CPU、设备和DMAC对内存的"视角"是不同的。CPU使用虚拟地址,而设备使用的是物理地址或总线地址(Bus Address)。此外,现代系统还有IOMMU(I/O内存管理单元),它在物理地址和总线地址之间又增加了一层转换。DMA Mapping框架 (dma-mapping.c) 的任务就是提供一套API,负责处理这些地址空间的转换、管理缓存一致性(Cache Coherency)以及与IOMMU交互,为设备驱动提供一个统一、安全的内存访问视图。
  2. DMA控制器多样性 (DMA Engine) :市面上有成百上千种不同的DMAC硬件,它们的功能和编程接口各不相同。DMA Engine框架 (dmaengine.c) 的任务是提供一个通用的"生产者-消费者"模型,将具体的DMAC驱动(生产者)和需要使用DMA服务的设备驱动(消费者/客户端)解耦。这使得一个SPI驱动可以在不关心底层DMAC是PL330还是IDMA64的情况下,请求并使用DMA服务来传输数据。
它的发展经历了哪些重要的里程碑或版本迭代?
  • 早期架构特定API:在早期内核中,DMA操作的API是高度依赖于特定硬件架构的(如ISA总线的DMA)。
  • 通用DMA Mapping框架 :一个重要的里程碑是创建了通用的DMA Mapping层,引入了dma_map_* / dma_unmap_* 等一系列API。这使得设备驱动可以以一种可移植的方式编写,无需关心底层是x86还是ARM,是否有IOMMU。
  • IOMMU子系统的集成:随着IOMMU的普及,DMA Mapping框架与IOMMU子系统紧密集成,提供了更好的内存保护、安全性以及对非连续物理内存的DMA支持。
  • DMA Engine子系统的建立:为了解决内存到内存(memory-to-memory)和内存到从设备(memory-to-slave-device)传输的通用需求,DMA Engine子系统被创建。它定义了DMA通道(Channel)、事务描述符(Descriptor)和异步回调(Callback)等标准概念,极大地促进了驱动代码的复用。
目前该技术的社区活跃度和主流应用情况如何?

DMA是现代Linux内核的基石,kernel/dma 是最核心、最活跃的子系统之一。

  • DMA Mapping :任何执行DMA操作的设备驱动(包括所有高性能的网络、存储、图形驱动)都必须使用这个框架。
  • DMA Engine :被内核中众多子系统广泛使用,以提升性能。例如:
    • SPI、I2S、UART等串行总线驱动,用于卸载数据流传输。
    • 内核加密子系统(Crypto subsystem),用于加速内存中数据的加解密。
    • 视频采集(V4L2)和音频(ASoC)驱动,用于处理媒体数据流。

核心原理与设计

它的核心工作原理是什么?

kernel/dma 的工作原理可以清晰地分为DMA Mapping和DMA Engine两个部分。

1. DMA Mapping 框架:

这是一个API层,为设备驱动屏蔽了底层内存管理的复杂性。

  • 地址转换 :驱动调用 dma_map_single()dma_map_page(),传入一个CPU可见的虚拟地址。该API会:
    1. 将虚拟地址转换为物理地址。
    2. 如果存在IOMMU,为这段物理内存创建IOMMU映射,生成一个设备可见的"I/O虚拟地址"(也称总线地址)。如果不存在IOMMU,总线地址通常就等于物理地址。
    3. 返回这个dma_addr_t类型的总线地址给驱动,驱动再将此地址编程到设备硬件中。
  • 缓存一致性 :在DMA传输前后,CPU缓存和主存之间可能存在数据不一致。dma_map_* API会根据传输方向(DMA_TO_DEVICE, DMA_FROM_DEVICE等)自动处理缓存。例如,对于到设备的传输,它会确保CPU缓存中修改过的数据被"写回"(flushed)到主存,这样设备才能读到最新的数据。
  • Bounce Buffering:对于一些老旧或有缺陷的硬件(如只能访问低端16MB内存的ISA设备),如果驱动尝试对高地址内存进行DMA,Mapping框架会自动分配一个位于低地址的"反弹缓冲区"(Bounce Buffer),先由CPU将数据拷贝到这里,再启动DMA。

2. DMA Engine 框架:

这是一个标准的生产者-消费者模型。

  • 生产者 (Provider) :具体的DMAC硬件驱动(如dma/pl330.c)负责初始化硬件。它会向DMA Engine核心注册自己,描述自身的能力(如支持内存到内存、支持循环传输等)。
  • 消费者 (Client) :需要DMA服务的驱动(如一个SPI驱动)通过 dma_request_channel() API向DMA Engine请求一个通道。它可以提供一个过滤器函数,以确保申请到的通道满足特定要求(例如,必须连接到指定的SPI控制器)。
  • 事务描述 (Transaction Descriptor) :客户端准备好一次传输后,不会直接操作硬件。而是填充一个 struct dma_async_tx_descriptor 结构体,描述这次传输的源、目的、长度等信息,并提供一个完成后的回调函数。
  • 异步提交与回调 :客户端通过 dmaengine_submit() 提交这个描述符,然后可以立即返回去做别的事情。DMA Engine核心会将该请求传递给相应的DMAC驱动,由后者将其放入硬件队列并启动传输。传输完成后,DMAC硬件产生中断,其驱动在中断处理程序中调用之前客户端注册的回调函数,通知传输已完成。
它的主要优势体现在哪些方面?
  • 性能:通过将数据传输任务从CPU卸载到专用硬件,极大地提升了系统吞吐量和CPU利用率。
  • 抽象与可移植性:驱动程序只需面向通用API编程,无需关心具体的CPU架构、内存布局、IOMMU或DMAC型号,使得驱动代码高度可移植。
  • 模块化:清晰地将设备驱动(DMA客户端)和DMAC驱动(DMA提供者)分离开来,降低了耦合度。
它存在哪些已知的劣势、局限性或在特定场景下的不适用性?
  • 设置开销(Setup Overhead):对于非常小(通常指几十或几百字节)的数据传输,配置DMA、创建描述符、处理中断等一系列操作的开销,可能比CPU直接拷贝数据还要大。
  • 编程复杂性:尤其是DMA Engine的异步模型,需要驱动开发者仔细管理回调函数、锁和内存生命周期,比简单的同步PIO编程要复杂。
  • 调试困难:DMA相关的问题,如缓存不一致、错误的内存描述符等,导致的内存损坏通常是"静默"且难以复现的,是内核中最难调试的问题之一。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?请举例说明。
  • 大数据块传输 :这是DMA最经典的应用场景。
    • 网络:网卡驱动使用DMA将网络数据包在内存和网卡FIFO之间来回传输。
    • 存储:SATA或NVMe驱动使用DMA在内存和磁盘之间读写数据块。
  • 流式数据处理
    • 音频:声卡驱动使用DMA的循环模式(Cyclic DMA)持续地从内存缓冲区读取数据进行播放,或将录音数据写入缓冲区,无需CPU干预。
    • 视频:摄像头驱动(CSI)使用DMA将图像帧数据从传感器传输到内存。
  • 计算卸载
    • 加密:内核加密子系统使用DMA Engine将内存中的明文数据喂给硬件加密引擎,并将结果写回内存。
    • 内存拷贝 :在一些SoC中,memcpy 操作本身就可以被卸载到DMAC上执行,以加速大规模内存拷贝。
是否有不推荐使用该技术的场景?为什么?
  • 小数据量传输 :当传输的数据量非常小(例如,小于一个缓存行大小)且频率不高时,使用CPU进行PIO或memcpy通常更快,因为DMA的设置开销不划算。
  • 设备控制:读写设备的控制寄存器、状态寄存器等,这些通常只是几个字节的操作,必须由CPU通过PIO方式同步完成。

对比分析

请将其 与 其他相似技术 进行详细对比。

DMA的主要对比对象就是由CPU直接执行的数据传输。

特性 DMA (Direct Memory Access) CPU-based Transfer (PIO / memcpy)
实现方式 CPU配置专用的DMAC硬件,由硬件完成数据搬运。 CPU执行mov/load/store等指令,逐个数据单元地搬运。
CPU占用率 极低。CPU仅在启动和完成时介入,传输期间可执行其他任务。 100%。在传输期间,CPU被完全占用,无法响应其他事件。
性能 对于大数据块非常高。吞吐量受限于总线和内存带宽。 对于大数据块较低。性能受限于CPU的执行速度。
启动延迟 较高。需要配置控制器、描述符,有一定的软件开销。 极低。几乎没有启动开销,指令可以立即执行。
编程模型 复杂。通常是异步的,需要处理回调和缓存一致性。 简单。是同步的、阻塞的操作。
适用场景 大数据块传输、数据流处理,如网络、存储、音视频。 小数据块传输、设备寄存器读写、对启动延迟敏感的操作。

kernel/dma/coherent.c

dma_init_coherent_memory: 初始化一块相干DMA内存区域

此函数的核心作用是接收一块物理内存区域的地址和大小, 并为其创建一个 dma_coherent_mem 管理结构体。这个结构体将这块物理内存转化为一个可用的、基于位图 (bitmap) 分配器的相干DMA内存池。它是实现内核DMA子系统的底层构建块, 负责内存池的元数据初始化。

  1. 内存映射 (memremap) : 在有MMU的系统中, memremap 会为指定的物理地址创建一段新的内核虚拟地址映射。但在无MMU的系统中, 物理地址和内核的虚拟地址本质上是相同的 。因此, memremap 的主要作用简化为:

    • 进行合法性检查。
    • 返回一个 void * 类型的指针, 该指针的值等于传入的物理地址 (phys_addr)。这使得类型系统能够正确处理, 让内核代码可以通过此指针直接访问该物理内存。
    • 尝试根据 MEMREMAP_WC (Write-Combining) 标志, 配置内存区域的缓存属性。在ARMv7M架构上, 这通常通过配置MPU (Memory Protection Unit) 来实现, 将该区域设置为支持写合并的内存类型, 以优化对显存或DMA缓冲区的连续写入性能。如果MPU不支持或未配置, 此标志可能被忽略。
  2. 内存池管理 : 由于无MMU系统无法进行虚拟内存分页, 拥有大块的、物理连续的内存对DMA操作是必需的。此函数正是将从设备树中预留 (reserved) 出来的物理连续内存块, 转化为内核DMA框架可以理解和管理的实体。它通过 bitmap_zalloc 创建了一个位图, 内存池中的每个页 (PAGE_SIZE) 对应位图中的一位。当设备请求分配DMA内存时, 就通过在这个位图中查找和设置相应的位来完成分配, 从而实现了对这块珍贵的连续内存的精细化管理。

  3. 锁机制 (spin_lock_init) : 即使在单核系统中, 如果内核是抢占式的 (CONFIG_PREEMPT), 自旋锁依然是必需的。它在这里的作用不是防止多核并发, 而是防止任务抢占 。当一个任务正在修改内存池的位图时, spin_lock 可以暂时禁用内核抢占, 确保这个关键操作的原子性, 防止另一个更高优先级的任务抢占它并试图从同一个池中分配或释放内存, 从而避免了位图数据的损坏。

c 复制代码
/*
 * dma_init_coherent_memory: 初始化一块给定的物理内存, 使之成为一个相干DMA内存池.
 *
 * @phys_addr:      内存区域的物理起始地址.
 * @device_addr:    设备视角看到的DMA起始地址. 在没有IOMMU的系统中, 这通常等于物理地址.
 * @size:           内存区域的总大小, 单位为字节.
 * @use_dma_pfn_offset: 一个布尔值, 指示是否使用设备特定的PFN偏移量.
 * @return:         成功时返回一个指向新创建的 dma_coherent_mem 管理结构体的指针. 失败时返回一个错误指针 (通过 IS_ERR() 判断).
 */
static struct dma_coherent_mem *dma_init_coherent_memory(phys_addr_t phys_addr,
		dma_addr_t device_addr, size_t size, bool use_dma_pfn_offset)
{
	/*
	 * 定义一个指向 dma_coherent_mem 结构体的指针 dma_mem.
	 * 这个结构体将包含管理此DMA内存池所需的所有元数据 (如位图, 基地址, 锁等).
	 */
	struct dma_coherent_mem *dma_mem;
	/*
	 * 定义一个整型变量 pages.
	 * 用于存储内存区域的总大小(以页为单位). size >> PAGE_SHIFT 是一个高效的计算方法, 等价于 size / PAGE_SIZE.
	 */
	int pages = size >> PAGE_SHIFT;
	/*
	 * 定义一个 void 指针 mem_base.
	 * 它将用于存储内核可以直接访问这块物理内存的 "虚拟" 地址. 在无MMU系统中, 其值将等于 phys_addr.
	 */
	void *mem_base;

	/*
	 * 检查传入的 size 是否为0. 如果是, 则无法创建内存池.
	 */
	if (!size)
		/*
		 * 返回一个内嵌了 -EINVAL (无效参数) 错误码的指针.
		 */
		return ERR_PTR(-EINVAL);

	/*
	 * 调用 memremap, 为指定的物理内存区域获取一个内核可用的地址.
	 * @ phys_addr: 要映射的物理地址.
	 * @ size:      映射的大小.
	 * @ MEMREMAP_WC: 请求将此内存区域映射为 "Write-Combining" (写合并) 缓存属性. 这可以提高对该区域连续写入的性能.
	 * 在STM32H750上, 这会尝试通过MPU配置实现.
	 */
	mem_base = memremap(phys_addr, size, MEMREMAP_WC);
	/*
	 * 检查 memremap 是否成功. 如果失败, 它将返回 NULL.
	 */
	if (!mem_base)
		/*
		 * 如果映射失败, 返回一个内嵌了 -EINVAL 错误码的指针.
		 */
		return ERR_PTR(-EINVAL);

	/*
	 * 为 dma_coherent_mem 管理结构体本身分配内存.
	 * kzalloc 会分配内存并将其初始化为零.
	 * GFP_KERNEL 标志表示这是一个常规的内核内存分配, 在需要时可以睡眠.
	 */
	dma_mem = kzalloc(sizeof(struct dma_coherent_mem), GFP_KERNEL);
	/*
	 * 检查内存分配是否成功.
	 */
	if (!dma_mem)
		/*
		 * 如果失败, 跳转到 out_unmap_membase 标签去执行清理操作.
		 */
		goto out_unmap_membase;
	/*
	 * 为内存池的分配位图分配内存. 位图的大小由 pages 决定.
	 * bitmap_zalloc 会分配内存并将其清零, 表示初始时所有页都是空闲的.
	 */
	dma_mem->bitmap = bitmap_zalloc(pages, GFP_KERNEL);
	/*
	 * 检查位图内存分配是否成功.
	 */
	if (!dma_mem->bitmap)
		/*
		 * 如果失败, 跳转到 out_free_dma_mem 标签去执行清理操作.
		 */
		goto out_free_dma_mem;

	/*
	 * 将 memremap 返回的内核可用基地址保存到管理结构体中.
	 */
	dma_mem->virt_base = mem_base;
	/*
	 * 将设备可见的DMA基地址保存到管理结构体中.
	 */
	dma_mem->device_base = device_addr;
	/*
	 * 计算并保存物理基地址对应的页帧号(Page Frame Number). PFN_DOWN 是一个宏, 等价于 phys_addr / PAGE_SIZE.
	 */
	dma_mem->pfn_base = PFN_DOWN(phys_addr);
	/*
	 * 将内存池的总大小 (以页为单位) 保存到管理结构体中.
	 */
	dma_mem->size = pages;
	/*
	 * 保存 use_dma_pfn_offset 标志.
	 */
	dma_mem->use_dev_dma_pfn_offset = use_dma_pfn_offset;
	/*
	 * 初始化管理结构体中的自旋锁.
	 * 这个锁将用于保护对位图的并发访问, 在单核抢占式内核中, 它可以防止在修改位图时被其他任务抢占.
	 */
	spin_lock_init(&dma_mem->spinlock);

	/*
	 * 所有初始化步骤都成功完成, 返回指向 dma_mem 管理结构体的指针.
	 */
	return dma_mem;

/*
 * 以下是初始化失败时的错误处理路径 (通过 goto 跳转).
 */
out_free_dma_mem:
	/*
	 * 如果位图分配失败, 释放之前为 dma_mem 结构体分配的内存.
	 */
	kfree(dma_mem);
out_unmap_membase:
	/*
	 * 释放由 memremap 创建的映射.
	 */
	memunmap(mem_base);
	/*
	 * 打印一条错误日志, 报告初始化失败.
	 * %pa 用于打印物理地址. %zd 用于打印 size_t 类型.
	 */
	pr_err("Reserved memory: failed to init DMA memory pool at %pa, size %zd MiB\n",
		&phys_addr, size / SZ_1M);
	/*
	 * 返回一个内嵌了 -ENOMEM (内存不足) 错误码的指针.
	 */
	return ERR_PTR(-ENOMEM);
}

Reserved Memory: 创建用于共享DMA池的预留内存区域

此代码片段是Linux内核中用于处理在设备树 (Device Tree) 中定义的 预留内存 (reserved memory) 的驱动程序。它的核心作用是识别设备树中被标记为 "shared-dma-pool" (共享DMA池) 的内存区域, 并将其初始化为一个专用的、物理地址连续的内存池, 供系统中的多个设备进行DMA (Direct Memory Access) 操作。

c 复制代码
/*
 * 此文件提供对设备树中定义的预留内存区域的支持.
 */

/*
 * 整个代码块只有在内核配置了 CONFIG_OF_RESERVED_MEM 时才会被编译.
 * 这个配置项开启了对设备树中 "reserved-memory" 节点的支持.
 */
#ifdef CONFIG_OF_RESERVED_MEM
/*
 * 下面的代码块只有在内核配置了 CONFIG_DMA_GLOBAL_POOL 时才会被编译.
 * 这个配置项用于支持一个全局的、默认的DMA内存池.
 */
#ifdef CONFIG_DMA_GLOBAL_POOL
/*
 * 定义一个静态的物理地址类型变量 dma_reserved_default_memory_base.
 * 它用于存储从设备树中找到的 "默认" DMA预留内存区的物理起始地址.
 * __initdata 属性告诉链接器将这个变量放在 .init.data 段中, 这段内存在内核初始化完成后会被释放, 以节省内存.
 */
static phys_addr_t dma_reserved_default_memory_base __initdata;
/*
 * 定义一个静态的物理地址类型变量 dma_reserved_default_memory_size.
 * 它用于存储默认DMA预留内存区的大小.
 * 同样, 它也是 __initdata.
 */
static phys_addr_t dma_reserved_default_memory_size __initdata;
#endif

static int dma_assign_coherent_memory(struct device *dev,
				      struct dma_coherent_mem *mem)
{
	if (!dev)
		return -ENODEV;

	if (dev->dma_mem)
		return -EBUSY;

	dev->dma_mem = mem;
	return 0;
}

/*
 * rmem_dma_device_init: 当一个设备需要使用这个预留内存池时, 该回调函数被调用.
 *
 * @rmem: 指向 struct reserved_mem 的指针, 代表这个预留内存区域的实例.
 * @dev:  指向 struct device 的指针, 代表请求使用此内存池的设备.
 * @return: 0 表示成功, 负值错误码表示失败.
 */
static int rmem_dma_device_init(struct reserved_mem *rmem, struct device *dev)
{
	/*
	 * 检查 rmem->priv 私有数据指针是否为空.
	 * priv 指针用于存储这个内存池的管理结构. 如果它为空, 说明这是第一次有设备请求使用该内存池, 需要进行初始化.
	 */
	if (!rmem->priv) {
		/*
		 * 定义一个指向 dma_coherent_mem 结构体的指针 mem.
		 * dma_coherent_mem 是用于管理一块相干DMA内存区域的核心结构.
		 */
		struct dma_coherent_mem *mem;

		/*
		 * 调用 dma_init_coherent_memory 来初始化这块预留内存, 将其变成一个可用的DMA内存池.
		 * @ rmem->base: 内存区域的物理起始地址.
		 * @ rmem->base: CPU可见的地址 (在无MMU系统中, 等同于物理地址).
		 * @ rmem->size: 内存区域的大小.
		 * @ true: 这个布尔值表示此内存区域应该被当作一个专门的 "设备" 来管理.
		 * 如果初始化失败, 它会返回一个错误指针.
		 */
		mem = dma_init_coherent_memory(rmem->base, rmem->base,
					       rmem->size, true);
		/*
		 * IS_ERR 宏用于检查返回的指针是否是一个错误码.
		 */
		if (IS_ERR(mem))
			/*
			 * PTR_ERR 宏用于从错误指针中提取出真正的负值错误码.
			 * 返回这个错误码, 表示初始化失败.
			 */
			return PTR_ERR(mem);
		/*
		 * 如果初始化成功, 将返回的 dma_coherent_mem 管理结构体指针存入 rmem->priv.
		 * 这样, 下次再有设备请求时, 就不需要重新初始化了.
		 */
		rmem->priv = mem;
	}
	/*
	 * 调用 dma_assign_coherent_memory, 将这个已经初始化好的DMA内存池(rmem->priv)分配给请求的设备(dev).
	 * 这个函数会将 dev->dma_mem 指向这个共享的内存池.
	 * 从此, 当这个设备调用 dma_alloc_coherent() 时, 就会从这个共享池中分配内存.
	 */
	dma_assign_coherent_memory(dev, rmem->priv);
	/*
	 * 返回 0 表示成功.
	 */
	return 0;
}

/*
 * rmem_dma_device_release: 当一个设备不再需要使用这个预留内存池时, 该回调函数被调用.
 *
 * @rmem: 指向预留内存区域的实例.
 * @dev:  指向正在释放此内存池的设备.
 */
static void rmem_dma_device_release(struct reserved_mem *rmem,
				    struct device *dev)
{
	/*
	 * 检查设备指针是否有效.
	 */
	if (dev)
		/*
		 * 将设备的 dma_mem 指针清空.
		 * 这就解除了该设备与共享DMA内存池的关联.
		 * 注意: 这里并不会释放内存池本身, 因为它可能还在被其他设备使用.
		 */
		dev->dma_mem = NULL;
}

/*
 * 定义一个静态的、常量 reserved_mem_ops 结构体实例.
 * 这个结构体包含了一系列操作函数(回调函数)的指针, 用于管理这类预留内存.
 */
static const struct reserved_mem_ops rmem_dma_ops = {
	/*
	 * 将 .device_init 操作指向我们上面实现的 rmem_dma_device_init 函数.
	 */
	.device_init	= rmem_dma_device_init,
	/*
	 * 将 .device_release 操作指向我们上面实现的 rmem_dma_device_release 函数.
	 */
	.device_release	= rmem_dma_device_release,
};

/*
 * rmem_dma_setup: 在内核启动时, 当扫描到设备树中匹配的预留内存节点时, 该初始化函数被调用.
 *
 * @rmem: 指向从设备树信息中创建的 reserved_mem 实例.
 * @return: 0 表示设置成功, 负值错误码表示失败.
 */
static int __init rmem_dma_setup(struct reserved_mem *rmem)
{
	/*
	 * 获取该预留内存区域在扁平设备树(flat device tree)中的节点偏移量.
	 */
	unsigned long node = rmem->fdt_node;

	/*
	 * 检查设备树节点中是否包含 "reusable" 属性.
	 * shared-dma-pool 不应该是 "reusable" 的, 因为它由DMA子系统专门管理. "reusable" 意味着内存可以被常规分配器回收.
	 * 如果存在这个属性, 说明设备树配置错误, 返回 -EINVAL (无效参数).
	 */
	if (of_get_flat_dt_prop(node, "reusable", NULL))
		return -EINVAL;

/*
 * 仅在为ARM架构编译时检查.
 */
#ifdef CONFIG_ARM
	/*
	 * 检查设备树节点中是否不包含 "no-map" 属性.
	 * 对于DMA内存池, 强制要求设置 "no-map" 属性.
	 * 这会阻止内核为这块内存创建常规的虚拟地址映射, 这对于无MMU系统是正确的行为, 对于有MMU系统也能避免混淆.
	 * 如果没有 "no-map", 打印错误信息并返回失败.
	 */
	if (!of_get_flat_dt_prop(node, "no-map", NULL)) {
		pr_err("Reserved memory: regions without no-map are not yet supported\n");
		return -EINVAL;
	}
#endif

/*
 * 仅在配置了 CONFIG_DMA_GLOBAL_POOL 时检查.
 */
#ifdef CONFIG_DMA_GLOBAL_POOL
	/*
	 * 检查设备树节点中是否包含 "linux,dma-default" 属性.
	 * 这个属性标记此内存区域为系统默认的DMA相干内存池.
	 */
	if (of_get_flat_dt_prop(node, "linux,dma-default", NULL)) {
		/*
		 * 如果 dma_reserved_default_memory_size 已经非零, 说明在设备树中定义了多个默认区域.
		 * 这是一个配置错误, 使用 WARN 宏打印一个警告信息.
		 */
		WARN(dma_reserved_default_memory_size,
		     "Reserved memory: region for default DMA coherent area is redefined\n");
		/*
		 * 将这个预留内存区域的基地址和大小保存到全局变量中.
		 */
		dma_reserved_default_memory_base = rmem->base;
		dma_reserved_default_memory_size = rmem->size;
	}
#endif

	/*
	 * 将这个预留内存区域的操作集设置为我们前面定义的 rmem_dma_ops.
	 * 这样内核就知道当设备要使用或释放这块内存时应该调用哪些函数.
	 */
	rmem->ops = &rmem_dma_ops;
	/*
	 * 打印一条内核信息, 宣告已成功创建了一个DMA内存池.
	 * @ %pa: 内核打印格式符, 用于打印物理地址.
	 * @ &rmem->base: 要打印的物理地址的指针.
	 * @ (unsigned long)rmem->size / SZ_1M: 计算大小并转换为MB单位.
	 */
	pr_info("Reserved memory: created DMA memory pool at %pa, size %ld MiB\n",
		&rmem->base, (unsigned long)rmem->size / SZ_1M);
	/*
	 * 返回0, 表示初始化成功.
	 */
	return 0;
}
/*
 * RESERVEDMEM_OF_DECLARE 是一个宏, 用于将一个设备树 `compatible` 字符串和一个初始化函数关联起来.
 * @ dma: 声明的唯一标识符.
 * @ "shared-dma-pool": 这是设备树节点中 `compatible` 属性的值. 当内核找到一个 `compatible = "shared-dma-pool"` 的预留内存节点时...
 * @ rmem_dma_setup: ...它就会调用 rmem_dma_setup 这个函数来对该节点进行处理.
 * 这就是驱动和设备树之间的 "绑定" 机制.
 */
RESERVEDMEM_OF_DECLARE(dma, "shared-dma-pool", rmem_dma_setup);
#endif /* CONFIG_OF_RESERVED_MEM */

dma_init_global_coherent 初始化一个全局的相干DMA内存区域

c 复制代码
static struct dma_coherent_mem *dma_coherent_default_memory __ro_after_init;
/*
 * 仅当内核配置了 CONFIG_DMA_GLOBAL_POOL (支持全局DMA池) 时, 才编译以下代码.
 */
#ifdef CONFIG_DMA_GLOBAL_POOL
/*
 * 静态全局变量, 用于存储从设备树中解析出的 "默认" DMA预留内存区域的物理起始地址.
 * __initdata 属性告诉链接器将其放入 .init.data 段, 这段内存在内核初始化完成后会被释放掉.
 */
static phys_addr_t dma_reserved_default_memory_base __initdata;
/*
 * 静态全局变量, 用于存储默认DMA预留内存区域的大小.
 */
static phys_addr_t dma_reserved_default_memory_size __initdata;
#endif
/*
 * dma_init_global_coherent: 初始化一个全局的相干DMA内存区域.
 * 这个函数可以被其他代码直接调用, 以编程方式设置默认的DMA池.
 *
 * @phys_addr: 要用作内存池的物理起始地址.
 * @size:      内存池的大小.
 * @return:    0 表示成功, 负值错误码表示失败.
 */
int dma_init_global_coherent(phys_addr_t phys_addr, size_t size)
{
	/*
	 * 定义一个指向 dma_coherent_mem 管理结构体的指针 mem.
	 */
	struct dma_coherent_mem *mem;

	/*
	 * 调用 dma_init_coherent_memory 函数, 将传入的物理内存区域转换成一个DMA内存池.
	 * @ phys_addr: 物理起始地址.
	 * @ phys_addr: 设备看到的地址 (在无MMU和IOMMU的STM32上, 等同于物理地址).
	 * @ size:      大小.
	 * @ true:      表示此内存区域应被当作一个独立的 "设备" 来管理.
	 * 这个函数会负责创建位图等管理数据结构.
	 */
	mem = dma_init_coherent_memory(phys_addr, phys_addr, size, true);
	/*
	 * 检查 dma_init_coherent_memory 的返回值. IS_ERR() 用于判断返回的指针是否是一个错误码.
	 */
	if (IS_ERR(mem))
		/*
		 * 如果是错误, 使用 PTR_ERR() 从指针中提取出实际的负值错误码并返回.
		 */
		return PTR_ERR(mem);
	/*
	 * 这是最关键的一步: 将初始化好的DMA内存池管理结构赋值给全局指针 dma_coherent_default_memory.
	 * 从此以后, 这个全局指针就指向了系统默认的相干DMA内存池.
	 */
	dma_coherent_default_memory = mem;
	/*
	 * 打印一条内核信息日志, 宣告默认的相干DMA区域已设置成功.
	 */
	pr_info("DMA: default coherent area is set\n");
	/*
	 * 返回 0 表示成功.
	 */
	return 0;
}
/*
 * dma_init_reserved_memory: 在内核启动时调用的初始化函数.
 * 它的作用是检查是否存在通过设备树预留的默认DMA内存, 如果存在, 就初始化它.
 */
static int __init dma_init_reserved_memory(void)
{
	/*
	 * 检查 dma_reserved_default_memory_size 是否为0.
	 * 如果为0, 说明在设备树解析阶段没有找到标记为 "linux,dma-default" 的预留内存.
	 * 在这种情况下, 无法创建默认池.
	 */
	if (!dma_reserved_default_memory_size)
		/*
		 * 返回 -ENOMEM (内存不足), 这里更准确的含义是 "所需资源不可用".
		 */
		return -ENOMEM;
	/*
	 * 如果找到了预留内存的信息, 就调用 dma_init_global_coherent 函数,
	 * 使用从设备树中获取的地址和大小来初始化全局默认DMA池.
	 */
	return dma_init_global_coherent(dma_reserved_default_memory_base,
					dma_reserved_default_memory_size);
}
/*
 * 使用 core_initcall() 宏将 dma_init_reserved_memory 函数注册为一个内核核心初始化函数.
 * 这确保了它会在内核启动过程中的一个合适的、较早的阶段被调用,
 * 从而保证默认DMA池在任何设备驱动尝试使用它之前就已经准备就绪.
 */
core_initcall(dma_init_reserved_memory);

dma_alloc_from_dev_coherent: Per-Device DMA一致性内存池的分配

本代码片段展示了dma_alloc_coherent函数中的最高优先级分配路径 。其核心功能是尝试从一个预留的、与特定设备关联的DMA一致性内存池中进行分配。这是一个为有特殊性能或硬件限制的设备设计的优化机制。它使用一个简单的、基于**位图(bitmap)**的分配器来管理这块预留的内存,实现了快速、无锁(指不使用互斥锁或信号量)的分配与释放。

实现原理分析

该机制的本质是一个专用的、小型的内存管理器,它在一块预先分配好的、物理连续的DMA适用内存上进行操作。

  1. 预留内存池 (dma_coherent_mem):

    • 这个机制的前提是系统在启动时,通过设备树的reserved-memory节点或CMA(Contiguous Memory Allocator)为某个特定设备预留了一块物理连续的内存。
    • 驱动在probe阶段会调用dma_declare_coherent_memory来将这块预留内存注册到DMA子系统中,形成一个struct dma_coherent_mem实例,并与struct device关联起来。
    • dma_coherent_mem结构体中包含了这块内存的CPU虚拟基地址(virt_base)、DMA总线基地址(device_base)、总大小(size)以及一个用于管理这块内存的位图bitmap)。
  2. 分配流程 (__dma_alloc_from_coherent):

    • 原子性 : 整个分配过程由一个自旋锁spin_lock_irqsave保护,确保了在查找和标记位图时不会被中断或其他任务打断,保证了操作的原子性。
    • 位图分配器 : 这是核心算法。
      • get_order(size): 将请求的字节大小转换为2的幂次的页数(order),因为内存是以页(PAGE_SIZE)为单位管理的。
      • bitmap_find_free_region(mem->bitmap, mem->size, order): 这是位图分配器的关键。它会在mem->bitmap中搜索一个连续的、长度为2^order个"0"位(代表空闲页)的区域。如果找到,它会原子地将这些位置1(标记为已使用),并返回起始的页号(pageno)。
    • 地址计算 :
      • *dma_handle = ... + (pageno << PAGE_SHIFT): 计算DMA控制器应该使用的总线地址。它等于内存池的DMA基地址加上分配区域的偏移量。
      • ret = mem->virt_base + (pageno << PAGE_SHIFT): 计算CPU应该使用的虚拟地址。它等于内存池的CPU虚拟基地址加上相同的偏移量。
  3. 接口与信令 (dma_alloc_from_dev_coherent):

    • 这个函数是一个封装,它首先通过dev_get_coherent_memory(dev)检查设备是否有关联的内存池。
    • 它的返回值非常关键:
      • 返回0: 表示该设备没有 专属内存池,或者分配失败。这会告诉上层调用者(dma_alloc_attrs):"我没处理,请你继续尝试用通用的方法(如dma_direct_alloc)去分配。"
      • 返回1: 表示该设备专属内存池,并且已经成功地(或失败地)完成了分配尝试。这会告诉上层调用者:"我已经处理了,无论成功与否,你都应该停止并使用我的结果。"
  4. 释放流程 (__dma_release_from_coherent):

    • 它首先进行一个边界检查,确认要释放的地址vaddr确实属于这个内存池。
    • page = (vaddr - mem->virt_base) >> PAGE_SHIFT: 根据CPU虚拟地址反向计算出它在内存池中的起始页号。
    • bitmap_release_region(mem->bitmap, page, order): 在自旋锁的保护下,将位图中对应的区域从"1"清零,标记为可用。

特定场景分析:单核、无MMU的STM32H750平台

硬件交互
  1. 内存池的来源 : 在STM32H750上,这块专属的内存池通常是在设备树(DTS)的/reserved-memory/节点下定义的。例如,可以预留一块位于SRAM中的内存专供某个DMA密集型的外设(如以太网或摄像头接口)使用。

    dts 复制代码
    reserved-memory {
        #address-cells = <1>;
        #size-cells = <1>;
        eth_dma_mem: eth_dma_pool@C0000000 {
            compatible = "shared-dma-pool";
            reg = <0xC0000000 0x10000>; // 64KB pool
            no-map;
        };
    };
    
    ethernet-device {
        memory-region = <&eth_dma_mem>;
    };
  2. 一致性: 这块预留的SRAM本身就是非缓存的,或者在系统启动时被MPU配置为非缓存区域。因此,从中分配出的内存在硬件上自然就是"一致的",无需CPU进行缓存操作。

单核环境影响
  • spin_lock_irqsave: 在单核处理器上,这个锁的主要作用是禁用本地中断 。这至关重要,因为一个DMA操作完成的中断服务程序(ISR)可能会立即尝试释放或重新分配同一块DMA内存。如果没有锁,中断就会打断正在进行的bitmap_find_free_regionbitmap_release_region操作,导致位图被破坏,引发内存泄露或重复分配。
无MMU影响
  • 地址等同 : 在无MMU的系统中,virt_base(CPU虚拟基地址)和dma_get_device_base返回的DMA基地址是完全相同的物理地址。代码中区分这两个地址是为了保持对有IOMMU系统的可移植性。
  • 位图分配器: 位图分配器是一种纯粹的软件算法,它管理的是页的索引号,与内存地址是物理的还是虚拟的无关。因此,它在无MMU系统上可以完美工作。

代码分析

c 复制代码
static inline struct dma_coherent_mem *dev_get_coherent_memory(struct device *dev)
{
	if (dev && dev->dma_mem)
		return dev->dma_mem;
	return NULL;
}

/**
 * @brief 从一个具体的一致性内存池中分配内存。
 * @param dev 请求内存的设备。
 * @param mem 指向一致性内存池的结构。
 * @param size 请求的字节数。
 * @param dma_handle 用于返回DMA总线地址的指针。
 * @return void* 成功则返回CPU虚拟地址,失败返回NULL。
 */
static void *__dma_alloc_from_coherent(struct device *dev,
				       struct dma_coherent_mem *mem,
				       ssize_t size, dma_addr_t *dma_handle)
{
	int order = get_order(size); /* 将字节大小转换为页阶。 */
	unsigned long flags;
	int pageno;
	void *ret;

	/* 锁住内存池并禁用中断,以保证原子性。 */
	spin_lock_irqsave(&mem->spinlock, flags);

	/* 检查请求大小是否超过池的总大小。 */
	if (unlikely(size > ((dma_addr_t)mem->size << PAGE_SHIFT)))
		goto err;

	/* 在位图中查找一个足够大的空闲区域。 */
	pageno = bitmap_find_free_region(mem->bitmap, mem->size, order);
	if (unlikely(pageno < 0))
		goto err; /* 未找到,内存不足。 */

	/*
	 * 在一致性区域中找到了内存。
	 */
	/* 计算DMA总线地址:池的DMA基地址 + 偏移量。 */
	*dma_handle = dma_get_device_base(dev, mem) +
			((dma_addr_t)pageno << PAGE_SHIFT);
	/* 计算CPU虚拟地址:池的CPU基地址 + 偏移量。 */
	ret = mem->virt_base + ((dma_addr_t)pageno << PAGE_SHIFT);
	spin_unlock_irqrestore(&mem->spinlock, flags);
	memset(ret, 0, size); /* 将分配的内存清零。 */
	return ret;
err:
	spin_unlock_irqrestore(&mem->spinlock, flags);
	return NULL;
}

/**
 * @brief 从设备专属的一致性内存池中分配内存。
 * @param dev 请求内存的设备。
 * @param size 请求的字节数。
 * @param dma_handle 用于返回DMA总线地址。
 * @param ret 用于返回CPU虚拟地址。
 * @return int 如果处理了请求(无论成功与否)则返回1,
 *         如果应继续尝试通用分配则返回0。
 */
int dma_alloc_from_dev_coherent(struct device *dev, ssize_t size,
		dma_addr_t *dma_handle, void **ret)
{
	/* 获取与此设备关联的专属内存池。 */
	struct dma_coherent_mem *mem = dev_get_coherent_memory(dev);

	/* 如果没有专属内存池,返回0,通知调用者继续。 */
	if (!mem)
		return 0;

	/* 调用核心分配函数。 */
	*ret = __dma_alloc_from_coherent(dev, mem, size, dma_handle);
	/* 返回1,通知调用者我们已经处理了请求。 */
	return 1;
}

/**
 * @brief 从一个具体的一致性内存池中释放内存。
 * @param mem 指向一致性内存池的结构。
 * @param order 要释放的页阶。
 * @param vaddr 要释放的CPU虚拟地址。
 * @return int 如果地址属于此池并成功释放则返回1,否则返回0。
 */
static int __dma_release_from_coherent(struct dma_coherent_mem *mem,
				       int order, void *vaddr)
{
	/* 检查vaddr是否在此内存池的地址范围内。 */
	if (mem && vaddr >= mem->virt_base && vaddr <
		   (mem->virt_base + ((dma_addr_t)mem->size << PAGE_SHIFT))) {
		/* 计算vaddr对应的页号。 */
		int page = (vaddr - mem->virt_base) >> PAGE_SHIFT;
		unsigned long flags;

		spin_lock_irqsave(&mem->spinlock, flags);
		/* 在位图中释放对应的区域(将位置0)。 */
		bitmap_release_region(mem->bitmap, page, order);
		spin_unlock_irqrestore(&mem->spinlock, flags);
		return 1;
	}
	return 0;
}

/**
 * @brief 将内存释放回设备专属的一致性内存池。
 * @param dev 分配内存的设备。
 * @param order 要释放的页阶。
 * @param vaddr 要释放的CPU虚拟地址。
 * @return int 如果内存被正确释放则返回1,
 *         如果调用者应继续尝试通用释放则返回0。
 */
int dma_release_from_dev_coherent(struct device *dev, int order, void *vaddr)
{
	struct dma_coherent_mem *mem = dev_get_coherent_memory(dev);

	return __dma_release_from_coherent(mem, order, vaddr);
}
相关推荐
cooldream20098 小时前
Vim 报错 E325:swap 文件冲突的原理、处理流程与彻底避免方案
linux·编辑器·vim
i建模8 小时前
在 Rocky Linux 上安装轻量级的 XFCE 桌面
linux·运维·服务器
lxl13078 小时前
学习C++(5)运算符重载+赋值运算符重载
学习
若风的雨9 小时前
WC (Write-Combining) 内存类型优化原理
linux
YMWM_9 小时前
不同局域网下登录ubuntu主机
linux·运维·ubuntu
ruxshui9 小时前
个人笔记: 星环Inceptor/hive普通分区表与范围分区表核心技术总结
hive·hadoop·笔记
zmjjdank1ng9 小时前
restart与reload的区别
linux·运维
哼?~9 小时前
进程替换与自主Shell
linux
慾玄9 小时前
渗透笔记总结
笔记
AutumnorLiuu9 小时前
C++并发编程学习(一)——线程基础
开发语言·c++·学习