3. Linux网络设备驱动框架:net_device结构体、网络设备驱动注册与注销、NAPI与中断机制
好,咱们今天聊聊网卡驱动的核心骨架。说实话,Linux网络设备驱动这块,刚入门时容易觉得乱——结构体多、回调函数多、中断处理还分好几套。但你别怕,抓住三个关键点就行:net_device 是网卡的“身份证”,注册与注销是驱动的“生老病死”,NAPI与中断是收发包的“交通规则”。
我个人习惯把这三块串起来理解:先有设备对象,再把它挂到内核里,最后用高效的方式处理数据。咱们一个一个拆开看。
3.1 net_device结构体:网卡的“户口本”
在Linux内核里,每个网络设备都对应一个 struct net_device 结构体。你可以把它想象成网卡的“户口本”——里面记着设备的名字、MAC地址、MTU、操作函数集等等。我当年第一次看这个结构体时,差点被它几百行的成员吓到。其实常用的就那几十个。
核心成员我列一下:
| 成员 | 类型 | 说明 |
|---|---|---|
| name | char[] | 设备名,比如 eth0、wlan0 |
| dev_addr | unsigned char* | MAC地址指针 |
| mtu | unsigned int | 最大传输单元,默认1500 |
| flags | unsigned short | 设备标志,比如 IFF_UP、IFF_RUNNING |
| netdev_ops | const struct net_device_ops* | 操作函数集,最重要的成员之一 |
| irq | int | 中断号 |
| features | netdev_features_t | 硬件特性标志,比如支持校验和卸载 |
这里我想特别提一下 netdev_ops。它定义了网卡怎么“干活”——打开、关闭、启动传输、设置MAC地址、修改MTU等等。你在驱动里主要就是填充这个结构体。举个例子:
static const struct net_device_ops my_netdev_ops = {
.ndo_open = my_netdev_open,
.ndo_stop = my_netdev_stop,
.ndo_start_xmit = my_netdev_start_xmit,
.ndo_set_mac_address = my_netdev_set_mac,
.ndo_change_mtu = my_netdev_change_mtu,
};
嗯,这里要注意:ndo_start_xmit 是发送数据的入口。内核协议栈把要发的skb交给你,你负责把它扔到硬件里。我在项目中遇到过一个问题——发送函数里忘了释放skb,结果内存泄漏,跑几天系统就挂了。所以记得:发完数据要 dev_kfree_skb()。
3.2 网络设备驱动注册与注销
驱动怎么告诉内核“嘿,我有个网卡要管”?靠两个函数:register_netdev() 和 unregister_netdev()。
注册流程大概是这样的:
- 用
alloc_etherdev()或alloc_netdev()分配一个 net_device 结构体 - 填充必要的成员:name、netdev_ops、ethtool_ops 等
- 设置硬件相关资源:IO地址、中断号、DMA缓冲区
- 调用
register_netdev()注册到内核
我贴一段典型的初始化代码:
static int my_netdev_probe(struct platform_device *pdev)
{
struct net_device *ndev;
struct my_priv *priv;
int ret;
// 1. 分配net_device,同时分配私有数据区
ndev = alloc_etherdev(sizeof(struct my_priv));
if (!ndev)
return -ENOMEM;
priv = netdev_priv(ndev);
priv->ndev = ndev;
// 2. 设置操作函数
ndev->netdev_ops = &my_netdev_ops;
ndev->ethtool_ops = &my_ethtool_ops;
// 3. 设置MAC地址(从设备树或EEPROM读取)
// ...
// 4. 注册
ret = register_netdev(ndev);
if (ret) {
free_netdev(ndev);
return ret;
}
platform_set_drvdata(pdev, ndev);
return 0;
}
注销时反过来:先 unregister_netdev(),再 free_netdev()。我曾经犯过一个错——先释放了私有数据再注销,结果注销函数里还访问了私有数据,直接Oops。顺序很重要,切记。
3.3 NAPI与中断机制
收包是网卡驱动的核心。早期驱动用纯中断方式:来一个包,触发一次中断,CPU 进中断处理函数收包。问题很明显——高负载下中断风暴,CPU 被活活打死。
NAPI(New API)就是来解决这个问题的。它的核心思想是:中断 + 轮询混合。中断来了先关中断,然后切到轮询模式,一口气收完所有包,再开中断。这样高负载时减少中断次数,低负载时响应也快。
NAPI 的关键数据结构是 struct napi_struct,里面有个 poll 回调函数。你想想看,驱动要做的事就是:
- 初始化时调用
netif_napi_add()注册 NAPI - 中断处理函数里调用
napi_schedule()触发轮询 - 在 poll 函数里收包,返回处理了多少个包
- 收完后调用
napi_complete()并重新开中断
我画个流程图帮你理解:
代码层面,poll 函数长这样:
static int my_netdev_poll(struct napi_struct *napi, int budget)
{
struct my_priv *priv = container_of(napi, struct my_priv, napi);
struct net_device *ndev = priv->ndev;
int work_done = 0;
// 循环从硬件收包
while (work_done < budget) {
struct sk_buff *skb = my_hw_receive_packet(priv);
if (!skb)
break;
skb->protocol = eth_type_trans(skb, ndev);
netif_receive_skb(skb);
work_done++;
}
// 如果收满了,继续轮询;否则退出轮询模式
if (work_done < budget) {
napi_complete_done(napi, work_done);
// 重新开中断
my_hw_enable_interrupt(priv);
}
return work_done;
}
中断处理函数里,你只需要做最轻量的事:
static irqreturn_t my_netdev_interrupt(int irq, void *dev_id)
{
struct net_device *ndev = dev_id;
struct my_priv *priv = netdev_priv(ndev);
// 关中断
my_hw_disable_interrupt(priv);
// 触发NAPI轮询
napi_schedule(&priv->napi);
return IRQ_HANDLED;
}
你看,中断处理函数里几乎不做实际收包工作,只是关中断 + 调度NAPI。实际收包在软中断上下文里完成。这样设计的好处是:中断处理时间极短,系统响应快。
核心要点:
- net_device 是网卡驱动的“身份证”,必须正确填充 netdev_ops
- 注册用 register_netdev(),注销用 unregister_netdev(),顺序不能乱
- NAPI 用中断唤醒、轮询收包,高负载下性能远优于纯中断
- 中断处理函数要轻量,只做关中断和调度NAPI
嗯,说到NAPI,我记得有一次调试一个千兆网卡驱动,发现收包性能上不去。查了半天,原来是 poll 函数里忘了调用 napi_complete(),导致中断一直关着,后续包只能靠轮询收。低负载时延迟飙升。所以啊,细节决定成败。
好了,这一章的内容就到这。你把这些搞懂了,网卡驱动的基本框架就搭起来了。下一块咱们聊聊更具体的——sk_buff 和 DMA 传输,那才是真正搬砖的地方。