1. 引言
在 Linux 系统中,以太网驱动是网络功能正常工作的基础。无论是开发板、嵌入式设备还是服务器网卡,都需要通过驱动与内核网络协议栈交互。本文将从驱动框架、核心数据结构、注册流程到实际代码示例,系统讲解如何在 Linux 中编写并添加一个以太网驱动。
2. 以太网驱动框架概述
Linux 内核为网络设备驱动提供了统一的抽象层,核心是struct net_device结构体。驱动开发者需要完成设备探测、资源初始化、打开/关闭接口、数据收发等操作,并把这些操作注册到内核网络子系统。
一个完整的以太网驱动通常包含以下组成部分:
- 设备探测与初始化:通过平台总线、PCI 或设备树匹配硬件。
- 网络设备注册:分配并初始化
net_device,设置操作函数集。 - 数据发送:实现
ndo_start_xmit回调,把 skb 交给硬件。 - 数据接收:通过中断或轮询方式接收数据,并上交给协议栈。
- 中断处理:处理收发完成、链路状态变化等事件。
3. 核心数据结构
编写以太网驱动前,需要先理解几个关键的内核数据结构。
3.1 struct net_device
这是网络设备的核心结构体,描述一个网络接口的全部属性,包括设备名、MAC 地址、MTU、操作函数集等。驱动通过alloc_etherdev或alloc_netdev分配。
3.2 struct net_device_ops
该结构体定义了设备的各种操作回调,例如打开设备、停止设备、发送数据、修改 MAC 地址等。驱动必须实现其中必要的回调并赋值给net_device的netdev_ops字段。
3.3 struct sk_buff
内核网络协议栈中用于承载数据包的核心结构体。发送时驱动从sk_buff中取出数据交给硬件;接收时驱动把硬件数据封装成sk_buff并交给协议栈。
4. 驱动注册流程
以太网驱动的注册流程通常分为以下几个步骤:
- 定义平台驱动或 PCI 驱动结构体,并注册到内核总线。
- 在探测函数中读取硬件资源(寄存器地址、中断号等)。
- 调用
alloc_etherdev分配网络设备结构体。 - 初始化
net_device的字段和操作函数集。 - 调用
register_netdev把设备注册到内核网络子系统。 - 在移除函数中调用
unregister_netdev和free_netdev释放资源。
5. 代码示例:一个简单的平台以太网驱动
下面以平台设备为例,演示一个最小可用的以太网驱动骨架。该示例使用虚拟寄存器模拟硬件,重点展示驱动框架的搭建过程。
#include <linux/module.h> #include <linux/platform_device.h> #include <linux/netdevice.h> #include <linux/etherdevice.h> #include <linux/skbuff.h> #include <linux/interrupt.h> #define DRV_NAME "demo_eth" struct demo_eth_priv { struct net_device *netdev; void __iomem *base; int irq; }; static int demo_eth_open(struct net_device *netdev) { netif_start_queue(netdev); return 0; } static int demo_eth_stop(struct net_device *netdev) { netif_stop_queue(netdev); return 0; } static netdev_tx_t demo_eth_start_xmit(struct sk_buff *skb, struct net_device *netdev) { /* 实际驱动在此处把 skb 数据写入硬件发送 FIFO */ dev_kfree_skb(skb); return NETDEV_TX_OK; } static const struct net_device_ops demo_eth_netdev_ops = { .ndo_open = demo_eth_open, .ndo_stop = demo_eth_stop, .ndo_start_xmit = demo_eth_start_xmit, }; static int demo_eth_probe(struct platform_device *pdev) { struct demo_eth_priv *priv; struct net_device *netdev; struct resource *res; int ret; netdev = alloc_etherdev(sizeof(struct demo_eth_priv)); if (!netdev) return -ENOMEM; priv = netdev_priv(netdev); priv->netdev = netdev; res = platform_get_resource(pdev, IORESOURCE_MEM, 0); priv->base = devm_ioremap_resource(&pdev->dev, res); if (IS_ERR(priv->base)) { ret = PTR_ERR(priv->base); goto err_free; } priv->irq = platform_get_irq(pdev, 0); if (priv->irq < 0) { ret = priv->irq; goto err_free; } netdev->netdev_ops = &demo_eth_netdev_ops; eth_hw_addr_set(netdev, (u8[]){0x00, 0x11, 0x22, 0x33, 0x44, 0x55}); ret = register_netdev(netdev); if (ret) goto err_free; platform_set_drvdata(pdev, priv); return 0; err_free: free_netdev(netdev); return ret; } static void demo_eth_remove(struct platform_device *pdev) { struct demo_eth_priv *priv = platform_get_drvdata(pdev); unregister_netdev(priv->netdev); free_netdev(priv->netdev); } static struct platform_driver demo_eth_driver = { .probe = demo_eth_probe, .remove = demo_eth_remove, .driver = { .name = DRV_NAME, .of_match_table = demo_eth_of_match, }, }; module_platform_driver(demo_eth_driver); MODULE_LICENSE("GPL"); MODULE_DESCRIPTION("Demo Ethernet Driver");6. 设备树匹配
在设备树中,需要为硬件节点指定 compatible 属性,与驱动中的of_match_table对应。示例设备树节点如下:
demo_eth: ethernet@10000000 { compatible = "vendor,demo-eth"; reg = <0x10000000 0x1000>; interrupts = <31>; };驱动中需要补充对应的匹配表:
static const struct of_device_id demo_eth_of_match[] = { { .compatible = "vendor,demo-eth" }, { } }; MODULE_DEVICE_TABLE(of, demo_eth_of_match);7. 编译与加载
将驱动源码放入内核源码树的drivers/net/ethernet/目录,并修改对应的 Kconfig 和 Makefile,然后编译内核或编译为模块。
如果编译为模块,加载命令如下:
insmod demo_eth.ko加载成功后,可以通过以下命令查看设备是否注册成功:
ip link show如果看到类似eth0的接口,说明驱动注册成功。
8. 总结
添加以太网驱动的核心在于理解net_device框架、实现必要的操作回调,并正确完成设备注册流程。实际项目中还需要处理 DMA、中断、链路状态检测、NAPI 等复杂逻辑,但整体框架与本文示例一致。建议读者在真实硬件上结合内核文档逐步调试,加深对网络驱动子系统的理解。