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_openndo_stopndo_start_xmit 这些回调。你的驱动必须实现它们,否则网卡没法正常工作。

核心要点:struct net_device 是协议栈和驱动之间的「契约」。驱动通过它告诉协议栈「我能做什么」,协议栈通过它告诉驱动「你现在该做什么」。

我在项目中遇到过一个问题:某款网卡在注册时忘了设置 netdev_ops,结果内核直接 panic。排查了半天才发现是 ops 指针为 NULL。所以,注册前一定要检查 ops 是否完整。

2.2 NAPI 与中断收包机制

收包机制,说白了就是网卡怎么把数据告诉 CPU。早期用的是纯中断方式:来一个包,触发一次中断。但问题来了——如果包量很大,CPU 会被中断淹没,这叫「中断风暴」。

为什么会这样?你想想看,每次中断都要保存上下文、执行中断处理函数、再恢复上下文。如果每秒几十万个包,CPU 光忙活中断切换了,哪有时间处理真正的数据?

NAPI 就是来解决这个问题的。它的核心思想是:中断 + 轮询混合。具体流程是这样的:

  1. 第一个包来了,触发中断,NAPI 被调度。
  2. NAPI 关闭中断,进入轮询模式。
  3. 在轮询中,一次性收完所有待处理的包。
  4. 收完了,重新开启中断,等待下一个包。

你看,这样就把多次中断合并成一次,大大降低了 CPU 开销。我建议你在写驱动时,只要硬件支持,就优先用 NAPI。

小技巧:NAPI 的轮询权重(weight)要合理设置。默认一般是 64。设太大,会导致其他设备饿死;设太小,又起不到合并效果。我一般根据实际流量来调,比如千兆网卡设 64,万兆网卡设 128。

我曾经踩过一个坑:某款网卡的 NAPI 轮询函数里,忘了调用 napi_complete。结果中断一直关着,收完一批包后就再也不收新包了。排查时发现 CPU 占用率很低,但网络就是不通。嗯,从那以后我每次写 NAPI 都会再三确认 napi_complete 有没有被调用。

2.3 sk_buff 结构体深度解析

sk_buff 是网络子系统中最重要的数据结构,没有之一。每个网络包在内核里都用一个 sk_buff 表示。它包含了包的数据、协议头、控制信息等等。

我个人习惯,把 sk_buff 分成三个区域来看:

  • 控制区:包括 devskcb 等,用于管理包的状态。
  • 指针区:headdatatailend,这四个指针决定了包数据的位置。
  • 数据区:实际存放包数据的地方,从 headend

这里有个关键点:data 指针是可以移动的。协议栈在解析包时,会通过移动 data 来剥离协议头。比如,从网卡收上来时,data 指向 MAC 头;到了 IP 层,data 就移到 IP 头后面。这样做的好处是,不需要拷贝数据,效率很高。

警告:千万不要直接修改 sk_buffdata 指针!要用 skb_pullskb_pushskb_putskb_reserve 这些专用函数。我见过有人直接写 skb->data += 14,结果导致内存越界,系统崩溃。

下面是一个典型的 sk_buff 布局图,我画了个 SVG 帮你理解:

sk_buff 结构体内存布局 数据区 (head ~ end) head data tail end 预留空间 协议头 + 数据 尾部预留 data 指针随协议层解析而移动,避免数据拷贝

你看,headdata 之间是预留空间,用来添加新的协议头。tailend 之间也是预留空间,用来添加新的协议尾。这种设计非常巧妙,让协议栈在每一层都能高效地处理包。

我记得有一次调试一个性能问题,发现 sk_buff 的分配和释放占了 CPU 的 15%。后来用了 skb_recycle 和 slab 缓存优化,降到了 3%。所以,如果你在做高性能网络,一定要关注 sk_buff 的分配开销。

总结一下:

  • net_device 是设备的「户口本」,驱动必须正确初始化它。
  • NAPI 用中断+轮询的方式,解决了中断风暴问题。
  • sk_buff 是网络包的「容器」,理解它的指针移动机制是关键。

好,这一章就到这里。内容不少,但都是基础中的基础。你把这些搞透了,后面讲协议栈实现的时候就会轻松很多。


专注资料整理