5.2 网络设备驱动:net_device 与收发包


5.2 网络设备驱动:net_device 与收发包

本节摘要:网络设备驱动不接文件接口,而是把自己挂进协议栈的收发流水线。本节讲清 net_device 的注册手续、发包函数的契约、收包从硬中断到协议栈的上行路径,以及套接字缓冲区的生命周期纪律,最后把字符、块、网络三类驱动收拢成一张全景对照。

网卡驱动是四类驱动里"存在感"最特殊的一个:用户态找不到代表它的设备文件,却在每一次网络通信里都踩着它的肩膀。它不实现 file_operations,而是向网络栈注册两个方向的接口——协议栈要发包时调用它的发包函数,它收到包时把包塞回协议栈。驱动与框架的关系从"应答"变成了"协作"

注册:把设备挂进协议栈

网络设备的核心对象是 net_device,注册流程比字符设备多一层"分配+初始化":

#include <linux/netdevice.h> #include <linux/etherdevice.h> static const struct net_device_ops net_ops = { .ndo_open = net_if_open, /* 接口启用:ifconfig up 时调用 */ .ndo_stop = net_if_stop, /* 接口停用 */ .ndo_start_xmit = net_if_xmit, /* 发包入口:协议栈的发货口 */ }; static int __init net_drv_init(void) { struct net_device *ndev; int ret; /* 分配以太网设备对象:自动带 14 字节链路头空间等默认设置 */ ndev = alloc_etherdev(sizeof(struct priv_data)); if (!ndev) return -ENOMEM; ndev->netdev_ops = &net_ops; eth_hw_addr_set(ndev, factory_mac_addr); /* 烧录地址或从硬件读出 */ ret = register_netdev(ndev); /* 挂进协议栈 */ if (ret) { free_netdev(ndev); return ret; } return 0; }

alloc_etherdev 分配的对象自带以太网默认配置,私有数据结构可挂在对象尾部随行——这是内核驱动保存实例状态的惯用法。注册完成后,接口在网络配置清单里出现,启用命令会触发 ndo_open(开 DMA 环、注册中断),停用命令触发 ndo_stop(对称收尾)。

$ ip link 2: eth0: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN link/ether 5a:21:3c:8e:11:04 brd ff:ff:ff:ff:ff:ff $ ip link set eth0 up # 触发 ndo_open

发包:协议栈的发货口

协议栈把封装好的包连同套接字缓冲区一起递给 ndo_start_xmit。驱动要做的三步:取包、投递硬件、管理缓冲:

static netdev_tx_t net_if_xmit(struct sk_buff *skb, struct net_device *ndev) { struct priv_data *priv = netdev_priv(ndev); /* 发送环满了:先报停,等完成中断里的空位再恢复 */ if (tx_ring_full(priv)) { netif_stop_queue(ndev); /* 告诉协议栈暂停投递 */ return NETDEV_TX_BUSY; } /* 映射给 DMA(第 3.3 节的流式映射)并写入发送描述符 */ tx_map_and_submit(priv, skb); /* 硬件收走数据前缓冲不能回收——记录引用,完成中断里释放 */ priv->tx_skb[priv->tx_tail] = skb; tx_advance(priv); return NETDEV_TX_OK; /* 注意:返回 OK 不代表已发出 */ }

三个契约要点。其一,返回值语义:返回成功只表示"已接收此包",实际发送异步完成,协议栈不指望同步确认。其二,流控靠停队:发送环满时用停队接口告诉协议栈"别再投了",完成中断腾出空位后再开队——不做流控,环溢出包就悄悄丢。其三,缓冲所有权:包数据被硬件取走前,套接字缓冲区必须存活;释放时机在完成中断里,过早释放等于让硬件读一块已被改写的内存。

收包:从硬中断到协议栈

接收路径是网络驱动的重头戏,环环相扣:

1. 硬中断到来:网卡有包到达(现代驱动多用"收包合并"少中断多收包) 2. 中断处理:应答硬件,禁止收包中断,调度收包软中断(3.2 节的底半部) 3. 收包软中断:循环收取——从接收环取描述符,补挂新缓冲,逐包上交 4. 上交协议栈:填好协议类型与长度,调用接收接口 5. 协议栈接管:校验、分层、匹配套接字,唤醒用户态

收包软中断里的核心片段:

static int rx_poll(struct napi_struct *napi, int budget) { struct priv_data *priv = container_of(napi, struct priv_data, napi); int received = 0; while (received < budget) { struct sk_buff *skb = rx_fetch(priv); if (!skb) break; /* 环空:收完了 */ skb->protocol = eth_type_trans(skb, priv->ndev); netif_receive_skb(skb); /* 上交协议栈 */ rx_refill(priv); /* 补挂新缓冲防止断粮 */ received++; } if (received < budget) { napi_complete_done(napi, received); rx_irq_enable(priv); /* 预算没用完:重新开中断 */ } return received; /* 用满预算则下轮继续 */ }

这套"中断关掉、按预算轮询、空了再开中断"的机制叫轮询式收包接口,是高速网卡的标准姿势:流量大时中断风暴被预算压制,流量小时回归中断唤醒——中断的低延迟与轮询的低开销兼得。预算机制还天然做了公平调度:每个设备每轮最多吃固定配额,谁也不能霸占处理器。

收发包双路径全景

收发包双路径全景

四类驱动全景对照

至此四类驱动框架全部到场,把 1.3 节的分类口诀兑现成框架级对照:

维度 字符设备 块设备 网络设备
用户态入口 设备文件读写控制 经文件系统间接访问 套接字与协议栈
驱动核心回调 file_operations 表 队列处理函数 发包函数加收包上行
缓冲归属 驱动自管 块层管请求驱动管段 套接字缓冲区全程流转
吞吐手段 无内建机制 请求队列合并调度 描述符环加轮询预算
流控手段 无内建机制 队列深度与背压 停队机制与接收预算
典型复杂点 接口语义正确 队列与完成时序 环管理与缓冲纪律

平台设备(第 4 章)不在表里——它是"发现机制"而非数据框架,字符、块、网络设备都可以坐在平台总线的设备侧。先选数据框架,再定发现机制,这个两步决策覆盖几乎全部驱动立项场景。

本节要点回顾

  • 协作而非应答:网络驱动向协议栈注册双向接口,没有文件操作表。
  • 发包三契约:接单语义、缓冲延迟释放、停队流控。
  • 收包走轮询预算:中断与轮询混合调度,公平与低延迟兼得。
  • 缓冲纪律贯穿全程:谁分配谁释放,上交即移交所有权。

吞吐的框架讲完了,但高压流量立刻暴露新问题:多核并发访问、缓冲竞态、时序违例。第 6 章正面处理驱动的"稳健性五件事"——同步、时序、电源、安全、性能。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U