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 数据包发送完整流程

从协议栈到硬件发送的路径涉及多个层次,驱动开发者需理解以下关键步骤:

  1. 协议栈封装:上层协议(TCP/UDP)将数据封装为 sk_buff,通过 dev_queue_xmit 进入网络设备子系统。
  2. 排队规则:经过 qdisc(排队规则)处理,选择发送队列。
  3. 调度发送:内核调用 dev_hard_start_xmit,最终执行驱动的 ndo_start_xmit。
  4. DMA 映射:驱动将 skb->data 映射为 DMA 地址,供硬件直接读取。
  5. 描述符提交:填充 TX 描述符(地址、长度、校验和卸载标志),更新硬件尾指针。
  6. 触发发送:写寄存器通知硬件开始发送。
  7. 完成中断:硬件发送完成后触发中断,驱动回收 sk_buff 并解除 DMA 映射。
性能要点: 现代驱动通常使用 skb_orphan 在发送前解除 sk_buff 与套接字的关联,避免 TCP 重传计时器误判。同时利用 skb->destructor 回调实现零拷贝发送。
数据包发送流程(协议栈 → 硬件) 协议栈 dev_queue_xmit qdisc 排队规则 dev_hard_start_xmit ndo_start_xmit (驱动) DMA 映射 + 描述符提交 硬件发送 + 完成中断 驱动关注点 • 不可睡眠,快速返回 • 管理 TX 描述符环 • DMA 映射与解映射 • 校验和卸载(TSO/GSO) • 发送队列停/启 • sk_buff 生命周期管理 • 完成中断处理 • 统计计数更新

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 回收

硬件发送完成后,驱动在中断处理函数中执行以下操作:

  1. 读取硬件完成寄存器,获取已发送的描述符数量。
  2. 遍历已完成的描述符,获取对应的 sk_buff 指针和 DMA 地址。
  3. 调用 dma_unmap_single 解除 DMA 映射。
  4. 调用 dev_kfree_skb_any 释放 sk_buff。
  5. 更新 priv->tx_clean 指针。
  6. 如果之前调用了 netif_stop_queue,调用 netif_wake_queue 唤醒发送队列。
sk_buff 与 TX 描述符环关系 sk_buff data ──────────► len = 1514 ip_summed destructor 数据缓冲区 (skb->data) TX 描述符环(环形缓冲区) Desc 0 addr: DMA_A len: 1514 flags: CSUM Desc 1 addr: DMA_B len: 64 flags: 0 Desc 2 addr: DMA_C len: 1200 flags: TSO Desc 3 addr: DMA_D len: 500 flags: 0 尾指针 → 3 硬件从此处开始发送

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 缓冲区。