3. ndo 接口基础:struct net_device_ops 结构体

在 Linux 内核网络设备驱动中,struct net_device_ops 是连接内核网络协议栈与硬件驱动行为的核心接口表。每个网络设备驱动都必须填充该结构体,向内核注册其操作函数指针(ndo:net device operations)。内核通过调用这些函数指针来触发设备的打开、关闭、数据发送等关键行为。

核心概念: ndo 接口是内核网络子系统对设备驱动的“回调契约”。驱动实现这些函数,内核在适当时机调用它们。

3.1 struct net_device_ops 结构体定义

该结构体定义在 include/linux/netdevice.h 中,包含数十个函数指针成员。以下是其核心成员的精简定义:

struct net_device_ops {
    int                     (*ndo_open)(struct net_device *dev);
    int                     (*ndo_stop)(struct net_device *dev);
    netdev_tx_t             (*ndo_start_xmit)(struct sk_buff *skb,
                                              struct net_device *dev);
    int                     (*ndo_set_rx_mode)(struct net_device *dev);
    int                     (*ndo_change_mtu)(struct net_device *dev, int new_mtu);
    int                     (*ndo_set_mac_address)(struct net_device *dev, void *addr);
    int                     (*ndo_validate_addr)(struct net_device *dev);
    int                     (*ndo_do_ioctl)(struct net_device *dev,
                                            struct ifreq *ifr, int cmd);
    int                     (*ndo_tx_timeout)(struct net_device *dev,
                                              unsigned int txqueue);
    // ... 更多成员
};
驱动开发要点: 并非所有成员都必须实现。对于简单设备,只需实现 ndo_open、ndo_stop、ndo_start_xmit 三个核心函数即可使设备基本可用。

3.2 三个核心 ndo 函数指针详解

在众多 ndo 函数指针中,以下三个是每个网络设备驱动都必须实现的“铁三角”:

函数指针 原型 调用时机 职责
ndo_open int (*ndo_open)(struct net_device *dev) 用户执行 ifconfig up 或 ip link set dev up 时 申请硬件资源、注册中断、启动 TX/RX 队列、启用 NAPI
ndo_stop int (*ndo_stop)(struct net_device *dev) 用户执行 ifconfig down 或 ip link set dev down 时 释放硬件资源、注销中断、停止 TX/RX 队列、禁用 NAPI
ndo_start_xmit netdev_tx_t (*ndo_start_xmit)(struct sk_buff *skb, struct net_device *dev) 内核协议栈有数据包需要发送时 将 sk_buff 中的数据通过硬件发送出去

3.3 ndo_open 与 ndo_stop 的典型实现模式

这两个函数通常成对出现,负责设备生命周期中的“上电”与“下电”操作。以下是典型的实现骨架:

static int my_ndo_open(struct net_device *ndev)
{
    struct my_priv *priv = netdev_priv(ndev);
    int ret;

    // 1. 申请 DMA 缓冲区、描述符环等硬件资源
    ret = my_alloc_resources(priv);
    if (ret)
        return ret;

    // 2. 注册中断处理函数(通常使用 MSI-X 或共享中断)
    ret = request_irq(priv->irq, my_interrupt_handler, 0,
                      ndev->name, ndev);
    if (ret)
        goto err_free_res;

    // 3. 启用 NAPI(与 NAPI 收包机制配合)
    napi_enable(&priv->napi);

    // 4. 启动硬件 TX/RX 引擎
    my_hw_start(priv);

    // 5. 通知内核网络栈:设备已就绪
    netif_start_queue(ndev);

    return 0;

err_free_res:
    my_free_resources(priv);
    return ret;
}

static int my_ndo_stop(struct net_device *ndev)
{
    struct my_priv *priv = netdev_priv(ndev);

    // 1. 停止内核 TX 队列(防止新数据包进入)
    netif_stop_queue(ndev);

    // 2. 停止硬件引擎
    my_hw_stop(priv);

    // 3. 禁用 NAPI(确保不再调度收包)
    napi_disable(&priv->napi);

    // 4. 注销中断
    free_irq(priv->irq, ndev);

    // 5. 释放硬件资源
    my_free_resources(priv);

    return 0;
}
常见错误: 在 ndo_stop 中忘记调用 netif_stop_queue,会导致内核在设备关闭后仍尝试发送数据包,引发 NULL 指针解引用或硬件访问异常。

3.4 ndo_start_xmit:数据发送的核心路径

ndo_start_xmit 是网络数据发送的“最后一公里”。内核协议栈将封装好的 sk_buff 传递给该函数,驱动负责将其送入硬件发送队列。典型实现流程如下:

  1. 获取私有数据:通过 netdev_priv(ndev) 获取驱动私有结构体。
  2. 检查发送队列状态:若硬件 TX 描述符已满,调用 netif_stop_queue 暂停上层发送。
  3. 映射 DMA 缓冲区:将 skb->data 映射为 DMA 地址,供硬件直接访问。
  4. 填充 TX 描述符:将 DMA 地址、长度等信息写入硬件描述符环。
  5. 触发硬件发送:写寄存器或 doorbell 通知硬件开始发送。
  6. 释放或推送 sk_buff:若硬件已复制数据,可调用 dev_kfree_skb;否则需在发送完成中断中释放。
