DMA 概念与讲解

在这里插入图片描述

文章目录


一、Linux 物理内存区

Linux 内核将物理内存划分为 ZONE_DMAZONE_NORMALZONE_HIGHMEM 的原则,主要基于以下核心因素:


1. 硬件限制与 DMA 兼容性

  • 传统 DMA 控制器
    老旧设备(如 ISA 总线设备)的 DMA 控制器仅支持 低物理地址范围(如 0x00000000 ~ 0x00FFFFFF,即 16MB)。此时必须使用 ZONE_DMA(通常对应物理地址 0x00000000 ~ 896MB),否则 DMA 操作会失败。

  • 现代 DMA 控制器
    新硬件(如 PCIe 设备)通常支持 32 位或 64 位全地址范围,理论上可以直接访问 ZONE_NORMAL 的物理内存(如 0x80000000 ~ 0x7FFFFFFFFF)。但需确保设备驱动明确支持此类操作。

2. 各个 ZONE 的作用

ZONE_DMA(直接内存访问区)

  • 目的:支持老旧 DMA 控制器(Direct Memory Access)的硬件设备。
  • 硬件限制
    许多早期 DMA 控制器(如 ISA 总线设备)仅能访问物理地址的低范围(通常为 0x00000000 ~ 0x00FFFFFF,即 16MB 或更低)。
    若设备 DMA 操作超出此范围,可能导致地址溢出或数据损坏。
  • 内核策略
    将物理内存的低地址段(如 0x00000000 ~ 896MB)保留为 ZONE_DMA,确保 DMA 设备可直接访问。

ZONE_NORMAL(普通内存区)

  • 目的:存放内核核心数据和频繁访问的内存。
  • CPU 架构优化
    现代 CPU 的 MMU(内存管理单元)和缓存机制对低端内存的访问效率更高。
    内核代码、页表、全局数据结构(如 task_struct)需稳定且连续的物理内存,因此优先分配 ZONE_NORMAL。

ZONE_HIGHMEM(高端内存区)

  • 目的:解决 32 位系统的物理内存寻址限制。
  • 32 位系统限制
    32 位系统的虚拟地址空间为 4GB,内核直接映射的物理内存范围通常为 0x00000000 ~ 0x7FFFFFFF(即 896MB)。
    超出部分(>896MB)无法直接映射到内核虚拟地址空间,需通过动态映射(如 vmallockmap)访问,这部分称为 ZONE_HIGHMEM。
  • 64 位系统差异
    64 位系统的虚拟地址空间极大(如 128TB),可直接映射全部物理内存,因此 无需 ZONE_HIGHMEM

3. 内存分配策略

