文章目录
一、Linux 物理内存区
Linux 内核将物理内存划分为 ZONE_DMA、ZONE_NORMAL 和 ZONE_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
)无法直接映射到内核虚拟地址空间,需通过动态映射(如vmalloc
或kmap
)访问,这部分称为 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 分配内存。此时需满足两个条件:- 物理地址连续:DMA 操作需要连续的物理内存。
- 设备支持全地址范围:DMA 控制器必须能访问 ZONE_NORMAL 的物理地址。
非连续内存分配(vmalloc
)
- ZONE_HIGHMEM(32 位):
vmalloc()
分配的虚拟内存可能映射到 ZONE_HIGHMEM,但物理页不要求连续,适用于大块内存或无需 DMA 的场景。
Slab 分配器与对象缓存
- 缓存亲和性:
频繁访问的小对象(如struct page
)优先从 ZONE_NORMAL 分配,减少缓存失效。
分配器 | 虚拟地址范围 | 物理地址连续性 | 适用场景 |
---|---|---|---|
kmalloc | 内核直接映射区 | 连续 | DMA、硬件操作、内核数据结构 |
vmalloc | VMALLOC_START ~ VMALLOC_END | 不连续 | 大块虚拟内存(无需物理连续) |
kmem_cache | 内核直接映射区 | 连续 | 小对象复用(如 struct page ) |
4. 跨架构差异
系统架构 | ZONE_DMA | ZONE_NORMAL | ZONE_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)
)。
- 若设备仅支持 32 位地址,DMA 缓冲区必须位于
- DMA 地址范围:
64 位系统
- 几乎无地址限制:
64 位系统的物理地址空间可达 128TB(48 位物理寻址),高端设备(如 x86-64 CPU)通常支持 40/48/57 位物理地址。- DMA 地址范围:
- 若设备支持 64 位地址,DMA 缓冲区可覆盖几乎全部物理内存。
- 需通过
dma_set_mask()
启用 64 位地址支持(如DMA_BIT_MASK(64)
)。
- DMA 地址范围:
3. 关键硬件组件
PCIe 控制器(Root Port)
- 地址转换能力:
PCIe 控制器需支持地址转换(如 IOMMU),将设备的 DMA 地址映射到系统物理地址。- IOMMU 的作用:
在启用 IOMMU(如 Intel VT-d、AMD-Vi)时,设备 DMA 地址可以是虚拟地址,由 IOMMU 动态映射到物理内存。
- 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 ~ 0xFFFFFFFF | Linux 内核默认支持 |
现代 NVMe SSD(64 位) | 0x00000000 ~ 0x7FFFFFFFFFFF (48 位) | 需启用 IOMMU 和 64 位 DMA mask |
GPU(64 位地址) | 0x0000000000000000 |
三、反弹缓冲区(Bounce Buffer)
DMA反弹缓冲区(Bounce Buffer)是解决 DMA设备与CPU内存访问地址不兼容问题 的关键技术,其核心目标是通过中间缓冲区实现数据的安全传输。以下是其原理、实现细节及应用场景的深度解析:
1. 核心原理
(1) 工作流程
- 数据复制:将原始缓冲区(不可访问区域)的数据复制到反弹缓冲区(设备可访问区域)。
- DMA传输:设备通过反弹缓冲区完成数据读写。
- 反向复制(可选):若需将数据返回给原始缓冲区,需再次复制。
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)、函数功能概述
-
核心目标
- 为 URB 的 控制包(Setup Packet) 和 数据缓冲区(Transfer Buffer) 分配 DMA 映射。
- 根据硬件限制(如本地内存池或系统 DMA 支持)选择映射方式。
-
关键场景
- 控制传输:映射 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)、核心设计思想
-
硬件兼容性
- 本地内存池:针对 DMA 地址受限的控制器(如仅支持小容量 SRAM),通过
hcd_alloc_coherent
分配一致性内存。 - 系统 DMA:通用场景下使用
dma_map_*
接口,依赖 CPU 缓存一致性协议。
- 本地内存池:针对 DMA 地址受限的控制器(如仅支持小容量 SRAM),通过
-
性能优化
- 分散/聚集合并:通过
URB_DMA_MAP_SG_COMBINED
标志减少 DMA 事务次数。 - 零拷贝:直接传递用户空间缓冲区(需确保物理连续性)。
- 分散/聚集合并:通过
-
安全性保障
- 栈内存检查:防止 DMA 访问无效的栈内存地址。
- 错误回滚:映射失败时释放已分配资源,避免内存泄漏。
(4)、关键数据结构与标志
结构体/标志 | 作用 |
---|---|
URB_SETUP_MAP_LOCAL | 标记 Setup 包使用本地内存池分配 |
URB_DMA_MAP_SG | 表示数据缓冲区使用分散/聚集映射 |
URB_DMA_MAP_SINGLE | 表示数据缓冲区使用单页 DMA 映射 |
hcd->localmem_pool | 指向本地内存池(如 SRAM),用于受限 DMA 场景 |
(5)、典型调用场景
-
批量数据传输
// 分配 4KB 连续内存并映射 urb->transfer_buffer = kmalloc(4096, GFP_KERNEL); ret = usb_hcd_map_urb_for_dma(hcd, urb, GFP_KERNEL);
-
分散/聚集传输
// 初始化 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_coherent
是 DMA(直接内存访问)操作的核心函数,用于分配物理连续且与设备 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. 内存分配流程
- 物理页分配:
调用__get_free_pages
或dma_alloc_attrs
分配物理连续的页(可能通过 CMA、SWIOTLB 或 buddy 系统)。 - 虚拟地址映射:
通过virt_to_bus
或平台特定函数(如page_to_bus
)将虚拟地址转换为总线地址。 - 缓存处理:
- 禁用缓存:通过页表标记(如
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 缓存中的对应区域无效,强制下次读取时从内存获取最新数据。
- DMA_TO_DEVICE:调用
(2) 地址转换
- 通过
virt_to_dma(dev, cpu_addr)
将 CPU 虚拟地址转换为总线地址(Bus Address),供设备直接访问。
(3) 错误检查
- 调用
valid_dma_direction(dir)
验证传输方向合法性。 - 检查内存地址和大小是否在有效范围内(
virt_addr_valid
)。
4. 使用流程
- 分配内存:通过
vmalloc ?
或dma_alloc_coherent
分配物理连续的内存。 - 映射:调用
dma_map_single
获取总线地址。 - 启动 DMA 传输:将总线地址传递给设备驱动。
- 解除映射:传输完成后调用
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);
}
- 关键步骤:
- 地址验证:检查设备是否支持该 DMA 地址范围(通过
dma_capable()
)。 - 页表映射:调用
remap_pfn_range
将物理页映射到用户虚拟地址空间。
- 地址验证:检查设备是否支持该 DMA 地址范围(通过
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_prot
为PROT_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_mmap
是 IOMMU(输入/输出内存管理单元)与 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);
}
- 关键步骤:
- 地址验证:检查设备是否支持该 DMA 地址范围(通过
iommu_dma_capable()
)。 - 页表映射:调用 IOMMU 驱动(如
intel_iommu_map_page
)建立虚拟地址到物理地址的映射。
- 地址验证:检查设备是否支持该 DMA 地址范围(通过
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_dma
是 USB 子系统用于解除 URB(USB 请求块)的 DMA 映射的核心函数,其核心作用是释放 URB 传输过程中分配的 DMA 资源,并确保 CPU 与设备之间的缓存一致性。以下从功能、实现流程、关键数据结构及代码逻辑进行深入分析:
(1)、函数功能与设计目标
1. 核心功能
- 解除 DMA 映射:释放 URB 的
transfer_buffer
或sg
列表对应的 DMA 地址。 - 缓存同步:根据传输方向(
DMA_TO_DEVICE
或DMA_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
。
☆