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 传递给该函数,驱动负责将其送入硬件发送队列。典型实现流程如下:
- 获取私有数据:通过
netdev_priv(ndev)获取驱动私有结构体。 - 检查发送队列状态:若硬件 TX 描述符已满,调用
netif_stop_queue暂停上层发送。 - 映射 DMA 缓冲区:将
skb->data映射为 DMA 地址,供硬件直接访问。 - 填充 TX 描述符:将 DMA 地址、长度等信息写入硬件描述符环。
- 触发硬件发送:写寄存器或 doorbell 通知硬件开始发送。
- 释放或推送 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 函数的完整链路:
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__); 日志,可以清晰追踪设备状态切换过程,是调试驱动初始化问题的第一道防线。