1. Linux网络设备驱动框架概述
在Linux内核中,网络设备驱动是连接硬件与协议栈的关键桥梁。与字符设备和块设备不同,网络设备驱动采用独特的架构设计,需要处理数据包的收发、中断处理、DMA操作等复杂场景。一个完整的网络设备驱动需要实现设备枚举、内核注册、资源挂载三大核心流程,这也是驱动开发者必须掌握的"三板斧"。
我曾在多个嵌入式项目中调试过各种网卡驱动,从最简单的DM9000到复杂的Intel千兆网卡。无论硬件如何变化,Linux内核为网络设备驱动提供的框架始终保持一致。这种一致性正是Linux驱动模型最精妙的设计——硬件细节被抽象化,驱动开发者只需关注与具体硬件交互的部分。
2. 网络设备驱动核心数据结构
2.1 net_device结构体剖析
net_device是网络设备在内核中的"身份证",驱动必须正确初始化这个结构体的各个字段。关键字段包括:
c复制struct net_device {
char name[IFNAMSIZ]; // 设备名如eth0
unsigned long mem_end; // 共享内存结束地址
unsigned long mem_start; // 共享内存起始地址
unsigned long base_addr; // I/O基地址
unsigned int irq; // 中断号
const struct net_device_ops *netdev_ops; // 操作函数集
const struct ethtool_ops *ethtool_ops; // ethtool支持
unsigned int mtu; // 最大传输单元
unsigned char perm_addr[MAX_ADDR_LEN]; // MAC地址
unsigned char addr_len; // 硬件地址长度
// ... 其他字段省略
};
实际开发中最容易出错的是mem_start/mem_end的设置。我曾遇到过一个案例:驱动工作不稳定,时断时续。最终发现是mem_end设置比实际内存区域小了4字节,导致DMA操作越界。
2.2 操作函数集net_device_ops
这是驱动功能的实际实现,主要包含:
c复制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);
void (*ndo_set_rx_mode)(struct net_device *dev);
// ... 其他操作函数
};
在嵌入式开发中,至少需要实现open、stop和start_xmit三个基本操作。start_xmit是最核心的函数,它负责将sk_buff中的数据包发送到硬件:
c复制static netdev_tx_t mynet_start_xmit(struct sk_buff *skb,
struct net_device *dev)
{
struct mynet_priv *priv = netdev_priv(dev);
// 1. 检查DMA描述符是否可用
if (dma_desc_full(priv)) {
netif_stop_queue(dev);
return NETDEV_TX_BUSY;
}
// 2. 映射skb到DMA区域
priv->tx_skb[priv->tx_next] = skb;
dma_map_single(&priv->pdev->dev, skb->data, skb->len,
DMA_TO_DEVICE);
// 3. 触发硬件发送
start_dma_transfer(priv);
return NETDEV_TX_OK;
}
3. 设备枚举与探测流程
3.1 总线枚举机制
Linux内核通过总线核心(bus core)管理所有设备枚举。对于PCIe网卡,典型枚举流程如下:
- BIOS/UEFI初始化时枚举PCIe设备
- Linux内核启动时扫描PCI配置空间
- 对每个设备检查vendor/device ID
- 匹配驱动程序的id_table
以Intel IGB驱动为例,其ID表定义如下:
c复制static const struct pci_device_id igb_pci_tbl[] = {
{ PCI_VDEVICE(INTEL, E1000_DEV_ID_I354_BACKPLANE_1GBPS) },
{ PCI_VDEVICE(INTEL, E1000_DEV_ID_I354_SGMII) },
{ PCI_VDEVICE(INTEL, E1000_DEV_ID_I354_BACKPLANE_2_5GBPS) },
// ... 其他设备ID
{0,}
};
3.2 探测函数实现
探测函数(probe)是驱动初始化的起点,需要完成:
- 启用PCI设备
- 申请I/O资源和IRQ
- 分配net_device结构
- 初始化私有数据结构
- 注册网络设备
关键代码示例:
c复制static int mynet_probe(struct pci_dev *pdev,
const struct pci_device_id *ent)
{
struct net_device *dev;
struct mynet_priv *priv;
int err;
// 1. 启用PCI设备
err = pci_enable_device(pdev);
if (err) return err;
// 2. 申请内存区域
err = pci_request_regions(pdev, DRV_NAME);
if (err) goto err_out;
// 3. 设置DMA掩码
err = pci_set_dma_mask(pdev, DMA_BIT_MASK(64));
if (err) {
err = pci_set_dma_mask(pdev, DMA_BIT_MASK(32));
if (err) goto err_out_free;
}
// 4. 分配net_device
dev = alloc_etherdev(sizeof(struct mynet_priv));
if (!dev) {
err = -ENOMEM;
goto err_out_free;
}
// 5. 初始化私有数据
priv = netdev_priv(dev);
priv->pdev = pdev;
priv->regs = pci_iomap(pdev, 0, 0);
// 6. 设置操作函数
dev->netdev_ops = &mynet_netdev_ops;
// 7. 注册设备
err = register_netdev(dev);
if (err) goto err_out_free;
return 0;
err_out_free:
// 错误处理代码...
err_out:
return err;
}
在ARM平台上,我曾遇到一个隐蔽的bug:DMA操作随机失败。最终发现是忘记调用dma_set_coherent_mask(),导致缓存一致性问题。这个教训让我养成了在probe函数中严格检查所有DMA相关设置的习惯。
4. 设备注册与初始化
4.1 注册网络设备
register_netdev()是驱动注册的最终步骤,内核会:
- 分配唯一的设备名(ethX)
- 将设备加入全局设备列表
- 在sysfs中创建设备属性文件
- 发送uevent通知用户空间
注册后,驱动需要处理以下关键任务:
- 实现ethtool操作(用于ifconfig等工具)
- 支持SIOCxxx系列ioctl命令
- 处理网络命名空间(如果启用)
4.2 初始化的黄金法则
根据我的经验,可靠的初始化应遵循以下顺序:
- 硬件复位(确保确定状态)
- 初始化DMA引擎和描述符环
- 配置MAC地址
- 设置中断处理
- 启动接收队列
典型的初始化代码:
c复制static int mynet_hw_init(struct mynet_priv *priv)
{
// 1. 硬件复位
writel(SW_RESET, priv->regs + REG_CTRL);
msleep(100);
// 2. 初始化TX/RX描述符环
priv->tx_ring = dma_alloc_coherent(
&priv->pdev->dev,
TX_RING_SIZE * sizeof(struct mynet_desc),
&priv->tx_ring_dma, GFP_KERNEL);
// 3. 配置MAC地址
mynet_set_mac(priv, priv->dev->dev_addr);
// 4. 设置中断
writel(INT_ENABLE_ALL, priv->regs + REG_INT_MASK);
// 5. 启动接收
writel(RX_ENABLE, priv->regs + REG_RX_CTRL);
return 0;
}
5. 中断处理与数据收发
5.1 中断处理最佳实践
网络设备中断通常处理:
- 数据包接收完成
- 数据包发送完成
- 错误条件(如DMA错误)
高效的中断处理应:
- 快速判断中断源
- 禁用已处理的中断
- 调度NAPI处理接收
- 唤醒发送队列(如果之前停止)
示例中断处理程序:
c复制static irqreturn_t mynet_interrupt(int irq, void *dev_id)
{
struct net_device *dev = dev_id;
struct mynet_priv *priv = netdev_priv(dev);
u32 status;
// 1. 读取中断状态
status = readl(priv->regs + REG_INT_STATUS);
if (!status) return IRQ_NONE;
// 2. 禁用中断(在NAPI中重新启用)
writel(0, priv->regs + REG_INT_MASK);
// 3. 处理接收中断
if (status & INT_RX_DONE) {
if (napi_schedule_prep(&priv->napi)) {
__napi_schedule(&priv->napi);
}
}
// 4. 处理发送完成
if (status & INT_TX_DONE) {
mynet_tx_clean(priv);
if (netif_queue_stopped(dev) &&
!dma_desc_full(priv))
netif_wake_queue(dev);
}
return IRQ_HANDLED;
}
5.2 NAPI接收处理
现代网络驱动都应使用NAPI(New API)接收数据,它通过混合中断和轮询提高效率:
c复制static int mynet_poll(struct napi_struct *napi, int budget)
{
struct mynet_priv *priv = container_of(napi,
struct mynet_priv, napi);
struct net_device *dev = priv->dev;
int work_done = 0;
// 1. 处理接收数据包
while (work_done < budget) {
struct sk_buff *skb = mynet_rx(dev);
if (!skb) break;
netif_receive_skb(skb);
work_done++;
}
// 2. 如果处理了所有数据包,退出NAPI
if (work_done < budget) {
napi_complete(napi);
writel(INT_ENABLE_ALL, priv->regs + REG_INT_MASK);
}
return work_done;
}
6. 资源挂载与卸载
6.1 设备挂载的完整流程
驱动卸载时需要严格逆序释放资源:
- 注销网络设备(unregister_netdev)
- 释放中断和I/O资源
- 释放DMA缓冲区
- 取消映射PCI内存
- 禁用PCI设备
典型remove函数:
c复制static void mynet_remove(struct pci_dev *pdev)
{
struct net_device *dev = pci_get_drvdata(pdev);
struct mynet_priv *priv = netdev_priv(dev);
// 1. 注销网络设备
unregister_netdev(dev);
// 2. 释放NAPI
netif_napi_del(&priv->napi);
// 3. 释放DMA缓冲区
dma_free_coherent(&pdev->dev,
TX_RING_SIZE * sizeof(struct mynet_desc),
priv->tx_ring, priv->tx_ring_dma);
// 4. 取消I/O映射
pci_iounmap(pdev, priv->regs);
// 5. 释放PCI资源
pci_release_regions(pdev);
pci_disable_device(pdev);
// 6. 释放net_device
free_netdev(dev);
}
6.2 热插拔支持
对于支持热插拔的设备,还需要实现:
c复制static struct pci_driver mynet_driver = {
.name = DRV_NAME,
.id_table = mynet_pci_tbl,
.probe = mynet_probe,
.remove = mynet_remove,
.suspend = mynet_suspend, // 电源管理
.resume = mynet_resume,
};
7. 调试与性能优化
7.1 常用调试技巧
- 查看设备注册信息:
bash复制cat /sys/kernel/debug/linux/netdev
- 检查中断计数:
bash复制cat /proc/interrupts | grep eth0
- DMA调试:
bash复制echo 1 > /sys/kernel/debug/dma-api/verbose
dmesg | grep DMA
- 丢包统计:
bash复制ethtool -S eth0
7.2 性能优化要点
- 调整NAPI权重:在驱动初始化时设置
c复制netif_napi_add(dev, &priv->napi, mynet_poll, 64);
- 优化DMA描述符环大小:
c复制#define TX_RING_SIZE 256
#define RX_RING_SIZE 512
- 启用GRO/GSO:
c复制dev->features |= NETIF_F_GRO | NETIF_F_SG | NETIF_F_TSO;
- 调整中断亲和性:
bash复制echo 2 > /proc/irq/123/smp_affinity
8. 实战案例:虚拟网卡驱动
我们以一个简单的虚拟网卡驱动为例,展示完整实现:
c复制#include <linux/module.h>
#include <linux/netdevice.h>
struct virtnet_priv {
struct net_device_stats stats;
};
static netdev_tx_t virtnet_xmit(struct sk_buff *skb, struct net_device *dev)
{
struct virtnet_priv *priv = netdev_priv(dev);
priv->stats.tx_packets++;
priv->stats.tx_bytes += skb->len;
dev_kfree_skb(skb);
return NETDEV_TX_OK;
}
static struct net_device_stats *virtnet_stats(struct net_device *dev)
{
return &((struct virtnet_priv *)netdev_priv(dev))->stats;
}
static const struct net_device_ops virtnet_ops = {
.ndo_start_xmit = virtnet_xmit,
.ndo_get_stats = virtnet_stats,
};
static void virtnet_setup(struct net_device *dev)
{
ether_setup(dev);
dev->netdev_ops = &virtnet_ops;
dev->flags |= IFF_NOARP;
}
static int __init virtnet_init(void)
{
struct net_device *dev;
int err;
dev = alloc_netdev(sizeof(struct virtnet_priv),
"vnet%d", NET_NAME_UNKNOWN,
virtnet_setup);
if (!dev) return -ENOMEM;
err = register_netdev(dev);
if (err) free_netdev(dev);
return err;
}
module_init(virtnet_init);
这个简单驱动展示了:
- 基本的net_device分配和注册
- 最小化的发送函数实现
- 统计信息维护
- 设备初始化流程