连续物理内存分配(kmalloc

  • ZONE_DMA & ZONE_NORMAL
    kmalloc() 分配的物理内存默认来自 ZONE_DMA 或 ZONE_NORMAL,保证连续性和低延迟,适用于 DMA、内核数据结构等场景。若指定 GFP_DMA 标志,内核会 强制从 ZONE_DMA 分配。如果未显式指定 GFP_DMA,内核可能从 ZONE_NORMAL 分配内存。此时需满足两个条件:
    1. 物理地址连续:DMA 操作需要连续的物理内存。
    2. 设备支持全地址范围:DMA 控制器必须能访问 ZONE_NORMAL 的物理地址。

非连续内存分配(vmalloc

  • ZONE_HIGHMEM(32 位):
    vmalloc() 分配的虚拟内存可能映射到 ZONE_HIGHMEM,但物理页不要求连续,适用于大块内存或无需 DMA 的场景。

Slab 分配器与对象缓存

  • 缓存亲和性
    频繁访问的小对象(如 struct page)优先从 ZONE_NORMAL 分配,减少缓存失效。
分配器虚拟地址范围物理地址连续性适用场景
kmalloc内核直接映射区连续DMA、硬件操作、内核数据结构
vmallocVMALLOC_START ~ VMALLOC_END不连续大块虚拟内存(无需物理连续)
kmem_cache内核直接映射区连续小对象复用(如 struct page

4. 跨架构差异

系统架构ZONE_DMAZONE_NORMALZONE_HIGHMEM
32 位低端内存(如 0~16MB)中段内存(16MB~896MB)高端内存(>896MB)
64 位通常空闲或极小覆盖几乎全部物理内存不存在

二、PCIe 的 DMA 内存寻址范围

PCIe 的 DMA 支持的内存寻址范围取决于 硬件设计系统架构操作系统配置。以下是其核心机制和限制的详细分析:


1. PCIe 规范的地址空间

  • 理论最大地址范围
    PCIe 协议规定总线地址为 64 位宽,理论上支持 16EB(Exabytes) 的物理地址空间。
    但实际可用范围受限于设备硬件、控制器和操作系统的实现。

  • 默认地址宽度
    大多数 PCIe 设备默认支持 32 位地址(4GB),高端设备(如 NVMe SSD、GPU)可能支持 64 位地址


2. 系统架构的影响

32 位系统

  • 物理地址限制
    32 位系统的物理地址空间为 4GB,但通过 PAE(Physical Address Extension) 可扩展到 64GB(36 位)。
    • DMA 地址范围
      • 若设备仅支持 32 位地址,DMA 缓冲区必须位于 0x00000000 ~ 0xFFFFFFFF 内。
      • 在 Linux 中可通过 dma_set_mask() 设置设备支持的地址掩码(如 DMA_BIT_MASK(32)DMA_BIT_MASK(36))。

64 位系统

  • 几乎无地址限制
    64 位系统的物理地址空间可达 128TB(48 位物理寻址),高端设备(如 x86-64 CPU)通常支持 40/48/57 位物理地址
    • DMA 地址范围
      • 若设备支持 64 位地址,DMA 缓冲区可覆盖几乎全部物理内存。
      • 需通过 dma_set_mask() 启用 64 位地址支持(如 DMA_BIT_MASK(64))。

3. 关键硬件组件

PCIe 控制器(Root Port)

  • 地址转换能力
    PCIe 控制器需支持地址转换(如 IOMMU),将设备的 DMA 地址映射到系统物理地址。
    • IOMMU 的作用
      在启用 IOMMU(如 Intel VT-d、AMD-Vi)时,设备 DMA 地址可以是虚拟地址,由 IOMMU 动态映射到物理内存。

设备自身的 DMA 引擎

  • 设备限制
    某些老旧设备(如早期 SATA 控制器)可能仅支持 32 位 DMA 地址,需通过 dma_set_coherent_mask() 强制约束。

4. Linux 内核的 DMA 管理

DMA 地址掩码设置

  • 驱动中的典型操作
    // 启用 64 位 DMA 支持
    pci_set_dma_mask(pdev, DMA_BIT_MASK(64));
    if (dma_set_mask(&pdev->dev, DMA_BIT_MASK(64))) {
        dev_warn(&pdev->dev, "Failed to set 64-bit DMA mask");
        return -EIO;
    }
    

Bounce Buffer 机制

  • 地址不匹配时的处理
    若设备的 DMA 地址范围不足,内核会分配一个 反弹缓冲区(Bounce Buffer),将数据从高端内存复制到设备可访问的低端地址。
    • 性能影响:反弹缓冲区会增加数据拷贝开销。

5. 实际限制示例

场景DMA 地址范围操作系统支持
老旧 PCIe 设备(32 位)0x00000000 ~ 0xFFFFFFFFLinux 内核默认支持
现代 NVMe SSD(64 位)0x00000000 ~ 0x7FFFFFFFFFFF(48 位)需启用 IOMMU 和 64 位 DMA mask
GPU(64 位地址)0x0000000000000000

三、反弹缓冲区(Bounce Buffer)

DMA反弹缓冲区(Bounce Buffer)是解决 DMA设备与CPU内存访问地址不兼容问题 的关键技术,其核心目标是通过中间缓冲区实现数据的安全传输。以下是其原理、实现细节及应用场景的深度解析:


1. 核心原理

(1) 工作流程

  1. 数据复制:将原始缓冲区(不可访问区域)的数据复制到反弹缓冲区(设备可访问区域)。
  2. DMA传输:设备通过反弹缓冲区完成数据读写。
  3. 反向复制(可选):若需将数据返回给原始缓冲区,需再次复制。

2. 优缺点分析

优点缺点
✔️ 解决硬件地址限制问题❌ 额外内存复制开销
✔️ 支持非连续内存传输❌ 增加延迟(尤其大数据量时)
✔️ 简化驱动开发(抽象底层细节)❌ 需管理缓冲区生命周期(分配/释放)

3. 代码示例(Linux内核)

// 分配反弹缓冲区
void *bounce_buf = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);

// 将数据从原始缓冲区复制到反弹缓冲区
memcpy(bounce_buf, original_buf, size);

// 配置DMA传输目标地址为反弹缓冲区的物理地址
dma_map_single(dev, bounce_buf, size, DMA_TO_DEVICE);

// 触发DMA传输
dmaengine_submit(desc);
dma_async_issue_pending(chan);

// 传输完成后同步数据回原始缓冲区(若需要)
dma_sync_single_for_cpu(dev, dma_handle, size, DMA_FROM_DEVICE);
memcpy(original_buf, bounce_buf, size);

// 释放资源
dma_free_coherent(dev, size, bounce_buf, dma_handle);

四、PCIe DMA 传输过程详解

PCIe MMIO、DMA、TLP
【PCIe】PCIe配置空间与CPU访问机制

从操作系统分配 DMA 内存

将 DMA 地址编程到设备并开始传输

原始数据
在这里插入图片描述
写入后数据
在这里插入图片描述

设备执行 DMA 事务

下面是内存读取和响应的图表,展示了请求使用地址发出请求,而完成使用请求的请求者字段中的 BDF 发送响应:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
该图中概述的步骤如下:

  • DMA 引擎创建 TLP —— DMA 引擎识别到它必须从 0x001FF000 读取 32 个字节。它生成包含此请求的 TLP,并通过其本地 PCIe 链路将其发送出去。
  • TLP 遍历层次结构 —— PCIe 的交换层次结构将此请求通过桥接设备移动,直到到达目的地,即根联合体。回想一下,RC 负责处理所有旨在访问系统 RAM 的传入数据包。
  • DRAM 控制器已通知 —— 根联合体内部与负责实际访问系统 DRAM 内存的 DRAM 控制器进行通信。
  • 从 DRAM 读取内存 —— 从地址 0x001FF000 处的 DRAM 请求给定长度的 32 字节,并将其返回到根联合体,值为 01 02 03 04…

最后,完成信息从根联合体发送回设备。注意,目的地与请求者相同:

在这里插入图片描述
以下是上面响应包中概述的步骤:

  • 内存从 DRAM 读取 —— DRAM 控制器从系统 DRAM 中 0x001FF000 处的 DMA 缓冲区地址读取 32 个字节。
  • DRAM 控制器响应根联合体 —— DRAM 控制器内部将从 DRAM 请求的内存响应到根联合体
  • 根复合体生成完成 —— 根复合体跟踪传输并为从 DRAM 读取的值创建完成 TLP。在此 TLP 中,元数据值是根据 RC 拥有的待处理传输的知识设置的,例如发送的字节数、传输的标签以及从原始请求中的请求者字段复制的目标 BDF。
  • DMA 引擎接收 TLP —— DMA 引擎通过 PCIe 链路接收 TLP,并发现标签与原始请求的标签相同。它还会在内部跟踪此值,并知道有效载荷中的内存应写入目标内存,该内存位于设备内部 RAM 中的 0x8000 处。
  • 目标内存已写入 —— 设备内存中的值将使用从数据包有效负载中复制出来的值进行更新。
  • 系统中断 —— 虽然这是可选的,但大多数 DMA 引擎将配置为在 DMA 完成时中断主机 CPU。当设备成功完成 DMA 时,这会向设备驱动程序发出通知。

再次强调,处理单个完成数据包涉及很多步骤。但是,您可以再次将整个过程视为“从设备请求中收到 32 个字节的响应”。其余步骤只是为了向您展示此响应处理的完整端到端流程。

五、USB Root Hub DMA 传输

usb 首先要准备数据,其大致流程如下:

在这里插入图片描述

函数 map_urb_for_dma 会根据 Hub 类型调用不同的函数来分配 DMA 内存空间。

// drivers/usb/core/hcd.c
static int map_urb_for_dma(struct usb_hcd *hcd, struct urb *urb,
			   gfp_t mem_flags)
{
	if (hcd->driver->map_urb_for_dma)
		return hcd->driver->map_urb_for_dma(hcd, urb, mem_flags);
	else
		return usb_hcd_map_urb_for_dma(hcd, urb, mem_flags);
}

hcd->driver->map_urb_for_dma 实现了 usb root hub 的差异化,其可以根据自己的能力调用不同的函数来实现分配 DMA 的差异化。

在 Linux 内核中,usb_hcd_map_urb_for_dma 是 USB 主机控制器驱动(HCD)中与 DMA(直接内存访问) 关键操作相关的函数,其核心功能是为 USB 请求块(URB)配置 DMA 传输所需的硬件资源。以下从功能、实现逻辑和应用场景三个维度进行详细分析:


1、usb_hcd_map_urb_for_dma

usb_hcd_map_urb_for_dma 是 Linux 内核 USB 子系统中 USB 主机控制器驱动(HCD) 的核心函数,负责为 USB 请求块(URB)建立 DMA 映射,确保主机控制器能直接访问内存数据。以下结合代码逐层分析其功能与实现逻辑:

// drivers/usb/core/hcd.c
int usb_hcd_map_urb_for_dma(struct usb_hcd *hcd, struct urb *urb,
			    gfp_t mem_flags)
{
	enum dma_data_direction dir;
	int ret = 0;

	/* Map the URB's buffers for DMA access.
	 * Lower level HCD code should use *_dma exclusively,
	 * unless it uses pio or talks to another transport,
	 * or uses the provided scatter gather list for bulk.
	 */

	if (usb_endpoint_xfer_control(&urb->ep->desc)) {
		if (hcd->self.uses_pio_for_control)
			return ret;
		if (hcd->localmem_pool) {
			ret = hcd_alloc_coherent(
					urb->dev->bus, mem_flags,
					&urb->setup_dma,
					(void **)&urb->setup_packet,
					sizeof(struct usb_ctrlrequest),
					DMA_TO_DEVICE);
			if (ret)
				return ret;
			urb->transfer_flags |= URB_SETUP_MAP_LOCAL;
		} else if (hcd_uses_dma(hcd)) {
			if (object_is_on_stack(urb->setup_packet)) {
				WARN_ONCE(1, "setup packet is on stack\n");
				return -EAGAIN;
			}

			urb->setup_dma = dma_map_single(
					hcd->self.sysdev,
					urb->setup_packet,
					sizeof(struct usb_ctrlrequest),
					DMA_TO_DEVICE);
			if (dma_mapping_error(hcd->self.sysdev,
						urb->setup_dma))
				return -EAGAIN;
			urb->transfer_flags |= URB_SETUP_MAP_SINGLE;
		}
	}

	dir = usb_urb_dir_in(urb) ? DMA_FROM_DEVICE : DMA_TO_DEVICE;
	if (urb->transfer_buffer_length != 0
	    && !(urb->transfer_flags & URB_NO_TRANSFER_DMA_MAP)) {
		if (hcd->localmem_pool) {
			ret = hcd_alloc_coherent(
					urb->dev->bus, mem_flags,
					&urb->transfer_dma,
					&urb->transfer_buffer,
					urb->transfer_buffer_length,
					dir);
			if (ret == 0)
				urb->transfer_flags |= URB_MAP_LOCAL;
		} else if (hcd_uses_dma(hcd)) {
			if (urb->num_sgs) {
				int n;

				/* We don't support sg for isoc transfers ! */
				if (usb_endpoint_xfer_isoc(&urb->ep->desc)) {
					WARN_ON(1);
					return -EINVAL;
				}

				n = dma_map_sg(
						hcd->self.sysdev,
						urb->sg,
						urb->num_sgs,
						dir);
				if (!n)
					ret = -EAGAIN;
				else
					urb->transfer_flags |= URB_DMA_MAP_SG;
				urb->num_mapped_sgs = n;
				if (n != urb->num_sgs)
					urb->transfer_flags |=
							URB_DMA_SG_COMBINED;
			} else if (urb->sg) {
				struct scatterlist *sg = urb->sg;
				urb->transfer_dma = dma_map_page(
						hcd->self.sysdev,
						sg_page(sg),
						sg->offset,
						urb->transfer_buffer_length,
						dir);
				if (dma_mapping_error(hcd->self.sysdev,
						urb->transfer_dma))
					ret = -EAGAIN;
				else
					urb->transfer_flags |= URB_DMA_MAP_PAGE;
			} else if (object_is_on_stack(urb->transfer_buffer)) {
				WARN_ONCE(1, "transfer buffer is on stack\n");
				ret = -EAGAIN;
			} else {
				urb->transfer_dma = dma_map_single(
						hcd->self.sysdev,
						urb->transfer_buffer,
						urb->transfer_buffer_length,
						dir);
				if (dma_mapping_error(hcd->self.sysdev,
						urb->transfer_dma))
					ret = -EAGAIN;
				else
					urb->transfer_flags |= URB_DMA_MAP_SINGLE;
			}
		}
		if (ret && (urb->transfer_flags & (URB_SETUP_MAP_SINGLE |
				URB_SETUP_MAP_LOCAL)))
			usb_hcd_unmap_urb_for_dma(hcd, urb);
	}
	return ret;
}

(1)、函数功能概述

  1. 核心目标

    • 为 URB 的 控制包(Setup Packet)数据缓冲区(Transfer Buffer) 分配 DMA 映射。
    • 根据硬件限制(如本地内存池或系统 DMA 支持)选择映射方式。
  2. 关键场景

    • 控制传输:映射 Setup 包(仅控制端点需要)。
    • 数据传输:处理批量(Bulk)、中断(Interrupt)或等时(Isoc)传输的数据缓冲区。
    • 分散/聚集(Scatter-Gather):支持非连续内存的合并传输。

(2)、代码逻辑详解

1. 控制传输处理
if (usb_endpoint_xfer_control(&urb->ep->desc)) {
    if (hcd->self.uses_pio_for_control)
        return ret;
    if (hcd->localmem_pool) {
        // 使用本地内存池分配一致性内存
        ret = hcd_alloc_coherent(...);
        if (ret)
            return ret;
        urb->transfer_flags |= URB_SETUP_MAP_LOCAL;
    } else if (hcd_uses_dma(hcd)) {
        // 检查 Setup 包是否在栈上(禁止 DMA 映射)
        if (object_is_on_stack(urb->setup_packet)) {
            WARN_ONCE(1, "setup packet is on stack\n");
            return -EAGAIN;
        }
        // 单页 DMA 映射
        urb->setup_dma = dma_map_single(...);
        if (dma_mapping_error(...))
            return -EAGAIN;
        urb->transfer_flags |= URB_SETUP_MAP_SINGLE;
    }
}
  • 关键点
    • PIO 模式:若 HCD 明确使用 PIO(无 DMA),直接跳过。
    • 本地内存池:通过 hcd_alloc_coherent 分配物理连续内存,适用于 SRAM 等受限场景。
    • 系统 DMA:使用 dma_map_single 映射用户空间或内核态内存,需确保内存物理连续。
2. 数据传输处理
dir = usb_urb_dir_in(urb) ? DMA_FROM_DEVICE : DMA_TO_DEVICE;
if (urb->transfer_buffer_length != 0
    && !(urb->transfer_flags & URB_NO_TRANSFER_DMA_MAP)) {
    if (hcd->localmem_pool) {
        // 本地内存池分配(一致性 DMA)
        ret = hcd_alloc_coherent(...);
        if (ret == 0)
            urb->transfer_flags |= URB_MAP_LOCAL;
    } else if (hcd_uses_dma(hcd)) {
        if (urb->num_sgs) {
            // 分散/聚集映射(多页合并)
            n = dma_map_sg(...);
            if (!n)
                ret = -EAGAIN;
            else
                urb->transfer_flags |= URB_DMA_MAP_SG;
        } else if (urb->sg) {
            // 单页 DMA 映射(偏移量处理)
            urb->transfer_dma = dma_map_page(...);
        } else if (object_is_on_stack(urb->transfer_buffer)) {
            WARN_ONCE(1, "transfer buffer is on stack\n");
            ret = -EAGAIN;
        } else {
            // 常规单页映射
            urb->transfer_dma = dma_map_single(...);
        }
    }
}
  • 关键点
    • 传输方向:通过 usb_urb_dir_in 确定 DMA 方向(输入/输出)。
    • 分散/聚集支持:通过 dma_map_sg 合并多个非连续内存页,需 HCD 支持且非等时传输。
    • 栈内存检查:禁止 DMA 映射栈内存(可能导致物理地址不连续)。
3. 错误处理与资源释放
if (ret && (urb->transfer_flags & (URB_SETUP_MAP_SINGLE | URB_SETUP_MAP_LOCAL)))
    usb_hcd_unmap_urb_for_dma(hcd, urb);
  • 回滚机制:若数据缓冲区映射失败但已分配 Setup 包映射,需释放 Setup 包资源。

(3)、核心设计思想

  1. 硬件兼容性

    • 本地内存池:针对 DMA 地址受限的控制器(如仅支持小容量 SRAM),通过 hcd_alloc_coherent 分配一致性内存。
    • 系统 DMA:通用场景下使用 dma_map_* 接口,依赖 CPU 缓存一致性协议。
  2. 性能优化

    • 分散/聚集合并:通过 URB_DMA_MAP_SG_COMBINED 标志减少 DMA 事务次数。
    • 零拷贝:直接传递用户空间缓冲区(需确保物理连续性)。
  3. 安全性保障

    • 栈内存检查:防止 DMA 访问无效的栈内存地址。
    • 错误回滚:映射失败时释放已分配资源,避免内存泄漏。

(4)、关键数据结构与标志

结构体/标志作用
URB_SETUP_MAP_LOCAL标记 Setup 包使用本地内存池分配
URB_DMA_MAP_SG表示数据缓冲区使用分散/聚集映射
URB_DMA_MAP_SINGLE表示数据缓冲区使用单页 DMA 映射
hcd->localmem_pool指向本地内存池(如 SRAM),用于受限 DMA 场景

(5)、典型调用场景

  1. 批量数据传输

    // 分配 4KB 连续内存并映射
    urb->transfer_buffer = kmalloc(4096, GFP_KERNEL);
    ret = usb_hcd_map_urb_for_dma(hcd, urb, GFP_KERNEL);
    
  2. 分散/聚集传输

    // 初始化 scatterlist
    struct scatterlist sg[2];
    sg_init_table(sg, 2);
    sg[0].page_link = (unsigned long)page1 + offset1;
    sg[0].offset = 0;
    sg[0].length = len1;
    sg[1].page_link = (unsigned long)page2 + offset2;
    sg[1].offset = 0;
    sg[1].length = len2;
    urb->sg = sg;
    urb->num_sgs = 2;
    ret = usb_hcd_map_urb_for_dma(hcd, urb, GFP_KERNEL);
    

(6)与 URB 生命周期的关系

阶段操作相关函数
URB 提交前分配 DMA 缓冲区并映射usb_alloc_urb + map_urb_for_dma
DMA 传输中主机控制器执行数据传输HCD 驱动代码(如 ehci_urb_enqueue
传输完成/取消解除 DMA 映射并释放资源unmap_urb_for_dma + usb_free_urb

(7)总结

usb_hcd_map_urb_for_dma 是 USB 子系统中连接 CPU 内存与 DMA 控制器的桥梁,其设计需兼顾 硬件兼容性性能效率。理解其实现细节对开发高性能 USB 驱动(如定制 HCD 或优化现有控制器)至关重要。

2、hcd_alloc_coherent

在 Linux 6.12.5 内核中,hcd_alloc_coherent 是 USB 主机控制器驱动(HCD)框架中用于 分配一致性 DMA 内存 的核心函数。其设计目标是确保 CPU 和 DMA 控制器对同一块内存的访问保持一致性,避免因缓存不同步导致的数据错误。以下从功能、实现细节和应用场景三个维度进行深入分析:


(1)、函数功能与设计目标

1. 核心功能
  • 一致性内存分配:分配物理连续的内存区域,并建立 CPU 虚拟地址与总线地址(DMA 地址)的映射。
  • 缓存一致性保障:通过页表标记或显式同步操作,确保 CPU 与 DMA 设备对内存的读写顺序一致。
2. 设计目标
  • 硬件兼容性:适配不同总线类型(如 PCIe、USB)的 DMA 地址映射规则。
  • 性能优化:减少 CPU 缓存刷新开销,适用于高频 DMA 传输场景(如 USB 批量传输)。
  • 资源管理:与 HCD 驱动的内存池(如本地内存池 localmem_pool)集成,避免内存碎片。

3、dma_alloc_coherent

在 Linux 内核中,dma_alloc_coherentDMA(直接内存访问)操作的核心函数,用于分配物理连续且与设备 DMA 兼容的内存区域。其设计目标是解决 CPU 虚拟地址与设备物理地址之间的映射问题,同时确保数据在 CPU 和 DMA 设备之间的读写一致性。以下从功能、实现原理、使用场景和优化方向进行详细分析:


(1)、函数功能与核心特性

1. 核心功能
  • 物理连续内存分配:确保分配的内存物理地址连续,满足 DMA 设备对物理地址连续性的要求。
  • 一致性内存管理:通过缓存一致性机制(如禁用缓存或显式同步),避免 CPU 与 DMA 设备因缓存不一致导致的数据错误。
  • 总线地址映射:返回内存的总线地址(dma_handle),供 DMA 设备直接访问。
2. 关键参数
void *dma_alloc_coherent(
    struct device *dev,      // 关联的设备(如 PCI 设备)
    size_t size,             // 分配的内存大小
    dma_addr_t *dma_handle,  // 输出参数:总线地址
    gfp_t flags              // 分配标志(如 GFP_KERNEL、GFP_ATOMIC)
);
  • 返回值:内核虚拟地址(CPU 可访问),dma_handle 为总线地址。
  • 内存对齐:自动对齐到页边界(PAGE_SIZE),部分平台支持更细粒度对齐(如 32 位系统对齐到 32 位边界)。
3. 返回值与错误处理
  • 成功:返回虚拟地址,dma_handle 填充总线地址。
  • 失败:返回 NULL,需检查 dev->dma_mask 是否支持请求的大小。

(2)、实现原理与底层机制

1. 内存分配流程
  1. 物理页分配
    调用 __get_free_pagesdma_alloc_attrs 分配物理连续的页(可能通过 CMA、SWIOTLB 或 buddy 系统)。
  2. 虚拟地址映射
    通过 virt_to_bus 或平台特定函数(如 page_to_bus)将虚拟地址转换为总线地址。
  3. 缓存处理
    • 禁用缓存:通过页表标记(如 pgprot_noncached)或硬件机制(如 ARM 的 VM_ARM_DMA_CONSISTENT)确保 CPU 不缓存该内存。
    • 清零初始化:调用 memset 初始化内存(部分驱动会跳过此步骤)。
2. 平台差异
  • x86
    直接使用物理地址作为总线地址,通过 virt_to_bus 转换。
  • ARM/PowerPC
    需通过页表重映射或 DMA 窗口(如 dma_map_single)实现虚拟地址到总线地址的映射。
  • 带 IOMMU 的系统
    通过 IOMMU 转换虚拟地址到设备物理地址,dma_handle 可能与物理地址不同。
3. 代码片段(x86 平台)
void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, gfp_t flags) {
    void *vaddr;
    vaddr = (void *)__get_free_pages(flags, get_order(size));  // 分配物理连续页
    if (!vaddr) return NULL;
    memset(vaddr, 0, size);
    *dma_handle = virt_to_bus(vaddr);  // 转换为总线地址
    return vaddr;
}

(3)、使用场景与典型应用

1. 典型场景
  • 控制传输(Control Transfer)
    分配 Setup 包缓冲区(如 USB 控制传输的 8 字节 Setup 包)。
  • 批量数据传输(Bulk Transfer)
    分配连续缓冲区用于文件传输或流媒体处理。
  • 中断/等时传输(Interrupt/Isoc Transfer)
    分配低延迟缓冲区,确保周期性数据传输的实时性。
2. 代码示例
// 分配 4KB 的一致性 DMA 缓冲区
void *buffer;
dma_addr_t dma_addr;
buffer = dma_alloc_coherent(dev, 4096, &dma_addr, GFP_KERNEL);
if (!buffer) return -ENOMEM;

// 将数据写入缓冲区(CPU 操作)
memset(buffer, 0xAA, 4096);

// 触发 DMA 传输(设备使用 dma_addr)
dma_engine_submit(desc);
dma_async_issue_pending(chan);

// 释放资源
dma_free_coherent(dev, 4096, buffer, dma_addr);

(4)、与其他 DMA 映射函数的对比

函数特点适用场景
dma_alloc_coherent分配一致性内存,物理连续,缓存禁用长期 DMA 缓冲区(如控制包、批量数据)
dma_map_single动态映射现有内存到 DMA 地址空间,需手动同步缓存临时 DMA 操作(如单次数据传输)
dma_map_sg映射分散/聚集缓冲区(多个非连续页),支持合并传输复杂数据结构(如网络数据包)
dma_alloc_attrs支持自定义属性(如 CMA、保留内存),灵活性更高特殊硬件需求(如 GPU 直接访问内存)

4、dma_map_single

在 Linux 内核中,dma_map_single 是用于 流式 DMA(Direct Memory Access)映射 的核心函数,主要用于将 CPU 虚拟地址映射为设备可直接访问的总线地址,同时处理 CPU 缓存与设备之间的数据一致性问题。以下是其核心原理和功能解析:


1. 功能与用途

  • 作用:将一段连续的 CPU 虚拟内存区域映射到设备的 DMA 地址空间,使设备(如网卡、磁盘控制器等)能够直接访问该内存,无需 CPU 干预。
  • 适用场景
    • 非一致性设备:设备不支持缓存一致性协议(如某些 DMA 控制器或外设)。
    • 动态或临时缓冲区:如从其他模块传入的地址空间,或需要频繁分配/释放的 DMA 缓冲区。
    • 流式数据传输:数据单向流动(如从内存到设备或设备到内存),而非长期共享的双向数据。

2. 函数原型与参数

dma_addr_t dma_map_single(struct device *dev, void *cpu_addr, size_t size, enum dma_data_direction dir);
  • 参数说明

    • dev:指向设备的 struct device 指针,标识目标设备。
    • cpu_addr:CPU 虚拟地址,需映射的内存起始地址。
    • size:映射的内存区域大小(以字节为单位)。
    • dir:传输方向,决定缓存操作类型:
      • DMA_TO_DEVICE:数据从 CPU 写入设备(需刷新缓存)。
      • DMA_FROM_DEVICE:数据从设备读入 CPU(需使缓存无效)。
      • DMA_BIDIRECTIONAL:双向传输(需同时刷新和使无效)。
  • 返回值:映射后的总线地址(Bus Address),供设备使用。


3. 实现原理

(1) 缓存一致性处理
  • 流式映射的核心挑战:CPU 缓存与设备内存可能不一致。例如:
    • 设备写入数据到 DMA 缓冲区时,CPU 可能仍读取旧缓存数据。
    • CPU 修改缓冲区后,设备可能读取到部分旧数据。
  • 解决方案
    • DMA_TO_DEVICE:调用 outer_clean_range,将 CPU 缓存中的数据刷写到内存,确保设备读取最新数据。
    • DMA_FROM_DEVICE:调用 outer_inv_range,使 CPU 缓存中的对应区域无效,强制下次读取时从内存获取最新数据。
(2) 地址转换
  • 通过 virt_to_dma(dev, cpu_addr) 将 CPU 虚拟地址转换为总线地址(Bus Address),供设备直接访问。
(3) 错误检查
  • 调用 valid_dma_direction(dir) 验证传输方向合法性。
  • 检查内存地址和大小是否在有效范围内(virt_addr_valid)。

4. 使用流程

  1. 分配内存:通过 vmalloc ?dma_alloc_coherent 分配物理连续的内存。
  2. 映射:调用 dma_map_single 获取总线地址。
  3. 启动 DMA 传输:将总线地址传递给设备驱动。
  4. 解除映射:传输完成后调用 dma_unmap_single,并可能调用 dma_sync_single_for_cpu 同步缓存(若需 CPU 访问数据)。

5. 与一致性映射(Consistent Mapping)的区别

特性dma_map_single(流式映射)dma_alloc_coherent(一致性映射)
生命周期临时映射,单次传输后解除长期映射,驱动卸载时解除
缓存管理需手动处理缓存一致性硬件自动维护一致性
适用场景动态缓冲区、非一致性设备长期共享的双向数据缓冲区
性能开销较高(每次传输需缓存操作)较低(初始化时完成映射)

6. 注意事项

  • 物理地址连续性:映射的内存需物理连续,否则可能导致 DMA 失败。
  • 缓存操作顺序:未正确同步缓存可能导致数据损坏(如未使无效缓存直接读取设备写入的数据)。
  • 设备能力检查:需确认设备是否支持流式 DMA 映射(部分设备强制要求一致性映射)。

7. 代码示例

void *cpu_buf = vmalloc(4096);	// ? 可能无法分配连续的内存
dma_addr_t dma_addr;

// 映射内存
dma_addr = dma_map_single(dev, cpu_buf, 4096, DMA_TO_DEVICE);
if (dma_mapping_error(dev, dma_addr)) {
    // 处理错误
}

// 启动 DMA 传输(将 dma_addr 传递给设备)

// 解除映射
dma_unmap_single(dev, dma_addr, 4096, DMA_TO_DEVICE);
vfree(cpu_buf);

总结

dma_map_single 是 Linux 内核中处理 非一致性设备 DMA 传输 的关键接口,通过缓存操作和地址转换确保数据一致性。其灵活性与性能开销需根据具体场景权衡,开发者需严格遵循传输方向和生命周期管理规则。

5、dma_direct_mmap

在 Linux 内核中,dma_direct_mmap直接 DMA(Direct Memory Access)内存映射的核心函数,专为需要设备直接访问内核虚拟地址的场景设计。其核心目标是 绕过 CPU 中转,实现设备与内存的高效数据传输。以下从功能、实现原理、使用场景及注意事项进行深入分析:


(1)、函数功能与设计目标

1. 核心功能
  • 直接地址映射:将内核虚拟地址(或物理地址)映射到设备的 DMA 地址空间,使设备可直接读写内存。
  • 零拷贝优化:避免 CPU 参与数据传输(如网络设备接收数据包时直接写入用户空间缓冲区)。
  • 硬件适配:支持不同总线类型(如 PCIe、USB)的 DMA 地址映射规则。
2. 设计目标
  • 性能提升:减少 CPU 负担和数据拷贝次数(如从 2 次减少到 1 次)。
  • 灵活性:支持用户空间与内核空间的共享内存(如视频采集卡直接访问用户缓冲区)。
  • 兼容性:适配带 IOMMU 的系统,处理地址转换和权限控制。

(2)、实现原理与底层机制

1. 映射流程
int dma_direct_mmap(struct device *dev, struct vm_area_struct *vma,
                    void *cpu_addr, dma_addr_t dma_addr, size_t size)
{
    // 1. 验证地址对齐和权限
    if (!dma_capable(dev, dma_addr, size))
        return -EINVAL;

    // 2. 建立页表映射
    return remap_pfn_range(vma, vma->vm_start, virt_to_phys(cpu_addr) >> PAGE_SHIFT,
                          size, vma->vm_page_prot);
}
  • 关键步骤
    1. 地址验证:检查设备是否支持该 DMA 地址范围(通过 dma_capable())。
    2. 页表映射:调用 remap_pfn_range 将物理页映射到用户虚拟地址空间。
2. 缓存一致性处理
  • ARM 架构:通过 dma_sync_single_for_cpu/dma_sync_single_for_device 同步缓存。
  • x86 架构:依赖 MTRR(内存类型范围寄存器)或 PAT(页属性表)设置缓存策略。
3. IOMMU 集成
  • 若启用 IOMMU,dma_direct_mmap 会通过 IOMMU 驱动建立虚拟地址到设备物理地址的映射表。
  • 示例:Intel IOMMU 的 intel_iommu_map_page 函数分配 IOMMU 页表项。

(3)、使用场景与典型代码

1. 典型场景
  • 用户空间 DMA:将用户空间缓冲区(通过 mmap)映射到设备,避免内核拷贝(如 GPU 直接渲染图像)。
  • 共享内存通信:多进程/线程共享 DMA 缓冲区(如视频流处理)。
  • 实时数据流:网络设备接收数据包时直接写入用户缓冲区(如 AF_PACKET 套接字)。
2. 代码示例
// 用户空间通过 mmap 映射 DMA 缓冲区
struct vm_area_struct *vma = ...;
void *cpu_addr = dma_alloc_coherent(dev, size, &dma_addr, GFP_KERNEL);

// 建立 DMA 直接映射
if (dma_direct_mmap(dev, vma, cpu_addr, dma_addr, size)) {
    dma_free_coherent(dev, size, cpu_addr, dma_addr);
    return -ENOMEM;
}

// 用户空间直接访问 dma_addr
read_from_device(dma_addr, buffer, size);

(4)、关键注意事项

1. 内存要求
  • 物理连续性cpu_addr 必须位于物理连续的内存区域(如通过 dma_alloc_coherent 分配)。
  • 对齐约束:地址和大小需满足设备对齐要求(如 4KB 对齐)。
2. 权限控制
  • 页保护标志:设置 vm_page_protPROT_READ/PROT_WRITE,禁止用户空间写入只读区域。
  • IOMMU 权限:配置 IOMMU 的页表项权限(如读写、执行)。
3. 性能优化
  • 预映射内存池:通过 dma_pool 预分配内存,减少动态映射开销。
  • 批量映射:对分散/聚集缓冲区使用 dma_map_sg,合并多个页的 DMA 操作。
4. 错误处理
  • 检查返回值:若 dma_direct_mmap 返回负值,需释放已分配资源。
  • 同步操作:在 DMA 操作前后调用 dma_sync_* 确保数据可见性。

6、iommu_dma_mmap

在 Linux 内核中,iommu_dma_mmapIOMMU(输入/输出内存管理单元)与 DMA 操作结合的核心函数,专门用于在启用 IOMMU 的场景下建立用户空间与设备之间的 DMA 映射。其核心目标是 解决虚拟化环境中的地址隔离问题,同时优化 DMA 传输效率。以下从功能、实现原理、使用场景及注意事项进行深入分析:


(1)、函数功能与设计目标

1. 核心功能
  • 虚拟地址到设备地址的转换:将用户空间虚拟地址(VA)或内核线性地址(GPA)通过 IOMMU 映射为设备可访问的 DMA 地址(HPA)。
  • 地址空间隔离:在虚拟化场景中,隐藏物理地址(HPA),仅暴露虚拟地址(GPA)给虚机,防止虚机直接访问物理内存。
  • 零拷贝优化:支持用户空间缓冲区直接参与 DMA 传输,减少 CPU 参与的数据拷贝。
2. 设计目标
  • 安全性:通过 IOMMU 限制设备访问的物理内存范围,避免 DMA 攻击。
  • 灵活性:支持分散/聚集缓冲区(Scatter-Gather)的合并传输。
  • 兼容性:适配不同总线类型(如 PCIe、USB)和 IOMMU 实现(如 Intel VT-d、ARM SMMU)。

(2)、实现原理与底层机制

1. 映射流程
int iommu_dma_mmap(struct device *dev, struct vm_area_struct *vma,
                   dma_addr_t dma_addr, phys_addr_t phys_addr, size_t size)
{
    // 1. 验证地址对齐和权限
    if (!iommu_dma_capable(dev, dma_addr, size))
        return -EINVAL;

    // 2. 建立 IOMMU 页表映射
    return iommu_map_page(dev, vma, dma_addr, phys_addr, size);
}
  • 关键步骤
    1. 地址验证:检查设备是否支持该 DMA 地址范围(通过 iommu_dma_capable())。
    2. 页表映射:调用 IOMMU 驱动(如 intel_iommu_map_page)建立虚拟地址到物理地址的映射。
2. IOMMU 页表机制
  • 页表层级:IOMMU 使用多级页表(如 4 级页表)管理地址转换,类似 CPU 的 MMU。
  • 页表项属性:设置读写权限、缓存策略(如 dma_pte_read/dma_pte_write)和页大小(4KB/2MB/1GB)。
  • 缓存一致性:通过 dma_flush_pmd_range 等函数同步页表修改到硬件。
3. 虚拟化场景处理
  • GPA→HPA 映射:在 KVM 虚拟化中,IOMMU 将虚机的 GPA(Guest Physical Address)映射到宿主机的 HPA(Host Physical Address)。
  • 设备透传:通过 vfio_iommu_map 实现设备直接访问用户空间缓冲区,隔离物理地址暴露。

(3)、使用场景与典型代码

1. 典型场景
  • 用户态 DMA:用户进程通过 mmap 直接访问 DMA 缓冲区(如 GPU 直接渲染图像)。
  • 虚拟设备直通:将 PCIe 设备透传给虚机,虚机通过 GPA 访问宿主机内存。
  • 实时音视频处理:用户空间缓冲区直接参与 DMA 传输,避免内核拷贝延迟。
2. 代码示例
// 用户空间通过 mmap 映射 DMA 缓冲区
struct vm_area_struct *vma = ...;
dma_addr_t dma_addr;
phys_addr_t phys_addr;

// 分配一致性内存并获取物理地址
void *cpu_addr = dma_alloc_coherent(dev, size, &dma_addr, GFP_KERNEL);
phys_addr = virt_to_phys(cpu_addr);

// 建立 IOMMU 映射
if (iommu_dma_mmap(dev, vma, dma_addr, phys_addr, size)) {
    dma_free_coherent(dev, size, cpu_addr, dma_addr);
    return -ENOMEM;
}

// 用户空间直接操作 dma_addr
read_from_device(dma_addr, buffer, size);

7、unmap_urb_for_dma

在 Linux 内核中,unmap_urb_for_dmaUSB 子系统用于解除 URB(USB 请求块)的 DMA 映射的核心函数,其核心作用是释放 URB 传输过程中分配的 DMA 资源,并确保 CPU 与设备之间的缓存一致性。以下从功能、实现流程、关键数据结构及代码逻辑进行深入分析:


(1)、函数功能与设计目标

1. 核心功能
  • 解除 DMA 映射:释放 URB 的 transfer_buffersg 列表对应的 DMA 地址。
  • 缓存同步:根据传输方向(DMA_TO_DEVICEDMA_FROM_DEVICE)刷新或无效化缓存。
  • 资源回收:释放 DMA 池或一致性内存分配的缓冲区。
2. 设计目标
  • 安全性:防止 DMA 映射泄漏或重复释放。
  • 性能优化:通过批量解除映射减少锁竞争。
  • 兼容性:适配不同架构(如 x86、ARM)的 DMA 实现。

(2)、实现流程与关键步骤

1. 解除单次传输映射(dma_unmap_single
// 伪代码示例(基于 Linux 内核源码)
void unmap_urb_for_dma(struct usb_hcd *hcd, struct urb *urb)
{
    if (urb->transfer_dma == DMA_MAPPING_ERROR)
        return;

    // 根据传输方向同步缓存
    if (usb_urb_dir_out(urb))
        dma_sync_single_for_cpu(hcd->self.controller, urb->transfer_dma,
                               urb->transfer_buffer_length, DMA_TO_DEVICE);
    else
        dma_sync_single_for_cpu(hcd->self.controller, urb->transfer_dma,
                               urb->transfer_buffer_length, DMA_FROM_DEVICE);

    // 释放 DMA 映射
    dma_unmap_single(hcd->self.controller, urb->transfer_dma,
                    urb->transfer_buffer_length, DMA_BIDIRECTIONAL);
}
  • 逻辑
    • 方向判断:根据传输方向(输入/输出)调用不同的缓存同步函数。
    • 解除映射:调用 dma_unmap_single 释放总线地址映射。
2. 解除分散/聚集传输映射(dma_unmap_sg
// 伪代码示例(基于 Linux 内核源码)
void unmap_urb_for_dma(struct usb_hcd *hcd, struct urb *urb)
{
    struct scatterlist *sg = urb->sg;
    int num_sgs = urb->num_mapped_sgs;

    // 同步缓存
    if (usb_urb_dir_out(urb))
        dma_sync_sg_for_cpu(hcd->self.controller, sg, num_sgs,
                           DMA_TO_DEVICE);
    else
        dma_sync_sg_for_cpu(hcd->self.controller, sg, num_sgs,
                           DMA_FROM_DEVICE);

    // 释放 DMA 映射
    dma_unmap_sg(hcd->self.controller, sg, num_sgs, DMA_BIDIRECTIONAL);
}
  • 逻辑
    • 分散/聚集处理:遍历 sg 列表,逐个解除映射。
    • 批量操作:通过 dma_unmap_sg 一次性处理多个内存页。
3. 资源回收
// 释放一致性内存(若使用 dma_alloc_coherent)
if (urb->transfer_flags & URB_NO_TRANSFER_DMA_MAP) {
    dma_free_coherent(hcd->self.controller, urb->transfer_buffer_length,
                      urb->transfer_buffer, urb->transfer_dma);
}
  • 条件:仅当 URB 使用一致性映射时释放内存。

(3)、关键数据结构

1. struct urb
struct urb {
    dma_addr_t transfer_dma;    // DMA 映射后的总线地址
    void *transfer_buffer;      // CPU 虚拟地址
    struct scatterlist *sg;     // 分散/聚集缓冲区列表
    int num_mapped_sgs;         // 映射的 SG 条目数
    unsigned int transfer_flags;// 标志位(如 URB_NO_TRANSFER_DMA_MAP)
    // 其他字段(如端点信息、状态等)
};
  • 关键字段
    • transfer_dma:DMA 映射后的物理地址。
    • sg:用于分散/聚集传输的缓冲区链表。
2. struct dma_map_ops
struct dma_map_ops {
    dma_addr_t (*map_single)(struct device *dev, void *cpu_addr,
                             size_t size, enum dma_data_direction dir);
    void (*unmap_single)(struct device *dev, dma_addr_t dma_addr,
                        size_t size, enum dma_data_direction dir);
    // 其他回调函数(如 map_sg、unmap_sg 等)
};
  • 功能:定义 DMA 映射/解除映射的硬件相关操作。

(4)、错误处理与调试

1. 典型错误场景
  • 重复解除映射:若 URB 未映射 DMA 地址,调用 unmap_urb_for_dma 会触发 DMA_MAPPING_ERROR 检查。
  • 缓存不一致:未正确同步缓存可能导致 CPU 读取旧数据(常见于 DMA_FROM_DEVICE 场景)。
2. 调试日志
CONFIG_DMA_API_DEBUG=y
  • 日志示例
    unmap_urb_for_dma: Freed DMA mapping 0x12345678 (length 512)
    dma_unmap_sg: Unmapped 2 SG entries for device 0000:00:14.0
    

(5)、性能优化策略

1. 批量解除映射
  • 减少锁竞争:通过 dma_unmap_sg 一次性解除多个 SG 条目,降低锁开销。
2. 预取优化
  • 硬件预取:在 DMA 传输前启用 CPU 预取指令(如 dma_prefetch),减少传输延迟。
3. 内存池管理
  • 预分配内存:使用 dma_pool 预分配连续内存块,避免频繁调用 dma_alloc_coherent


   
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值