【study】DMA内存申请--dma_alloc_coherent 及 寄存器 与 内存

DMA内存管理
本文详细介绍了在Linux内核中用于DMA操作的内存管理方法,重点解析了dma_alloc_coherent函数的工作原理及其在x86和PowerPC平台的具体实现差异。
在项目驱动过程中会经常用到dma传输数据,而dma需要的内存有自己的特点,一般认为需要物理地址连续,并且内存是不可cache的,在linux内核中提供一个供dma所需内存的申请函数dma_alloc_coherent. 如下所述:
dma_alloc_coherent()

dma_alloc_coherent() -- 获取物理页,并将该物理页的总线地址保存于dma_handle,返回该物理页的虚拟地址

DMA映射建立了一个新的结构类型---------dma_addr_t来表示总线地址。dma_addr_t类型的变量对驱动程序是不透明的;唯一允许的操作是将它们传递给DMA支持例程以及设备本身。作为一个总线地址,如果CPU直接使用了dma_addr_t,将会导致发生不可预期的后果!

一致性DMA映射存在与驱动程序的生命周期中,它的缓冲区必须可同时被CPU和外围设备访问!因此一致性映射必须保存在一致性缓存中。建立和使用一致性映射的开销是很大的!

                                                                              (以上红色文字摘录在LDD3)

void *
dma_alloc_coherent (struct device *dev,        size_t size,
                   dma_addr_t    *dma_handle, gfp_t gfp)
{
    void *ret;
    if (!dev || *dev->dma_mask >= 0xffffffffUL)
        gfp &= ~GFP_DMA;
    ret = (void *) __get_free_pages (gfp, get_order(size)); //(1)
    if (ret) {
        memset(ret, 0, size);
        *dma_handle =  virt_to_bus (ret);                   //(2)
    }
    return ret;
}

(1) 将size转换成order, 即2^order
(2) 将虚拟地址ret转换成总线地址

这个函数是一个平台相关的函数,以上是在x86平台的实现细节,从这里我们可以看到该函数返回值为linux 内核线性地址,所以对于驱动开发过程的mmap函数实现提供了便利。
但是在powerpc平台却不是这样,笔者就曾经遇到在将pci驱动从x86平台移植到powerpc平台时出现问题。
首先我们来先看一下两个平台对于dma内存的处理。
x86:
     linux内存区域分为DMA区域,Normal内存区域与高端内存区域,高端内存区域为当物理内存高于768M时使用,一般DMA区域为16M,这段空间由操作系统预留。DMA区域与Normal区域全部使用线性映射,采用逻辑地址使用,高端内存使用内核虚拟地址。其中内核空间的分部为:
物理区--8M隔离--vmalloc区--8k隔离--4M的高端映射区--固定映射区--128k

powerpc:
    本节采用freescale的mpc5121芯片为例,内核没有采用Normal内存区域,只使用ZONE_DMA和ZONE_HIMEM两种类型的空间,其中ZONE_DMA存放低端内存, ZONE_HIMEN存放高端内存,整个内存不在采用逻辑地址这一概念。所以基于逻辑地址的操作没有可移植性。

