虚拟网卡ndo_start_xmit函数中如何按序发送第二个struct sk_buff
解决虚拟网卡中skb_second在skb_one之后完整走传输流程的方案
嘿,这个场景我在调试虚拟网卡驱动时刚好碰到过,咱们先拆解下你当前两种方法的问题,再给出可行的解决方案:
问题分析
- 方法一:直接修改skb链表插入队列,确实能保证顺序,但跳过了网络栈中qdisc(队列调度器)的调度逻辑、设备状态检查、skb预处理(比如校验和计算、GSO分片)等关键环节,相当于绕过了虚拟网卡的完整传输流程,不符合内核网络栈的设计规范。
- 方法二:直接调用
dev_queue_xmit(skb_second)会让skb_second直接进入qdisc调度,但此时skb_one还在当前的ndo_start_xmit上下文里,内核会优先处理新提交的skb,导致顺序反转。
可行解决方案
方案一:利用qdisc队列插入尾部(推荐)
内核的qdisc本身就是负责管理发送顺序的组件,我们可以把skb_second插入到当前设备qdisc队列的尾部,让它跟随skb_one之后被调度,同时完整走传输流程:
static netdev_tx_t my_dev_start_xmit(struct sk_buff *skb_one, struct net_device *dev) { struct sk_buff *skb_second; struct Qdisc *q; struct sk_buff *to_free = NULL; int err; // 1. 先完成skb_one的正常处理(比如封装、校验和计算等) // ... 你的skb_one处理逻辑 ... // 2. 生成并初始化skb_second(绑定设备、设置协议等) skb_second = ...; // 你的skb_second生成逻辑 skb_set_dev(skb_second, dev); skb_second->protocol = skb_one->protocol; // 保持和skb_one一致的协议 // 其他必要的skb初始化... // 3. 获取当前设备的qdisc队列 q = rcu_dereference_bh(dev->qdisc); // 4. 将skb_second插入qdisc队列尾部 err = qdisc_enqueue(skb_second, q, &to_free); if (err == NET_XMIT_SUCCESS) { // 触发qdisc调度,确保skb_second被处理 qdisc_run(q); } else { // 处理插入失败的情况,释放skb kfree_skb(skb_second); } // 处理to_free中的skb(如果有的话) if (to_free) { dev_kfree_skb_any(to_free); } // 5. 正常发送skb_one return dev_hard_start_xmit(skb_one, dev, NULL); }
这种方式完全遵循内核网络栈的流程:skb_second会经过qdisc调度、再次进入ndo_start_xmit函数,而且因为是插入队列尾部,会严格在skb_one之后被发送。
方案二:使用tasklet延迟提交
如果觉得qdisc的操作有点复杂,也可以利用内核的tasklet(软中断任务)来延迟提交skb_second,确保它在skb_one发送完成后再走完整流程:
// 定义全局tasklet(或者绑定到设备私有数据) static void my_xmit_tasklet(unsigned long data) { struct sk_buff *skb = (struct sk_buff *)data; dev_queue_xmit(skb); } DECLARE_TASKLET(my_dev_tasklet, my_xmit_tasklet, 0); static netdev_tx_t my_dev_start_xmit(struct sk_buff *skb_one, struct net_device *dev) { struct sk_buff *skb_second; // 1. 处理并发送skb_one // ... 你的skb_one处理逻辑 ... netdev_tx_t ret = dev_hard_start_xmit(skb_one, dev, NULL); // 2. 生成并初始化skb_second skb_second = ...; // 你的skb_second生成逻辑 skb_set_dev(skb_second, dev); // 其他必要的skb初始化... // 3. 用tasklet延迟提交skb_second my_dev_tasklet.data = (unsigned long)skb_second; tasklet_schedule(&my_dev_tasklet); return ret; }
tasklet会在当前ndo_start_xmit流程结束后的软中断上下文执行,所以skb_second会在skb_one之后被提交,并且完整走dev_queue_xmit的所有流程。不过要注意:高负载下tasklet可能有微小延迟,但顺序是绝对保证的。
注意事项
- 无论哪种方案,都要确保
skb_second的引用计数正确,避免内存泄漏; - 如果你的虚拟网卡支持GSO、校验和卸载等特性,要给
skb_second设置对应的skb标志位,和skb_one保持一致; - 方案一中的
qdisc_enqueue可能会返回失败(比如队列满),一定要处理这种情况,及时释放skb_second。
内容的提问来源于stack exchange,提问作者Eknoes
相关产品推荐
相关产品推荐

