1.从应用层send()到发送到网线上的过程




send之后触发系统调用陷入内核态,调用copy_from_user()的位置:

简述应用层到网线上发送数据的过程:
(1)应用层调用send,系统调用陷入内核态,通过copy_from_user()将原始skb拷贝至内核空间;
(2)抵达网络协议栈,经过TCP和IP分别为skb添加帧头,添加MAC帧头(即以太网帧头)。到此skb准备好了,接下来调用网卡驱动中的ndo_start_xmit();
(3)抵达网卡驱动,进入ndo_start_xmit(),将skb中的数据的虚拟地址转换为DMA地址(即对skb->data调用dma_map_single(),获得物理地址dma_addr),接着将转换好的dma_addr和数据长度写入在ndo_open中准备好的TX Descriptor,并通知网卡数据准备好;
(4)网卡把 TX Descriptor 从 DDR 读取到网卡内部的描述符暂存器中,解析出数据物理地址后网卡的DMA控制器再次发起请求,把 DDR 中的真实数据包搬运到网卡内部的 TX FIFO 缓冲区。
------------------------------至此和CPU已经没有关系了,数据已经发出了---------------------------------------
(5)数据进入 TX FIFO 后,网卡 MAC 芯片实时补充前导码(Preamble)和 CRC 校验码(FCS),最后交给 PHY 芯片转换成电脉冲发往网线。
问题1:TX Descriptor的创建与流转,以及dma_map_single()与dma_alloc_coherent()
在网卡启动时,如ip link set dev eth0 up,调用ndo_open,驱动调用dma_alloc_coherent()在DDR中开辟一段Ring Buffer,作为TX Descriptor槽位,等待后续内容填入。
当数据到达ndo_start_xmit()时,即网络协议栈提交skb后,驱动通过dma_map_single()或dma_alloc_coherent()将skb->data映射为物理地址dma_addr,并dma_addr和数据长度放入上述开辟好的槽位。
最后Doorbell后,网卡将TX Descriptor拉入网卡暂存器。
| DMA 映射方式 | API | 适用场景与工作原理 | 性能与拷贝特征 |
|---|---|---|---|
| 单区映射(Single Mapping) | dma_map_single() | 映射 skb->data 对应的线性连续内存块。产生单个 DMA 物理地址。 | 依赖 CPU 内存分配,适合小包或普通数据包。 |
| 页级分散映射(Page Mapping) | dma_map_page() | 针对零散物理页。支持 Scatter-Gather DMA,一个逻辑包占用多个相邻 TX Descriptor。 | |
| 一致性 DMA 分配(Coherent DMA) | dma_alloc_coherent() | 直接分配一块 CPU 与网卡共享的、关闭 Cache 或保持强缓存一致性的物理连续内存区。 | 专门用于创建 TX/RX Ring Buffer 描述符本身,不用于发送具体的 Payload。 |
| 跨设备缓冲区共享(dma-buf) | dma_buf_attach() | 允许网卡直接从 GPU 显存或 V4L2 采集卡内存中 DMA 拉取数据并发送。 | 跨硬件零拷贝:数据无需经过 CPU 内存中转,适合 AI 训练与视频实时传输。 |
网卡驱动中用到的是前三种,dma_buf_attach() 较为特殊,dma_buf_attach() 函数用于将一个访问方硬件设备(Importer)绑定到指定的 DMA 缓冲区(Exporter)上。其传入的参数是:通过 dma_buf_get(fd)(根据用户态文件描述符获取)或由 Exporter 驱动导出的 struct dma_buf 指针。
问题2:为什么TX Descriptor被设计出Ring Buffer?
- 提高并行效率:CPU可以和DMA并行,CPU 填后面的 TX Descriptor,DMA 同时发送前面的包,形成流水线。
- 减少申请释放开销:Descriptor 在 open() 时一次性申请,之后循环复用,不需要每发一个包都重新申请。
- 方便队列管理:通过 tx_head,tx_tail 管理待发送和已完成的 Descriptor,容易判断 Ring 是否为空或已满。