Linux网络之设备接口层:发送数据包流程dev_queue_xmit(二)

本文分析了在发送数据包过程中,设备接口层可能出现的问题及分析思路。主要涉及两类问题:一是某些数据包被不断重传;二是数据包送达netdevice层后未能到达driver。文章深入探讨了这些问题产生的原因及解决思路。


本文主要分析在发送数据包过程中,设备接口层容易出现的问题及分析思路。

从抓包工具中抓出来的网络日志的现象以及其他可疑的点来看可以分为2类:

1. 有些数据包被不断的重传(并不是TCP/UDP层的重传),同一个ipid的包不断的被抓到

2.数据包被送到netdevice层,但是并没有看到被送到driver


问题一:

   主要原因是driver 发送数据包出现问题,返回Error,导致数据包被放到Qdisc的queue中,但是还是会不断被尝试着重新发送!

下面我们从code 逻辑来分析:

正常的flow一般情况下是不会enqueue的,会直接发送,如果一旦driver发送失败了,就先将数据包enqueue,详细的发送过程可以参考文章:

最接近driver的函数__netdev_start_xmit如果发送失败会返回一个Error,如下面的值,例如Tx_Busy


enum netdev_tx {
	__NETDEV_TX_MIN	 = INT_MIN,	/* make sure enum is signed */
	NETDEV_TX_OK	 = 0x00,	/* driver took care of packet */
	NETDEV_TX_BUSY	 = 0x10,	/* driver tx path was busy*/
	NETDEV_TX_LOCKED = 0x20,	/* driver tx lock was already taken */
};

static inline netdev_tx_t __netdev_start_xmit(const struct net_device_ops *ops,
					      struct sk_buff *skb, struct net_device *dev,
					      bool more)
{
	skb->xmit_more = more ? 1 : 0;
	return ops->ndo_start_xmit(skb, dev);
}
返回一个错误的值,表示这个数据包发送出现问题,但是目前Qdisc会进行拥塞处理,将数据包在放到queue中,下面的代码段是在sch_direct_xmit函数中返回值处理的部分

如果发送成功,就会返回Q的len,如果NETDEV_TX_LOCKED会说明driver去lock的时候失败,其他状况下一般认为是默认的错误即TX BUSY,这个时候会调用dev_requeue_skb把数据包重新放到Qdisc的队列中!

	if (dev_xmit_complete(ret)) {
		/* Driver sent out skb successfully or skb was consumed */
		ret = qdisc_qlen(q);
	} else if (ret == NETDEV_TX_LOCKED) {
		/* Driver try lock failed */
		ret = handle_dev_cpu_collision(skb, txq, q);
	} else {
		/* Driver returned NETDEV_TX_BUSY - requeue skb */
		if (unlikely(ret != NETDEV_TX_BUSY))
			net_warn_ratelimited("BUG %s code %d qlen %d\n",
					     dev->name, ret, q->q.qlen);

		ret = dev_requeue_skb(skb, q);
	}

简单的来看下这个函数,把skb放到q->gso_skb中,然后增加q数量,然后调用__netif_schedule,这个是来使用软中断去发送数据包!
/* Main transmission queue. */

/* Modifications to data participating in scheduling must be protected with
 * qdisc_lock(qdisc) spinlock.
 *
 * The idea is the following:
 * - enqueue, dequeue are serialized via qdisc root lock
 * - ingress filtering is also serialized via qdisc root lock
 * - updates to tree and tree walking are only done under the rtnl mutex.
 */

static inline int dev_requeue_skb(struct sk_buff *skb, struct Qdisc *q)
{
	q->gso_skb = skb;
	q->qstats.requeues++;
	q->q.qlen++;	/* it's still part of the queue */
	__netif_schedule(q);

	return 0;
}
所以说基本上这个包被放入queue中,还会通过软中断,或者下一个数据包包来的时候去触发__qdisc_run, 这里应该就是谁快就使用谁吧。

中间发送的过程我们不再详细的解释,这个地方重点说一点就是dequeue skb的时候,发现TX stop的状况下都不会dequeue出skb

/* Note that dequeue_skb can possibly return a SKB list (via skb->next).
 * A requeued skb (via q->gso_skb) can also be a SKB list.
 */
