4. 流式映射(Streaming Mapping)原理:非一致性、需要显式Cache同步、适合短时传输

流式映射(Streaming DMA Mapping)是PCIe驱动中另一类核心的DMA映射机制。与一致性映射不同,流式映射不保证CPU与设备之间的Cache一致性,驱动必须显式地管理Cache同步操作。这种映射方式专为短时、频繁、非固定缓冲区的数据传输场景设计,在吞吐量和灵活性之间提供了更精细的控制。

核心定义:流式映射是一种“按需分配、用完即走”的DMA映射策略。驱动在每次传输前动态映射缓冲区,传输完成后立即解除映射,并在映射/解映射时通过显式Cache操作保证数据一致性。

4.1 流式映射与非一致性内存模型

流式映射的“非一致性”本质源于现代处理器架构中的Cache层次结构。当CPU通过Cache访问内存时,设备(如PCIe端点)通过DMA直接读写物理内存,两者看到的视图可能不同步。

流式映射非一致性模型 CPU Core Cache (L1/L2/L3) Memory Controller System Memory (DMA Buffer) PCIe Endpoint (DMA Master) load/store cache line fill/evict DMA Read/Write (绕过Cache) ⚠ Cache与内存视图不一致 CPU看到旧数据 / 设备看到脏数据
关键理解: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→等待完成→同步→解映射”的严格流程:

流式映射DMA传输生命周期 ① dma_map_single() 获取DMA地址 + 建立映射 ② dma_sync_for_device() Cache Writeback (CPU→设备) ③ 启动DMA传输 设备读写系统内存 ④ 等待DMA完成 中断/轮询检查完成状态 ⑤ dma_sync_for_cpu() Cache Invalidate (设备→CPU) ⑥ dma_unmap_single() 解除映射,释放资源 关键规则 • 映射后、DMA前:必须调用 for_device 将CPU写入的数据刷到内存 • DMA完成后、CPU读取前:必须调用 for_cpu 使Cache失效,加载设备写入的新数据 • 同步操作有方向性: for_device = Writeback (Clean) for_cpu = Invalidate • 解映射前必须确保DMA已停止 否则可能导致数据损坏或系统崩溃 • 流式映射不保证Cache一致性 驱动必须显式管理同步 • 适用于短时、频繁的传输场景 如网络数据包、块设备I/O

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),开销不可忽略。