下面看下具体的区别:
void *  __dma_alloc_coherent(size_t size, dma_addr_t *handle, gfp_t gfp)
{
          //物理空间页的申请
          page = alloc_pages(gfp, order);
         
         //对物理空间进行清零cache
         { 
                       unsigned long kaddr = (unsigned long)page_address(page);                 
                       memset(page_address(page), 0, size);
                       flush_dcache_range(kaddr, kaddr + size);
          }

         //申请虚拟空间
         c = vm_region_alloc(&consistent_head, size,   gfp &  
                                     ~(__GFP_DMA | __GFP_HIGHMEM));

       //实现虚拟地址与物理地址映射
       if (c) {
                 unsigned long vaddr = c->vm_start;
                 pte_t *pte = consistent_pte + CONSISTENT_OFFSET(vaddr);
                 struct page *end = page + (1 << order);

                 split_page(page, order);

                 /*
                  * Set the \"dma handle\"
                  */
                 *handle = page_to_bus(page);

                do {
                         BUG_ON(!pte_none(*pte));

                         SetPageReserved(page);
                         set_pte_at(&init_mm, vaddr,
                                   pte, mk_pte(page, pgprot_noncached(PAGE_KERNEL)));
                         page++;
                         pte++;
                         vaddr += PAGE_SIZE;
                 } while (size -= PAGE_SIZE);

     // 返回值为内核虚拟地址。

      return (void *)c->vm_start


1. I/O 端口和 I/O 内存

每个外设都是通过读写它的寄存器来控制. 大部分时间一个设备有几个寄存器, 并且在连续地址存取它们, 或者在内存地址空间或者在 I/O 地址空间.

在硬件级别上, 内存区和 I/O 区域没有概念上的区别: 它们都是通过在地址总线和控制总线上发出电信号来存取(即, 读写信号)[32]并且读自或者写到数据总线.

但是一些 CPU 制造商在他们的芯片上实现了一个单个地址空间, 有人认为外设不同于内存, 因此, 应该有一个分开的地址空间. 一些处理器(最有名的是 x86 家族)有分开的读和写电线给 I/O 端口和特殊的 CPU 指令来存取端口.

因为外设被建立来适合一个外设总线, 并且大部分流行的 I/O 总线成型在个人计算机上, 即便那些没有单独地址空间给 I/O 端口的处理器, 也必须在存取一些特殊设备时伪装读写端口, 常常利用外部的芯片组或者 CPU 核的额外电路. 后一个方法在用在嵌入式应用的小处理器中常见.

由于同样的理由, Linux 在所有它运行的计算机平台上实现了 I/O 端口的概念, 甚至在那些 CPU 实现一个单个地址空间的平台上. 端口存取的实现有时依赖特殊的主机制造和型号( 因为不同的型号使用不同的芯片组来映射总线传送到内存地址空间).

即便外设总线有一个单独的地址空间给 I/O 端口, 不是所有的设备映射它们的寄存器到 I/O 端口. 虽然对于 ISA 外设板使用 I/O 端口是普遍的, 大部分 PCI 设备映射寄存器到一个内存地址区. 这种 I/O 内存方法通常是首选的, 因为它不需要使用特殊目的处理器指令; CPU 核存取内存更加有效, 并且编译器当存取内存时有更多自由在寄存器分配和寻址模式的选择上.

1.1. I/O 寄存器和常规内存

不管硬件寄存器和内存之间的强相似性, 存取 I/O 寄存器的程序员必须小心避免被 CPU(或者编译器)优化所戏弄, 它可能修改希望的 I/O 行为.

I/O 寄存器和 RAM 的主要不同是 I/O 操作有边际效果, 而内存操作没有: 一个内存写的唯一效果是存储一个值到一个位置, 并且一个内存读返回最近写到那里的值. 因为内存存取速度对 CPU 性能是至关重要的, 这种无边际效果的情况已被多种方式优化: 值被缓存, 并且 读/写指令被重编排.

编译器能够缓存数据值到 CPU 寄存器而不写到内存, 并且即便它存储它们, 读和写操作都能够在缓冲内存中进行而不接触物理 RAM. 重编排也可能在编译器级别和在硬件级别都发生: 常常一个指令序列能够执行得更快, 如果它以不同于在程序文本中出现的顺序来执行, 例如, 为避免在 RISC 流水线中的互锁. 在CISC 处理器, 要花费相当数量时间的操作能够和其他的并发执行, 更快的.

当应用于传统内存时(至少在单处理器系统)这些优化是透明和有益的, 但是它们可能对正确的 I/O 操作是致命的, 因为它们干扰了那些"边际效果", 这是主要的原因为什么一个驱动存取 I/O 寄存器. 处理器无法预见这种情形, 一些其他的操作(在一个独立处理器上运行, 或者发生在一个 I/O 控制器的事情)依赖内存存取的顺序. 编译器或者 CPU 可能只尽力胜过你并且重编排你请求的操作; 结果可能是奇怪的错误而非常难于调试. 因此, 一个驱动必须确保没有进行缓冲并且在存取寄存器时没有发生读或写的重编排.

硬件缓冲的问题是最易面对的:底层的硬件已经配置(或者自动地或者通过 Linux 初始化代码)成禁止任何硬件缓冲, 当存取 I/O 区时(不管它们是内存还是端口区域).

对编译器优化和硬件重编排的解决方法是安放一个内存屏障在必须以一个特殊顺序对硬件(或者另一个处理器)可见的操作之间. Linux 提供 4 个宏来应对可能的排序需要:

#include <linux/kernel.h>
void barrier(void)

这个函数告知编译器插入一个内存屏障但是对硬件没有影响. 编译的代码将所有的当前改变的并且驻留在 CPU 寄存器的值存储到内存, 并且后来重新读取它们当需要时. 对屏障的调用阻止编译器跨越屏障的优化, 而留给硬件自由做它的重编排.

#include <asm/system.h>
void rmb(void);
void read_barrier_depends(void);
void wmb(void);
void mb(void);

这些函数插入硬件内存屏障在编译的指令流中; 它们的实际实例是平台相关的. 一个 rmb ( read memory barrier) 保证任何出现于屏障前的读在执行任何后续读之前完成. wmb 保证写操作中的顺序, 并且 mb 指令都保证. 每个这些指令是一个屏障的超集.

read_barrier_depends 是读屏障的一个特殊的, 弱些的形式. 而 rmb 阻止所有跨越屏障的读的重编排, read_barrier_depends 只阻止依赖来自其他读的数据的读的重编排. 区别是微小的, 并且它不在所有体系中存在. 除非你确切地理解做什么, 并且你有理由相信, 一个完整的读屏障确实是一个过度地性能开销, 你可能应当坚持使用 rmb.

#define isb() __asm__ __volatile__ ("isb" : : : "memory")
#define dsb() __asm__ __volatile__ ("dsb" : : : "memory")
#define dmb() __asm__ __volatile__ ("dsb" : : : "memory")
#define mb()		do { dsb(); outer_sync(); } while (0)
#define rmb()		dsb()
#define wmb()		mb()
#define smp_mb()	dmb()
#define smp_rmb()	dmb()
#define smp_wmb()	dmb()

表4.27 隔离指令

指令名

功能描述

DMB

数据存储器隔离。DMB 指令保证: 仅当所有在它前面的存储器访问操作

都执行完毕后,才提交(commit)在它后面的存储器访问操作。

DSB

数据同步隔离。比 DMB 严格: 仅当所有在它前面的存储器访问操作

都执行完毕后,才执行在它后面的指令(亦即任何指令都要等待存储器访 问操作——译者注)

ISB

指令同步隔离。最严格:它会清洗流水线,以保证所有它前面的指令都执

行完毕之后,才执行它后面的指令。




void smp_rmb(void);
void smp_read_barrier_depends(void);
void smp_wmb(void);
void smp_mb(void);

屏障的这些版本仅当内核为 SMP 系统编译时插入硬件屏障; 否则, 它们都扩展为一个简单的屏障调用.

在一个设备驱动中一个典型的内存屏障的用法可能有这样的形式:

writel(dev->registers.addr, io_destination_address);
writel(dev->registers.size, io_size);
writel(dev->registers.operation, DEV_READ);
wmb();
writel(dev->registers.control, DEV_GO);

在这种情况, 是重要的, 确保所有的控制一个特殊操作的设备寄存器在告诉它开始前已被正确设置. 内存屏障强制写以需要的顺序完成.

因为内存屏障影响性能, 它们应当只用在确实需要它们的地方. 屏障的不同类型也有不同的性能特性, 因此值得使用最特定的可能类型. 例如, 在 x86 体系上, wmb() 目前什么都不做, 因为写到处理器外不被重编排. 但是, 读被重编排, 因此 mb() 被 wmb() 慢.

值得注意大部分的其他的处理同步的内核原语, 例如自旋锁和原子的 _t 操作, 如同内存屏障一样是函数. 还值得注意的是一些外设总线(例如 PCI 总线)有它们自己的缓冲问题; 我们在以后章节遇到时讨论它们.

一些体系允许一个赋值和一个内存屏障的有效组合. 内核提供了几个宏来完成这个组合; 在缺省情况下, 它们如下定义:

#define set_mb(var, value) do {var = value; mb();}  while 0
#define set_wmb(var, value) do {var = value; wmb();} while 0
#define set_rmb(var, value) do {var = value; rmb();} while 0

在合适的地方, <asm/system.h> 定义这些宏来使用体系特定的指令来很快完成任务. 注意 set_rmb 只在少量体系上定义. (一个 do...while 结构的使用是一个标准 C 用语, 来使被扩展的宏作为一个正常的 C 语句可在所有上下文中工作).



[32不是所有的计算机平台使用一个读和一个写信号; 有些有不同的方法来寻址外部电路. 这个不同在软件层次是无关的, 但是, 我们将假设全部有读和写来简化讨论.


<think>嗯...用户这次的问题是关于ZU板卡使用dma_alloc_coherent申请内存失败的解决方案。从上下文看,用户应该是在开发嵌入式或驱动程序时遇到了实际困难。 用户可能正在调试一个关键项目,语气比较急切。这类内存分配问题通常很棘手,因为涉及硬件、内核和驱动的多层交互。需要从几个维度来分析: 首先想到的是CMA配置问题。ZU板卡依赖Contiguous Memory Allocator,如果内核启动参数没预留足够空间就会失败。用户可能需要检查cmdline里的cma=参数值,256MB通常是最低要求。 其次是内存碎片化问题。系统长时间运行后,即使总内存足够也可能无法分配连续大块。建议用户尝试在启动早期分配,或者重启后立即测试。 还有设备树配置容易被忽略。DMA范围设置不当会导致物理地址不匹配,特别要注意memory-regions节点的定义。这个坑我见很多人踩过。 用户没提具体错误代码,但ENOMEM是最常见的。如果伴随dmesg警告"alloc failed",基本可以确认是CMA不足。不过更隐蔽的是32位DMA掩码问题,ZU的DMA引擎有些只支持低4GB地址。 顺便想到个优化点:如果用户不需要大块连续内存,可以用dma_pool代替。不过对于视频处理这类场景,连续内存还是必须的。 最后要提醒内存释放问题。驱动卸载时如果没正确释放,会造成CMA内存泄漏。这个建议结合devm_来管理生命周期。</think>针对ZU板卡使用`dma_alloc_coherent`申请内存失败的问题,以下是常见原因及解决方法: --- ### **一、主要原因分析** 1. **CMA空间不足** - Zynq UltraScale+ (ZU+) 依赖**连续内存分配器(CMA)** 管理DMA内存。若内核启动参数未预留足够CMA空间(如`cma=256M`),或系统碎片化导致连续内存不足,分配将失败[^1]。 2. **设备树配置错误** - DMA地址范围未正确定义: ```dts reserved-memory { #address-cells = <2>; #size-cells = <2>; dma_reserved: buffer@0 { compatible = "shared-dma-pool"; reg = <0x0 0x80000000 0x0 0x20000000>; // 起始地址0x80000000,大小512MB no-map; }; }; ``` - 未关联DMA控制器: ```dts &axi_dma_0 { memory-region = <&dma_reserved>; }; ``` 3. **DMA掩码设置不当** - ZU+支持64位地址,但外设可能仅支持32位DMA。需检查设备驱动是否设置正确掩码: ```c if (dma_set_mask_and_coherent(dev, DMA_BIT_MASK(64)) != 0) // 尝试64位 dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32)); // 回退到32位 ``` 4. **内存泄漏** 未释放已分配的DMA内存(`dma_free_coherent`缺失)会导致CMA池耗尽。 --- ### **二、解决方案** #### **步骤1:检查CMA配置** - **确认启动参数**:在U-Boot或内核命令行添加`cma=512M`(根据需求调整大小): ```bash bootargs = "earlycon clk_ignore_unused cma=512M ..."; ``` - **验证CMA分配**:系统启动后检查`/proc/meminfo`: ```bash grep Cma /proc/meminfo # 输出示例:CmaTotal: 524288 kB, CmaFree: 512000 kB ``` #### **步骤2:修正设备树** 1. 在`reserved-memory`节点定义CMA区域,例如保留512MB: ```dts reserved-memory { dma_reserved: region@80000000 { compatible = "shared-dma-pool"; reg = <0x0 0x80000000 0x0 0x20000000>; // 起始0x8000_0000, 大小512MB reusable; }; }; ``` 2. 将CMA区域关联到DMA控制器: ```dts &axi_dma { memory-region = <&dma_reserved>; }; ``` #### **步骤3:设置DMA掩码** 在驱动初始化代码中显式设置掩码: ```c struct device *dev = &pdev->dev; if (dma_set_mask(dev, DMA_BIT_MASK(64))) { dev_warn(dev, "64-bit DMA not supported, falling back to 32-bit"); dma_set_mask(dev, DMA_BIT_MASK(32)); } ``` #### **步骤4:排查内存泄漏** - 确保每次调用`dma_alloc_coherent`后,在驱动卸载或错误路径中调用`dma_free_coherent`: ```c void *dma_buf; dma_addr_t dma_handle; dma_buf = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL); // ...使用内存... dma_free_coherent(dev, size, dma_buf, dma_handle); // 必须释放! ``` #### **步骤5:替代方案(如CMA不足)** - **使用`dma_map_single`** 对非连续缓冲区进行映射: ```c void *buf = kmalloc(size, GFP_KERNEL); dma_addr_t dma_handle = dma_map_single(dev, buf, size, DMA_TO_DEVICE); // 使用后解除映射 dma_unmap_single(dev, dma_handle, size, DMA_TO_DEVICE); kfree(buf); ``` - **启用`IOMMU`** 若ZU+启用IOMMU(如`SMMU`),可绕过连续内存限制: 1. 确认内核启用`CONFIG_IOMMU_DMA` 2. 在设备树中为外设添加`iommus`属性 --- ### **三、调试技巧** 1. **查看内核日志** ```bash dmesg | grep -i "dma\|cma" # 重点检查错误:"failed to allocate DMA buffer", "CMA: out of memory" ``` 2. **监控CMA使用** ```bash watch -n 1 'cat /proc/meminfo | grep Cma' ``` 3. **检查DMA掩码** ```bash cat /sys/kernel/debug/dma_mask/<device_name> ``` --- ### **相关问题** 1. **ZU+板卡如何优化CMA性能以避免内存碎片化?** > 答:通过`CONFIG_CMA_AREAS`增加CMA区域数量(默认7个),或使用`dma_alloc_from_contiguous`的`align`参数对齐大页内存[^2]。 2. **启用IOMMU后DMA性能下降如何解决?** > 答:调整IOMMU映射粒度(如64KB页)、启用硬件TLB缓存,或对频繁操作使用`dma_map_single_attrs(..., DMA_ATTR_SKIP_CPU_SYNC)`减少同步开销[^3]。 3. **设备树中`reusable``no-map`属性对CMA有何影响?** > 答:`reusable`允许内核复用CMA区域作普通内存,`no-map`保留物理地址空间但不可复用。CMA池通常用`reusable`[^1]。 [^1]: Xilinx官方文档:Zynq UltraScale+ MPSoC DMA引擎CMA配置指南 [^2]: Linux内核文档:Documentation/core-api/cma.rst [^3]: ARM SMMU优化白皮书:IOMMU硬件加速最佳实践
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值