2、网络设备驱动基础:struct net_device结构体详解、NAPI与中断收包机制、sk_buff结构体深度解析
大家好,我是你们的讲师。今天我们来聊聊网络设备驱动里最核心的三个东西:struct net_device、NAPI 收包机制,还有 sk_buff。这三个东西,说白了就是网络子系统的「三驾马车」。你搞懂了它们,内核网络这块就算入门了。
我个人习惯,每次接手一个新项目,第一件事就是先看 net_device 的注册流程。为什么?因为它决定了你的网卡怎么跟协议栈「握手」。好,我们一个一个来。
2.1 struct net_device:网络设备的「身份证」
每个网络设备在内核里都有一个 struct net_device 实例。它记录了设备的名字、MAC 地址、MTU、操作函数集等等。你可以把它想象成设备的「户口本」。
我建议你重点关注以下几个字段:
| 字段名 | 作用 | 备注 |
|---|---|---|
char name[IFNAMSIZ] |
设备名,比如 eth0 | 注册时自动分配或手动指定 |
unsigned int mtu |
最大传输单元 | 默认 1500,改它要小心 |
unsigned char dev_addr[MAX_ADDR_LEN] |
MAC 地址 | 驱动初始化时填充 |
const struct net_device_ops *netdev_ops |
操作函数指针 | 这是最关键的一个 |
struct list_head dev_list |
全局设备链表节点 | 内核用它遍历所有设备 |
嗯,这里要注意 netdev_ops。它里面定义了 ndo_open、ndo_stop、ndo_start_xmit 这些回调。你的驱动必须实现它们,否则网卡没法正常工作。
核心要点:struct net_device 是协议栈和驱动之间的「契约」。驱动通过它告诉协议栈「我能做什么」,协议栈通过它告诉驱动「你现在该做什么」。
我在项目中遇到过一个问题:某款网卡在注册时忘了设置 netdev_ops,结果内核直接 panic。排查了半天才发现是 ops 指针为 NULL。所以,注册前一定要检查 ops 是否完整。
2.2 NAPI 与中断收包机制
收包机制,说白了就是网卡怎么把数据告诉 CPU。早期用的是纯中断方式:来一个包,触发一次中断。但问题来了——如果包量很大,CPU 会被中断淹没,这叫「中断风暴」。
为什么会这样?你想想看,每次中断都要保存上下文、执行中断处理函数、再恢复上下文。如果每秒几十万个包,CPU 光忙活中断切换了,哪有时间处理真正的数据?
NAPI 就是来解决这个问题的。它的核心思想是:中断 + 轮询混合。具体流程是这样的:
- 第一个包来了,触发中断,NAPI 被调度。
- NAPI 关闭中断,进入轮询模式。
- 在轮询中,一次性收完所有待处理的包。
- 收完了,重新开启中断,等待下一个包。
你看,这样就把多次中断合并成一次,大大降低了 CPU 开销。我建议你在写驱动时,只要硬件支持,就优先用 NAPI。
小技巧:NAPI 的轮询权重(weight)要合理设置。默认一般是 64。设太大,会导致其他设备饿死;设太小,又起不到合并效果。我一般根据实际流量来调,比如千兆网卡设 64,万兆网卡设 128。
我曾经踩过一个坑:某款网卡的 NAPI 轮询函数里,忘了调用 napi_complete。结果中断一直关着,收完一批包后就再也不收新包了。排查时发现 CPU 占用率很低,但网络就是不通。嗯,从那以后我每次写 NAPI 都会再三确认 napi_complete 有没有被调用。
2.3 sk_buff 结构体深度解析
sk_buff 是网络子系统中最重要的数据结构,没有之一。每个网络包在内核里都用一个 sk_buff 表示。它包含了包的数据、协议头、控制信息等等。
我个人习惯,把 sk_buff 分成三个区域来看:
- 控制区:包括
dev、sk、cb等,用于管理包的状态。 - 指针区:
head、data、tail、end,这四个指针决定了包数据的位置。 - 数据区:实际存放包数据的地方,从
head到end。
这里有个关键点:data 指针是可以移动的。协议栈在解析包时,会通过移动 data 来剥离协议头。比如,从网卡收上来时,data 指向 MAC 头;到了 IP 层,data 就移到 IP 头后面。这样做的好处是,不需要拷贝数据,效率很高。
警告:千万不要直接修改 sk_buff 的 data 指针!要用 skb_pull、skb_push、skb_put、skb_reserve 这些专用函数。我见过有人直接写 skb->data += 14,结果导致内存越界,系统崩溃。
下面是一个典型的 sk_buff 布局图,我画了个 SVG 帮你理解:
你看,head 和 data 之间是预留空间,用来添加新的协议头。tail 和 end 之间也是预留空间,用来添加新的协议尾。这种设计非常巧妙,让协议栈在每一层都能高效地处理包。
我记得有一次调试一个性能问题,发现 sk_buff 的分配和释放占了 CPU 的 15%。后来用了 skb_recycle 和 slab 缓存优化,降到了 3%。所以,如果你在做高性能网络,一定要关注 sk_buff 的分配开销。
总结一下:
net_device是设备的「户口本」,驱动必须正确初始化它。- NAPI 用中断+轮询的方式,解决了中断风暴问题。
sk_buff是网络包的「容器」,理解它的指针移动机制是关键。
好,这一章就到这里。内容不少,但都是基础中的基础。你把这些搞透了,后面讲协议栈实现的时候就会轻松很多。