3. 网络驱动初始化流程
网络设备驱动的初始化是驱动生命周期中最关键的阶段,它决定了设备能否被内核正确识别、资源是否充足、以及数据通路是否畅通。本章将按照驱动加载的完整时序,逐一剖析模块加载/卸载、设备探测、资源分配和 net_device 注册四个核心环节。
3.1 驱动模块加载与卸载
Linux 网络驱动通常以内核模块的形式存在,通过 module_init 和 module_exit 宏声明入口与出口函数。加载时,内核执行初始化入口;卸载时,执行清理并释放资源。
| 宏 / 函数 | 作用 | 典型实现 |
|---|---|---|
module_init(x) |
声明模块加载入口函数 | static int __init mydrv_init(void) |
module_exit(x) |
声明模块卸载出口函数 | static void __exit mydrv_exit(void) |
MODULE_LICENSE("GPL") |
声明模块许可证 | 必须声明,否则内核拒绝加载 |
MODULE_AUTHOR("") |
声明模块作者 | 可选,用于调试信息 |
关键点: 加载函数必须使用
__init 标记,卸载函数使用 __exit 标记。这些标记告诉内核在初始化完成后释放代码段内存,节省运行态空间。
// 模块加载/卸载骨架
static int __init mydrv_init(void)
{
printk(KERN_INFO "MyNetDrv: module loaded\n");
// 1. 注册平台驱动或 PCI 驱动
// 2. 探测设备
// 3. 分配资源
// 4. 注册 net_device
return 0;
}
static void __exit mydrv_exit(void)
{
printk(KERN_INFO "MyNetDrv: module unloaded\n");
// 1. 注销 net_device
// 2. 释放资源
// 3. 注销驱动
}
module_init(mydrv_init);
module_exit(mydrv_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kernel Expert");
3.2 设备探测:从总线到驱动匹配
设备探测是驱动与硬件建立联系的起点。对于 PCI 设备,驱动通过 pci_driver 结构体注册 id_table 和 probe 回调;对于平台设备,则使用 platform_driver。当总线发现设备 ID 匹配时,内核自动调用驱动的 probe 函数。
最佳实践: 在
probe 函数中尽早检查硬件是否存在、寄存器是否可访问,避免后续资源分配失败导致内核崩溃。
// PCI 驱动探测示例
static struct pci_device_id mydrv_pci_tbl[] = {
{ PCI_DEVICE(0x10EC, 0x8168) }, // Realtek 8168
{ 0, }
};
MODULE_DEVICE_TABLE(pci, mydrv_pci_tbl);
static int mydrv_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
struct net_device *ndev;
int ret;
// 1. 启用 PCI 设备
ret = pci_enable_device(pdev);
if (ret)
return ret;
// 2. 请求 MMIO / IO 资源
ret = pci_request_regions(pdev, DRV_NAME);
if (ret)
goto err_disable;
// 3. 分配 net_device
ndev = alloc_etherdev(sizeof(struct mydrv_priv));
if (!ndev) {
ret = -ENOMEM;
goto err_release;
}
// 4. 设置私有数据、回调函数
// ...
// 5. 注册 net_device
ret = register_netdev(ndev);
if (ret)
goto err_free;
pci_set_drvdata(pdev, ndev);
return 0;
err_free:
free_netdev(ndev);
err_release:
pci_release_regions(pdev);
err_disable:
pci_disable_device(pdev);
return ret;
}
3.3 资源分配:内存、I/O 与中断
设备探测成功后,驱动必须为数据收发准备必要的系统资源。资源分配不当是驱动崩溃和性能瓶颈的主要来源。
| 资源类型 | 分配函数 | 释放函数 | 说明 |
|---|---|---|---|
| MMIO / I/O 端口 | pci_request_regions() |
pci_release_regions() |
防止多个驱动操作同一硬件 |
| DMA 缓冲区 | dma_alloc_coherent() |
dma_free_coherent() |
用于描述符环和 SKB 数据 |
| 中断 | request_irq() |
free_irq() |
注册中断处理函数 |
| NAPI 结构 | netif_napi_add() |
netif_napi_del() |
初始化 NAPI 轮询上下文 |
常见陷阱: 忘记在卸载路径中按分配顺序的逆序释放资源,会导致内存泄漏、DMA 缓冲区残留、中断处理函数悬挂,严重时引发内核 panic。
// 资源分配典型代码段
static int mydrv_alloc_resources(struct mydrv_priv *priv)
{
// 1. 分配 DMA 描述符环(Tx / Rx)
priv->tx_ring = dma_alloc_coherent(&priv->pdev->dev,
TX_RING_SIZE * sizeof(struct desc),
&priv->tx_ring_dma, GFP_KERNEL);
if (!priv->tx_ring)
return -ENOMEM;
priv->rx_ring = dma_alloc_coherent(&priv->pdev->dev,
RX_RING_SIZE * sizeof(struct desc),
&priv->rx_ring_dma, GFP_KERNEL);
if (!priv->rx_ring)
goto err_free_tx;
// 2. 注册中断(支持 MSI-X 优先)
priv->irq = pci_irq_vector(priv->pdev, 0);
if (request_irq(priv->irq, mydrv_isr, 0, DRV_NAME, priv))
goto err_free_rx;
// 3. 添加 NAPI
netif_napi_add(priv->ndev, &priv->napi, mydrv_poll, 64);
return 0;
err_free_rx:
dma_free_coherent(&priv->pdev->dev, RX_RING_SIZE * sizeof(struct desc),
priv->rx_ring, priv->rx_ring_dma);
err_free_tx:
dma_free_coherent(&priv->pdev->dev, TX_RING_SIZE * sizeof(struct desc),
priv->tx_ring, priv->tx_ring_dma);
return -ENOMEM;
}
3.4 net_device 注册:驱动与内核网络栈的桥梁
net_device 是内核网络子系统对每个网络接口的抽象。驱动必须正确填充该结构体,并通过 register_netdev() 将其纳入内核管理。注册成功后,用户空间可通过 ifconfig 或 ip 命令看到该接口。
// net_device 注册完整示例
static int mydrv_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
struct net_device *ndev;
struct mydrv_priv *priv;
int ret;
// 1. 分配 net_device + 私有数据
ndev = alloc_etherdev(sizeof(struct mydrv_priv));
if (!ndev)
return -ENOMEM;
priv = netdev_priv(ndev);
priv->ndev = ndev;
priv->pdev = pdev;
// 2. 填充操作函数集
ndev->netdev_ops = &mydrv_netdev_ops;
ndev->ethtool_ops = &mydrv_ethtool_ops;
// 3. 设置硬件特性(支持 TSO、SG、RXCSUM)
ndev->hw_features = NETIF_F_SG | NETIF_F_TSO | NETIF_F_RXCSUM;
ndev->features |= ndev->hw_features;
// 4. 设置 MAC 地址(从 EEPROM 读取)
// eth_hw_addr_set(ndev, mac_addr);
// 5. 注册到内核
ret = register_netdev(ndev);
if (ret) {
free_netdev(ndev);
return ret;
}
pci_set_drvdata(pdev, ndev);
return 0;
}
3.5 卸载流程:逆序清理
模块卸载时,必须严格按照初始化的逆序释放所有资源。任何遗漏都会导致内核资源泄漏,严重时影响系统稳定性。
- 注销 net_device:调用
unregister_netdev(ndev),将接口从内核网络栈中移除。 - 删除 NAPI:调用
netif_napi_del(&priv->napi),清理 NAPI 上下文。 - 释放中断:调用
free_irq(priv->irq, priv),注销中断处理函数。 - 释放 DMA 缓冲区:调用
dma_free_coherent()释放描述符环和 SKB 数据区。 - 释放 PCI 资源:调用
pci_release_regions(pdev)和pci_disable_device(pdev)。 - 释放 net_device:调用
free_netdev(ndev),回收内存。
调试建议: 在卸载函数中增加
printk 日志,标记每个清理步骤的完成状态。如果卸载时系统卡死或报错,日志可快速定位未释放的资源。
// 模块卸载函数
static void __exit mydrv_exit(void)
{
struct net_device *ndev = pci_get_drvdata(pdev);
struct mydrv_priv *priv = netdev_priv(ndev);
// 1. 注销 net_device
unregister_netdev(ndev);
// 2. 删除 NAPI
netif_napi_del(&priv->napi);
// 3. 释放中断
free_irq(priv->irq, priv);
// 4. 释放 DMA 缓冲区
dma_free_coherent(&priv->pdev->dev, TX_RING_SIZE * sizeof(struct desc),
priv->tx_ring, priv->tx_ring_dma);
dma_free_coherent(&priv->pdev->dev, RX_RING_SIZE * sizeof(struct desc),
priv->rx_ring, priv->rx_ring_dma);
// 5. 释放 PCI 资源
pci_release_regions(priv->pdev);
pci_disable_device(priv->pdev);
// 6. 释放 net_device
free_netdev(ndev);
}
3.6 初始化流程全景图
将上述四个环节串联起来,形成完整的驱动初始化生命周期。理解这张图,就掌握了网络驱动加载的核心脉络。
3.7 常见错误与调试技巧
| 错误场景 | 现象 | 解决方案 |
|---|---|---|
probe 中未检查 pci_enable_device 返回值 |
设备无法访问,寄存器读写异常 | 始终检查返回值,失败时提前返回 |
| DMA 缓冲区大小未对齐 | 硬件 DMA 错误,数据损坏 | 使用 dma_set_coherent_mask 确保对齐 |
| 中断注册在资源分配之前 | 中断触发时访问未初始化的数据结构 | 严格遵循:资源分配 → 中断注册 |
卸载时遗漏 netif_napi_del |
NAPI 残留,下次加载时冲突 | 在 unregister_netdev 后立即删除 NAPI |
调试利器: 使用
dev_info() / dev_err() 替代 printk,它们会自动附加设备名称,让日志更易读。配合 dmesg -w 实时观察驱动加载过程。