static struct sk_buff *dequeue_skb(struct Qdisc *q, bool *validate,
				   int *packets)
{
       //这里先看Q中有没有requeue的skb,一般情况下存在发送失败的状况就会被requeue
	struct sk_buff *skb = q->gso_skb;
	const struct netdev_queue *txq = q->dev_queue;


	*packets = 1;
	*validate = true;
	if (unlikely(skb)) {
		/* check the reason of requeuing without tx lock first */
		txq = skb_get_tx_queue(txq->dev, skb);
		//如果gso_skb有,并且没有TXQ stop,那么就dequeu这个gso skb,发送失败的优先..
		if (!netif_xmit_frozen_or_stopped(txq)) {
			q->gso_skb = NULL;
			q->q.qlen--;
		} else
		//如果TXQ被stop了,那么无法dequeue出skb 就返回空
			skb = NULL;
		/* skb in gso_skb were already validated */
		*validate = false;
	} else {
	        //如果是多Q或者txq没有被stop 就dequeue skb
		if (!(q->flags & TCQ_F_ONETXQUEUE) ||
		    !netif_xmit_frozen_or_stopped(txq)) {
			skb = q->dequeue(q);
			if (skb && qdisc_may_bulk(q))
				try_bulk_dequeue_skb(q, skb, txq, packets);
		}
	}
	return skb;
}

从上面来看,我们的数据包仍然是能够被dequeue出来,之后还是会走到dev_hard_start_xmit 发送给driver, 如果driver还是无作为的话,那么我们就看到netdevice层同一个ipid的包被不断的重发...


问题二:

    其实问题二在上述的分析中已经提及到了,如果TCP协议中有数据包发送,送到netdevice了,但是driver并没有收到,在网络抓包工具中也没有显示出来任何发包的痕迹,不过一般情况下是 发着发着就不能发了。这个的原因主要是上层管理interface的process或者driver调用了netif_tx_stop_queue, 这个函数的作用就是把interface的txq停掉,不让发包

static inline void netif_tx_stop_queue(struct netdev_queue *dev_queue)
{
	if (WARN_ON(!dev_queue)) {
		pr_info("netif_stop_queue() cannot be called before register_netdev()\n");
		return;
	}
	set_bit(__QUEUE_STATE_DRV_XOFF, &dev_queue->state);
}

那么我们再来看下发包过程中会不会遇到阻碍,假设没有enqueue的状况下,一个数据包跑到了sch_direct_xmit里面,会判断txq是否为stop或者frozen状态,如果是 就不发了

直接requeue了.

		if (!netif_xmit_frozen_or_stopped(txq))
			skb = dev_hard_start_xmit(skb, dev, txq, &ret);
		...........省掉N行
				/* Driver returned NETDEV_TX_BUSY - requeue skb */
		if (unlikely(ret != NETDEV_TX_BUSY))
			net_warn_ratelimited("BUG %s code %d qlen %d\n",
					     dev->name, ret, q->q.qlen);

		ret = dev_requeue_skb(skb, q);

那么好 既然requeue了,我还可以dequeue再去发,想一下,及时能够dequeue出来,还是要调用到sch_direct_xmit啊,还是被当掉的继续requeue,这样其实不免白白的操作了链表,实际上kernel也没有那么傻了,在dequeue的时候就已经做了手脚,可以参考dequeue_skb的解释了... 如果txq 被stop直接就dequeue失败了...确实想的周到

		//如果gso_skb有,并且没有TXQ stop,那么就dequeu这个gso skb,发送失败的优先..
		if (!netif_xmit_frozen_or_stopped(txq)) {
			q->gso_skb = NULL;
			q->q.qlen--;
		} else
		//如果TXQ被stop了,那么无法dequeue出skb 就返回空
			skb = NULL;


所以我们会看到不能上网,但上层确实有数据包发出来,不过Q最多帮你顶到1000个包,如果超了就彻底被drop了。这时就会产生丢包现象,另外提一下就是在driver发送速度较慢,上层输送数据较快的状况下,这样会不断的有数据包被enqueue,当达到一定的瓶颈的时候,也就是1000的时候,后面灌输过来的数据包就会被丢掉了,这样就会产生掉包的现象,这个没有太好的办法,需要driver优化!


