3. ndo_start_xmit 发送接口:函数原型、数据包发送流程、sk_buff 结构体操作
3.1 函数原型与注册机制
ndo_start_xmit 是网络设备驱动中负责将数据包从内核协议栈发送到硬件设备的回调函数。该函数通过 net_device_ops 结构体注册,在协议栈调用 dev_hard_start_xmit 时被间接触发。
| 项目 | 说明 |
|---|---|
| 函数原型 | netdev_tx_t (*ndo_start_xmit)(struct sk_buff *skb, struct net_device *dev); |
| 返回值 | NETDEV_TX_OK(发送成功)或 NETDEV_TX_BUSY(发送队列满,需重试) |
| 注册方式 | 在 struct net_device_ops 中赋值 .ndo_start_xmit = my_start_xmit |
| 调用上下文 | 持有 RCU 读锁,可能处于软中断或进程上下文,不能睡眠 |
| 锁要求 | 驱动需自行保护发送描述符环(TX Ring)的并发访问 |
关键约束: 该函数必须快速返回。若硬件队列已满,应返回
NETDEV_TX_BUSY 并停止发送队列(netif_stop_queue),待硬件完成中断后再唤醒(netif_wake_queue)。
3.2 数据包发送完整流程
从协议栈到硬件发送的路径涉及多个层次,驱动开发者需理解以下关键步骤:
- 协议栈封装:上层协议(TCP/UDP)将数据封装为
sk_buff,通过dev_queue_xmit进入网络设备子系统。 - 排队规则:经过
qdisc(排队规则)处理,选择发送队列。 - 调度发送:内核调用
dev_hard_start_xmit,最终执行驱动的ndo_start_xmit。 - DMA 映射:驱动将
skb->data映射为 DMA 地址,供硬件直接读取。 - 描述符提交:填充 TX 描述符(地址、长度、校验和卸载标志),更新硬件尾指针。
- 触发发送:写寄存器通知硬件开始发送。
- 完成中断:硬件发送完成后触发中断,驱动回收
sk_buff并解除 DMA 映射。
性能要点: 现代驱动通常使用
skb_orphan 在发送前解除 sk_buff 与套接字的关联,避免 TCP 重传计时器误判。同时利用 skb->destructor 回调实现零拷贝发送。
3.3 sk_buff 结构体操作
在 ndo_start_xmit 中,驱动需要从 sk_buff 中提取发送所需信息,并在发送完成后正确释放。以下是最常用的操作:
| 操作 | API / 宏 | 说明 |
|---|---|---|
| 获取数据指针 | skb->data |
指向当前协议头后的有效载荷起始位置 |
| 获取数据长度 | skb->len |
整个 sk_buff 的有效数据长度(字节) |
| 获取头部长度 | skb_headlen(skb) |
线性数据区长度(非分片部分) |
| 分片遍历 | skb_walk_frags(skb, frag) |
遍历 skb_shared_info 中的分片(frag) |
| DMA 映射 | dma_map_single / dma_map_page |
将线性数据或分片映射为 DMA 地址 |
| 校验和卸载 | skb->ip_summed |
检查 CHECKSUM_PARTIAL 标志,设置硬件校验和卸载 |
| GSO/TSO 信息 | skb_shinfo(skb)->gso_size |
获取 GSO 分段大小,用于硬件 TSO 卸载 |
| 释放 sk_buff | dev_kfree_skb_any(skb) |
发送完成后释放(可在中断或软中断中安全调用) |
| 解除 DMA 映射 | dma_unmap_single / dma_unmap_page |
在完成中断中调用,释放 DMA 资源 |
3.4 典型驱动发送实现模板
以下代码展示了一个简化的 ndo_start_xmit 实现,包含核心操作步骤:
static netdev_tx_t my_start_xmit(struct sk_buff *skb, struct net_device *dev)
{
struct my_priv *priv = netdev_priv(dev);
struct my_tx_desc *desc;
dma_addr_t dma_addr;
u32 idx;
// 1. 检查 TX 队列是否可用
if (my_tx_ring_full(priv)) {
netif_stop_queue(dev);
return NETDEV_TX_BUSY;
}
// 2. 获取下一个可用描述符索引
idx = priv->tx_next;
desc = &priv->tx_ring[idx];
// 3. 映射线性数据区到 DMA 地址
dma_addr = dma_map_single(priv->dev, skb->data, skb->len, DMA_TO_DEVICE);
if (dma_mapping_error(priv->dev, dma_addr)) {
dev_kfree_skb_any(skb);
return NETDEV_TX_OK;
}
// 4. 填充硬件描述符
desc->addr = cpu_to_le32(dma_addr);
desc->len = cpu_to_le16(skb->len);
desc->flags = 0;
// 5. 处理校验和卸载(如果硬件支持)
if (skb->ip_summed == CHECKSUM_PARTIAL) {
desc->flags |= TX_DESC_CSUM;
// 设置 L4 偏移等硬件特定字段
}
// 6. 保存 skb 指针用于完成时释放
priv->tx_skb[idx] = skb;
priv->tx_dma[idx] = dma_addr;
// 7. 更新尾指针,通知硬件
priv->tx_next = (idx + 1) % TX_RING_SIZE;
writel(priv->tx_next, priv->regs + TX_TAIL_REG);
// 8. 如果队列已满,停止发送队列
if (my_tx_ring_full(priv))
netif_stop_queue(dev);
return NETDEV_TX_OK;
}
常见错误:
- 在
ndo_start_xmit中调用dev_kfree_skb(非 any 版本),可能导致在中断上下文死锁。 - DMA 映射后未检查
dma_mapping_error,导致硬件访问无效地址。 - 忘记在完成中断中解除 DMA 映射,造成 DMA 泄漏。
- 在
NETDEV_TX_BUSY返回前未调用netif_stop_queue,导致协议栈反复重试。
3.5 完成中断处理与 sk_buff 回收
硬件发送完成后,驱动在中断处理函数中执行以下操作:
- 读取硬件完成寄存器,获取已发送的描述符数量。
- 遍历已完成的描述符,获取对应的
sk_buff指针和 DMA 地址。 - 调用
dma_unmap_single解除 DMA 映射。 - 调用
dev_kfree_skb_any释放sk_buff。 - 更新
priv->tx_clean指针。 - 如果之前调用了
netif_stop_queue,调用netif_wake_queue唤醒发送队列。
3.6 性能优化要点
- 批量提交:累积多个描述符后一次性更新尾指针,减少 PCIe 事务。
- 预分配 sk_buff:在 TX 完成中断中复用
sk_buff,避免频繁分配/释放。 - 使用
skb_orphan:在ndo_start_xmit中调用skb_orphan(skb),断开与套接字的关联,避免 TCP 重传误判。 - 避免
NETDEV_TX_BUSY:通过提前停止队列(在队列满之前预留一个描述符位置)减少 BUSY 返回。 - DMA 映射缓存:对于小数据包,使用
dma_pool预分配 DMA 缓冲区。