4. 流式映射(Streaming Mapping)原理:非一致性、需要显式Cache同步、适合短时传输
流式映射(Streaming DMA Mapping)是PCIe驱动中另一类核心的DMA映射机制。与一致性映射不同,流式映射不保证CPU与设备之间的Cache一致性,驱动必须显式地管理Cache同步操作。这种映射方式专为短时、频繁、非固定缓冲区的数据传输场景设计,在吞吐量和灵活性之间提供了更精细的控制。
核心定义:流式映射是一种“按需分配、用完即走”的DMA映射策略。驱动在每次传输前动态映射缓冲区,传输完成后立即解除映射,并在映射/解映射时通过显式Cache操作保证数据一致性。
4.1 流式映射与非一致性内存模型
流式映射的“非一致性”本质源于现代处理器架构中的Cache层次结构。当CPU通过Cache访问内存时,设备(如PCIe端点)通过DMA直接读写物理内存,两者看到的视图可能不同步。
关键理解:PCIe设备通过DMA直接访问系统内存,不经过CPU Cache。如果CPU在Cache中持有该内存区域的脏数据,设备将读到过时的内容;反之,如果设备写入内存后CPU Cache仍保留旧副本,CPU将看到错误数据。这就是“非一致性”的根本原因。
4.2 显式Cache同步:dma_sync_* 系列API
流式映射要求驱动在关键时间点调用Cache同步函数,确保CPU与设备看到一致的数据视图。Linux内核提供了一组标准的API用于此目的:
| API函数 | 方向 | 调用时机 | 作用 |
|---|---|---|---|
dma_sync_single_for_cpu() |
设备 → CPU | 设备DMA完成后,CPU读取前 | 使Cache失效(Invalidate),确保CPU读到设备写入的最新数据 |
dma_sync_single_for_device() |
CPU → 设备 | CPU写入完成后,设备DMA启动前 | 将Cache脏数据写回内存(Flush/Writeback),确保设备看到CPU写入的数据 |
dma_sync_sg_for_cpu() |
设备 → CPU | Scatter/Gather列表同步 | 对SG列表中的每个缓冲区执行for_cpu同步 |
dma_sync_sg_for_device() |
CPU → 设备 | Scatter/Gather列表同步 | 对SG列表中的每个缓冲区执行for_device同步 |
4.3 流式映射的典型生命周期
一个完整的流式映射DMA传输遵循“映射→同步→启动DMA→等待完成→同步→解映射”的严格流程:
4.4 代码示例:流式映射的典型实现
以下代码展示了一个典型的网络驱动中流式映射的收发流程:
/* 发送路径:CPU写入数据 → 设备DMA读取 */
static int netdev_start_xmit(struct sk_buff *skb, struct net_device *ndev)
{
struct my_priv *priv = netdev_priv(ndev);
dma_addr_t dma_addr;
/* 1. 映射SKB数据缓冲区到DMA地址 */
dma_addr = dma_map_single(priv->dev, skb->data,
skb->len, DMA_TO_DEVICE);
if (dma_mapping_error(priv->dev, dma_addr))
goto err_dma_map;
/* 2. 显式同步:将CPU Cache数据写回内存 */
dma_sync_single_for_device(priv->dev, dma_addr,
skb->len, DMA_TO_DEVICE);
/* 3. 将DMA地址写入设备描述符,触发DMA传输 */
writel(dma_addr, priv->regs + TX_DESC_ADDR);
writel(skb->len, priv->regs + TX_DESC_LEN);
writel(1, priv->regs + TX_START);
/* 4. 设备完成DMA后产生中断,在中断处理中解映射 */
return NETDEV_TX_OK;
err_dma_map:
dev_kfree_skb(skb);
return NETDEV_TX_OK;
}
/* 接收路径:设备DMA写入 → CPU读取 */
static irqreturn_t netdev_rx_irq(int irq, void *data)
{
struct my_priv *priv = data;
struct sk_buff *skb;
dma_addr_t dma_addr;
u32 pkt_len;
/* 从完成队列获取DMA地址和长度 */
dma_addr = priv->rx_dma_addr;
pkt_len = readl(priv->regs + RX_PKT_LEN);
/* 5. 显式同步:使Cache失效,加载设备写入的数据 */
dma_sync_single_for_cpu(priv->dev, dma_addr,
pkt_len, DMA_FROM_DEVICE);
/* 6. CPU安全读取数据 */
skb = build_skb(priv->rx_buf, pkt_len);
netif_receive_skb(skb);
/* 7. 解除映射 */
dma_unmap_single(priv->dev, dma_addr,
priv->rx_buf_size, DMA_FROM_DEVICE);
/* 8. 重新分配接收缓冲区并映射 */
priv->rx_buf = kmalloc(priv->rx_buf_size, GFP_KERNEL);
priv->rx_dma_addr = dma_map_single(priv->dev, priv->rx_buf,
priv->rx_buf_size,
DMA_FROM_DEVICE);
return IRQ_HANDLED;
}
常见错误:
- 遗漏同步:忘记调用
dma_sync_*导致数据不一致,这是流式映射中最隐蔽的bug。 - 同步方向错误:发送路径使用
for_cpu或接收路径使用for_device,导致数据损坏。 - 重复同步:在映射时已经隐含了同步操作(某些架构),再显式调用可能导致性能下降。
- 解映射后访问:调用
dma_unmap_single后继续访问缓冲区,造成use-after-free。
4.5 流式映射 vs 一致性映射:对比总结
| 对比维度 | 流式映射 (Streaming) | 一致性映射 (Coherent) |
|---|---|---|
| Cache一致性 | 不保证,需显式同步 | 硬件保证,无需同步 |
| 映射开销 | 低(动态映射/解映射) | 高(固定映射,可能消耗TLB资源) |
| 同步开销 | 每次传输需2次Cache同步 | 无同步开销 |
| 适用场景 | 短时、频繁、动态缓冲区 | 长时、固定、共享数据结构 |
| 典型应用 | 网络数据包、块设备I/O、USB传输 | 描述符环、控制寄存器、统计计数器 |
| API函数 | dma_map_* / dma_unmap_* + dma_sync_* |
dma_alloc_coherent / dma_free_coherent |
| 内存类型 | 普通内存(可回收、可移动) | 特殊一致性内存(可能非Cacheable) |
4.6 流式映射的最佳实践
- 最小化同步范围:只同步实际传输的数据长度,而非整个缓冲区,减少Cache操作开销。
- 合并同步操作:如果多个小缓冲区连续传输,考虑合并为一次映射和同步。
- 使用SG列表:对于非连续物理内存,使用
dma_map_sg和dma_sync_sg_*批量处理。 - 避免过度同步:在性能关键路径上,评估是否可以通过调整数据流减少同步次数。
- 检查架构差异:不同CPU架构(x86、ARM、RISC-V)的Cache同步开销差异很大,ARM架构通常比x86更昂贵。
性能提示:在x86平台上,由于硬件Cache一致性协议(MESI)较强,
dma_sync_single_for_cpu 在某些情况下可能退化为空操作(NOP)。但驱动代码必须保持架构无关性,不能依赖此行为。在ARM平台上,Cache同步操作通常涉及显式的Cache维护指令(如DC CVAC、DC IVAC),开销不可忽略。