总结:

   本文主要分析了在netdevice Tx方向比较常见的问题,当然小的系统可能遇不到,在像手机,平板这样的大的复杂的系统中,这种问题在所难免,记住逻辑行为,结合抓包分析工具去分析!


<think>我们正在分析`__dev_queue_xmit`和`br_dev_xmit`之间的调用关系。根据之前的上下文,我们知道`__dev_queue_xmit`是网络数据包发送的核心函数,而 `br_dev_xmit`是桥接设备(网桥)的发送函数。关键点:1.当数据包需要通过网桥发送时,会调用网桥设备发送函数,即`br_dev_xmit`。2.在`br_dev_xmit`函数中,网桥会根据目标MAC地址和转发表决定数据包应该从哪个端口转发出去。3.对于每个需要转发的端口,网桥会克隆数据包(如果需要),然后调用 `dev_queue_xmit`(最终调用 `__dev_queue_xmit`)将数据包发送到该端口对应的网络设备。因此,调用关系是:`br_dev_xmit`在决定转发端口后,会调用`dev_queue_xmit`(进而调用 `__dev_queue_xmit`)来实际发送数据包。具体步骤:1.数据包进入网桥设备(作为桥设备)的发送函数`br_dev_xmit`。2.在`br_dev_xmit`中,网桥处理逻辑(如学习、转发、过滤等)后,对于需要转发的数据包,会遍历需要发送的端口。3.对于每个端口,网桥可能会修改数据包的源MAC地址(如果使用了网桥的MAC地址),然后通过`br_forward`函数将数据包发送到指定端口。4.在 `br_forward`中,最终会调用`__br_forward`,然后调用`deliver_clone`(如果需要克隆)或直接调用 `br_dev_queue_push_xmit`。5.在 `br_dev_queue_push_xmit`中,会设置数据包发送设备(即网桥端口对应的设备),然后调用`dev_queue_xmit`。因此,`br_dev_xmit`会间接调用 `dev_queue_xmit`(即 `__dev_queue_xmit`)来发送数据包。代码流程示例(基于Linux内核源码,如4.16+):1.`br_dev_xmit`函数(在`net/bridge/br_device.c`中):```cnetdev_tx_tbr_dev_xmit(struct sk_buff*skb,struct net_device *dev){//...网桥处理逻辑if(is_broadcast_ether_addr(dest))br_flood(br,skb, BR_PKT_BROADCAST, false);elseif (is_multicast_ether_addr(dest))br_flood(br,skb, BR_PKT_MULTICAST, false);elsebr_forward(br_get_port(dev,skb->dev),skb, false);// ...}```2. `br_forward`会调用 `__br_forward`,然后:```cvoid__br_forward(const structnet_bridge_port*to, structsk_buff *skb){// ...处理数据包,然后调用:br_dev_queue_push_xmit(to->br,to,skb);}```3.`br_dev_queue_push_xmit`函数:```cvoidbr_dev_queue_push_xmit(struct net_bridge*br, structnet_bridge_port*port,struct sk_buff*skb){//设置数据包发送设备为端口设备skb->dev= port->dev;//然后调用 dev_queue_xmitdev_queue_xmit(skb);}```4. `dev_queue_xmit`是一个封装函数,它会调用`__dev_queue_xmit`。因此,调用链为:`br_dev_xmit` →`br_forward` →`__br_forward`→ `br_dev_queue_push_xmit` →`dev_queue_xmit`→ `__dev_queue_xmit`注意:在网桥转发过程中,如果数据包需要发送到多个端口(如广播或组播),则会克隆数据包,每个端口发送一个副本。总结:`br_dev_xmit`作为网桥设备发送函数,在处理完网桥逻辑后,会通过调用 `dev_queue_xmit`(最终调用 `__dev_queue_xmit`)将数据包发送到各个端口设备。相关问题:1.网桥转发过程中如何避免循环?2.网桥在转发数据包时如何进行MAC地址学习?3.当网桥端口设备调用`__dev_queue_xmit`时,数据包会经过哪些排队规则(qdisc)?</think>### `__dev_queue_xmit` 与 `br_dev_xmit` 调用关系分析 在 Linux 网络栈中,`__dev_queue_xmit` 和 `br_dev_xmit` 是网桥转发路径上的关键协作函数,它们的调用关系如下: #### 1. 核心调用关系 ```mermaid graph LR A[br_dev_xmit] --> B[网桥转发决策] B -->|单播| C[br_forward] B -->|广播/组播| D[br_flood] C & D --> E[__br_forward] E --> F[br_dev_queue_push_xmit] F --> G[dev_queue_xmit] G --> H[__dev_queue_xmit] ``` #### 2. 详细调用流程 1. **网桥入口** 当数据包通过网桥设备发送时,内核调用网桥的发送函数: ```c // net/bridge/br_device.c netdev_tx_t br_dev_xmit(struct sk_buff *skb, struct net_device *dev) { // 1. 获取目标MAC和网桥实例 const unsigned char *dest = eth_hdr(skb)->h_dest; struct net_bridge *br = netdev_priv(dev); // 2. 转发决策 if (is_broadcast_ether_addr(dest)) br_flood(br, skb, BR_PKT_BROADCAST); else if (is_multicast_ether_addr(dest)) br_flood(br, skb, BR_PKT_MULTICAST); else br_forward(br_get_port(dev, skb->dev), skb, false); // 单播转发 } ``` 2. **转发执行** 在 `__br_forward()` 中准备最终发送: ```c // net/bridge/br_forward.c void __br_forward(const struct net_bridge_port *to, struct sk_buff *skb) { // 设置出口设备 skb->dev = to->dev; // 最终推送到发送队列 br_dev_queue_push_xmit(to->br, to, skb); } ``` 3. **调用 `__dev_queue_xmit`** 通过 `dev_queue_xmit()` 间接调用: ```c // net/bridge/br_device.c void br_dev_queue_push_xmit(...) { dev_queue_xmit(skb); // 实际调用 __dev_queue_xmit } ``` [^1] #### 3. 关键协作机制 1. **设备切换** - `br_dev_xmit` 接收时 `skb->dev` 是**网桥设备**(如 `br0`) - 在 `__br_forward` 中重定向 `skb->dev` 为**物理端口设备**(如 `eth0`) - 使 `__dev_queue_xmit` 能正确选择物理设备发送队列 2. **克隆处理** 广播/组播场景下,网桥会克隆数据包: ```c // net/bridge/br_forward.c void br_flood(...) { list_for_each_entry_rcu(p, &br->port_list, list) { struct sk_buff *skb2; if (should_deliver(p, skb)) { skb2 = skb_clone(skb, GFP_ATOMIC); // 克隆数据包 __br_forward(p, skb2); // 每个端口独立发送 } } } ``` 3. **转发控制** - 在调用 `__dev_queue_xmit` 前执行: - MAC 地址学习 (`br_fdb_update`) - STP 状态检查 (`br_forwardable`) - VLAN 标签处理 (`br_handle_vlan`) #### 4. 性能优化 1. **快速路径** 现代内核中通过 `br_handle_frame_finish` 实现快速转发路径,直接调用 `br_forward` 避免额外处理 2. **XDP 支持** 网桥支持 XDP 快速路径: ```c // net/bridge/br_device.c br_dev_xmit() { if (static_branch_unlikely(&br_xdp_needed_key)) { br_handle_xdp(...); // XDP 快速处理 } } ``` 3. **无锁转发** 使用 RCU 保护端口列表,避免转发时的锁竞争 --- ### 相关问题 1. **网桥如何处理目的地址为网桥自身 MAC 的数据包?** 网桥会将其提交给上层协议栈而不是转发[^2]。 2. **当 `__dev_queue_xmit` 返回错误时网桥如何响应?** 网桥会更新端口统计信息并释放数据包,不会重试[^1]。 3. **如何查看网桥转发的数据包统计信息?** 使用 `bridge -s link show` 命令查看端口转发计数。 4. **VLAN 网桥转发与普通网桥有何区别?** VLAN 网桥在 `__br_forward` 前会添加/剥离 VLAN 标签(通过 `br_handle_vlan`)。 5. **网桥转发过程中如何防止循环?** 通过生成树协议(STP)阻塞冗余端口,并在 `br_forwardable` 中检查端口状态[^2]。 [^1]: 参考内核源码 `net/core/dev.c` 和 `net/bridge/` 相关实现 [^2]: 基于网桥设备处理逻辑和原始套接字捕获机制
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值