static netdev_tx_t my_ndo_start_xmit(struct sk_buff *skb,
                                     struct net_device *ndev)
{
    struct my_priv *priv = netdev_priv(ndev);
    struct my_tx_desc *desc;
    dma_addr_t dma_addr;
    int idx;

    // 1. 检查 TX 队列是否已满
    if (my_tx_full(priv)) {
        netif_stop_queue(ndev);
        return NETDEV_TX_BUSY;  // 告诉内核稍后重试
    }

    // 2. 获取下一个可用 TX 描述符索引
    idx = priv->tx_next_idx;
    desc = &priv->tx_ring[idx];

    // 3. 映射 skb 数据到 DMA 地址
    dma_addr = dma_map_single(&ndev->dev, skb->data,
                              skb->len, DMA_TO_DEVICE);
    if (dma_mapping_error(&ndev->dev, dma_addr))
        goto err_dma_map;

    // 4. 填充描述符
    desc->addr = dma_addr;
    desc->len  = skb->len;
    desc->flags = TX_DESC_FLAG_LAST;  // 标记为最后一个片段

    // 5. 保存 skb 指针(用于发送完成中断中释放)
    priv->tx_skb[idx] = skb;

    // 6. 更新索引并触发硬件发送
    priv->tx_next_idx = (idx + 1) % TX_RING_SIZE;
    my_hw_kick(priv);  // 写 doorbell 寄存器

    return NETDEV_TX_OK;

err_dma_map:
    dev_kfree_skb(skb);
    return NETDEV_TX_OK;
}
返回值说明: NETDEV_TX_OK 表示驱动已接管 sk_buff(无论成功与否);NETDEV_TX_BUSY 表示队列满,内核会稍后重试。现代驱动应尽量避免返回 BUSY,优先使用 netif_stop_queue 进行流控。

3.5 ndo 接口与内核网络栈的交互流程

下图展示了从用户态发起网络操作到内核调用 ndo 函数的完整链路:

ndo 接口与内核网络栈交互流程 用户态工具 ifconfig / ip 内核系统调用 dev_change_flags 网络协议栈 dev_open / dev_close ndo 接口层 ndo_open / ndo_stop 驱动实现 my_ndo_open / my_ndo_stop 应用程序 socket send TCP/UDP 层 构造 sk_buff ndo_start_xmit 驱动发送入口 硬件发送 DMA / 写寄存器 左侧:设备生命周期控制(open/stop) | 右侧:数据发送路径(start_xmit)

3.6 ndo 接口注册与绑定

驱动在探测函数(probe)中分配 net_device 后,需要将实现好的 ndo 函数集绑定到设备上:

static const struct net_device_ops my_netdev_ops = {
    .ndo_open       = my_ndo_open,
    .ndo_stop       = my_ndo_stop,
    .ndo_start_xmit = my_ndo_start_xmit,
    .ndo_set_rx_mode = my_ndo_set_rx_mode,
    .ndo_change_mtu  = my_ndo_change_mtu,
    .ndo_tx_timeout  = my_ndo_tx_timeout,
    // 其他成员按需赋值
};

static int my_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
    struct net_device *ndev;
    struct my_priv *priv;

    // 1. 分配 net_device(包含私有数据区)
    ndev = alloc_etherdev(sizeof(struct my_priv));
    if (!ndev)
        return -ENOMEM;

    // 2. 绑定 ndo 操作表
    ndev->netdev_ops = &my_netdev_ops;

    // 3. 设置其他必要字段(MAC 地址、特性标志等)
    // ...

    // 4. 注册到内核网络子系统
    ret = register_netdev(ndev);
    if (ret)
        goto err_free;

    return 0;

err_free:
    free_netdev(ndev);
    return ret;
}
最佳实践: 将 net_device_ops 定义为 static const 结构体,放在文件作用域中。这既保证了只读安全性,也便于代码审查和维护。

3.7 常用 ndo 函数指针速查表

除了核心三函数外,以下 ndo 成员在实际驱动中也频繁使用:

函数指针 用途 若不实现的影响
ndo_set_rx_mode 设置设备接收模式(单播/多播/混杂模式) 多播和混杂模式无法正常工作
ndo_change_mtu 修改设备 MTU 值 使用内核默认 MTU,用户无法修改
ndo_set_mac_address 修改设备 MAC 地址 MAC 地址不可更改
ndo_validate_addr 验证 MAC 地址是否合法 使用内核默认验证逻辑
ndo_tx_timeout 发送超时处理(看门狗) 发送卡死时无法自动恢复
ndo_do_ioctl 处理自定义 ioctl 命令 不支持私有 ioctl 接口
ndo_get_stats64 获取设备统计信息(64位) 统计信息不准确或缺失
调试技巧: 在 ndo_open 和 ndo_stop 中添加 netdev_info(ndev, "device %s\n", __func__); 日志,可以清晰追踪设备状态切换过程,是调试驱动初始化问题的第一道防线。