H3 网络 TX 路径中 qdisc requeues 的来源分析:从 stmmac 到 BQL/DQL

文章目录

  • [1. 背景](#1. 背景)
  • [2. 环境](#2. 环境)
  • [3. 初始现象](#3. 初始现象)
  • [4. TX 路径关键代码](#4. TX 路径关键代码)
  • [5. 验证](#5. 验证)
    • [5.1 第一轮验证:驱动是否返回 NETDEV_TX_BUSY](#5.1 第一轮验证:驱动是否返回 NETDEV_TX_BUSY)
    • [5.2 第二轮验证:是不是 stmmac 主动 stop queue](#5.2 第二轮验证:是不是 stmmac 主动 stop queue)
    • [5.3 第三轮验证:是不是进入 sch_direct_xmit 时 txq 已经 stopped](#5.3 第三轮验证:是不是进入 sch_direct_xmit 时 txq 已经 stopped)
    • [5.4 第四轮验证:BQL/DQL](#5.4 第四轮验证:BQL/DQL)
  • [6. BQL/DQL 是什么](#6. BQL/DQL 是什么)
    • [6.1 BQL 和 qdisc 的关系](#6.1 BQL 和 qdisc 的关系)
    • [6.2 对实时延迟的影响](#6.2 对实时延迟的影响)
  • [7. 本次实验结论](#7. 本次实验结论)
  • [8. 后续计划](#8. 后续计划)

1. 背景

最近在 H3(AllWinner H3,NanoPi-NEO-Core 板型)上学习 Linux 4.14 网络 TX 路径、qdisc、softirq 与实时延迟之间的关系。

测试过程中发现,在 H3 作为 TCP 发送端时,tc -s qdisc show dev eth0requeues 增长非常明显。例如 5 分钟 TCP TX 测试后,fq_codel 的 requeues 可以达到数十万:

text 复制代码
qdisc mq 0: root
qdisc fq_codel 0: parent :1 limit 10240p flows 1024 quantum 1514 target 5ms interval 100ms memory_limit 32Mb ecn drop_batch 64
 Sent ... bytes ... pkt
 dropped 0, overlimits 0 requeues ...
 backlog 0b 0p

但与此同时,iperf3 吞吐基本稳定在 100Mbps 网口的实际上限附近:

text 复制代码
94Mbps 左右
Retr = 0
qdisc dropped = 0
qdisc backlog = 0

这引出了一个问题:

text 复制代码
qdisc requeues 很高,到底意味着什么?
是驱动 TX ring 满?
是 stmmac_xmit() 返回 NETDEV_TX_BUSY?
是 fq_codel 自身造成的排队?
还是 Linux 网络栈中的其它限流机制?

本文记录这次排查过程。

2. 环境

硬件:

text 复制代码
H3,AllWinner H3,4 核 ARMv7
NanoPi-NEO-Core 板型
100Mbps Ethernet (eth0)

内核:

text 复制代码
Linux 4.14.111
源码树:linux-sunxi-4.14.y

工具:

text 复制代码
trace-cmd 3.3.4
KernelShark 2.2.1
iperf3 3.16
tc
cyclictest
stress-ng
perf

当前 qdisc 配置:

sh 复制代码
# tc qdisc show dev eth0
qdisc mq 0: root
qdisc fq_codel 0: parent :1 limit 10240p flows 1024 quantum 1514 target 5ms interval 100ms memory_limit 32Mb ecn drop_batch 64
# cat /proc/sys/net/core/default_qdisc
fq_codel

也就是说,eth0 使用 mq root qdisc,实际 TX queue 上挂的是 fq_codel

3. 初始现象

H3 作为 TCP 发送端,Host 作为接收端:

sh 复制代码
iperf3 -c 192.168.1.188 -t 30

另外,找一台 Windows/Ubuntu 机器运行:

bash 复制代码
iperf3 -s

典型结果:

text 复制代码
[ ID] Interval           Transfer     Bitrate         Retr  Cwnd
[  5]   0.00-30.00  sec   337 MBytes  94.2 Mbits/sec    0             sender
[  5]   0.00-30.06  sec   337 MBytes  93.9 Mbits/sec                  receiver

吞吐稳定,重传为 0。

tc -s qdisc show dev eth0 显示 qdisc requeues 明显增长。直觉上容易怀疑:

text 复制代码
stmmac TX ring 满
  -> stmmac_xmit() 返回 NETDEV_TX_BUSY
  -> sch_direct_xmit() 调用 dev_requeue_skb()
  -> qdisc requeues++

于是第一步是验证驱动是否真的返回了 NETDEV_TX_BUSY

4. TX 路径关键代码

TX 基本路径如下:

text 复制代码
__dev_queue_xmit()
  -> __dev_xmit_skb()
    -> q->enqueue()
    -> __qdisc_run()
      -> qdisc_restart()
        -> dequeue_skb()
        -> sch_direct_xmit()
          -> dev_hard_start_xmit()
            -> xmit_one()
              -> ndo_start_xmit()
                -> stmmac_xmit()

最终都会进入驱动:

text 复制代码
ndo_start_xmit = stmmac_xmit

sch_direct_xmit() 中,如果发送失败,会调用 dev_requeue_skb(skb, q);dev_requeue_skb() 会增加 requeue 计数:

c 复制代码
int sch_direct_xmit(struct sk_buff *skb, struct Qdisc *q,
		    struct net_device *dev, struct netdev_queue *txq,
		    spinlock_t *root_lock, bool validate)
{
	int ret = NETDEV_TX_BUSY;

	...
	if (likely(skb)) {
		...
		if (!netif_xmit_frozen_or_stopped(txq))
			skb = dev_hard_start_xmit(skb, dev, txq, &ret); /* 调用网卡驱动接口 .ndo_start_xmit 传送数据 */
		...
	} else {
		...
	}

	if (dev_xmit_complete(ret)) {
		...
	} else {
		...
		/*
		 * NETDEV_TX_BUSY 表示驱动暂时没法接收这个 skb,例如 TX ring 满了、tx queue stopped。
		 * dev_requeue_skb() 会把 skb 放回 qdisc 。
		 */
		ret = dev_requeue_skb(skb, q);
	}
}

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

	return 0;
}

所以 tc -s qdisc 中看到的 requeues,正是来自这里。

5. 验证

5.1 第一轮验证:驱动是否返回 NETDEV_TX_BUSY

使用 trace-cmd 抓取。抓取前的 trace 配置:

bash 复制代码
mkdir -p /data/trace /data/qdisc /data/iperf3

echo 0 > /sys/kernel/debug/tracing/tracing_on
echo > /sys/kernel/debug/tracing/trace
echo 10240 > /sys/kernel/debug/tracing/buffer_size_kb
echo 1 > /sys/kernel/debug/tracing/tracing_on

tc -s qdisc show dev eth0 > /data/qdisc/requeue-trace-before.txt
cat /proc/softirqs > /data/qdisc/requeue-softirqs-before.txt
cat /proc/interrupts > /data/qdisc/requeue-interrupts-before.txt

trace-cmd 抓取命令:

sh 复制代码
trace-cmd record \
  -e net:net_dev_queue \
  -e net:net_dev_start_xmit \
  -e net:net_dev_xmit \
  -e irq:softirq_entry \
  -e irq:softirq_exit \
  -o /data/trace/net-tx-requeue-30s.dat \
  -- iperf3 -c 192.168.1.188 -t 30

抓取完成后,导出 trace 内容:

bash 复制代码
echo 0 > /sys/kernel/debug/tracing/tracing_on

tc -s qdisc show dev eth0 > /data/qdisc/requeue-trace-after.txt
cat /proc/softirqs > /data/qdisc/requeue-softirqs-after.txt
cat /proc/interrupts > /data/qdisc/requeue-interrupts-after.txt

cat /sys/kernel/debug/tracing/trace > /data/trace/stmmac-qdisc-requeue-30s.trace

然后查看 net_dev_xmit 中是否有 rc=16 (rc=16 即 rc=NETDEV_TX_BUSY):

sh 复制代码
trace-cmd report /data/trace/net-tx-requeue-30s.dat \
  | grep net_dev_xmit \
  | grep dev=eth0 \
  | grep rc=16 \
  | wc -l

结果:

text 复制代码
0

这说明:

text 复制代码
stmmac_xmit() 没有直接返回 NETDEV_TX_BUSY

也就是说,最初的假设不成立,这不是 stmmac 网卡驱动返回 NETDEV_TX_BUSY 导致的 qdisc requeue 计数增长。

5.2 第二轮验证:是不是 stmmac 主动 stop queue

stmmac 驱动中有两个明显的 stop queue 点。

第一个是 TX ring 可用描述符不足,当前 skb 直接无法提交:

c 复制代码
static netdev_tx_t stmmac_xmit(struct sk_buff *skb, struct net_device *dev)
{
	...
	if (unlikely(stmmac_tx_avail(priv, queue) < nfrags + 1)) {
			if (!netif_tx_queue_stopped(netdev_get_tx_queue(dev, queue))) {
				netif_tx_stop_queue(netdev_get_tx_queue(priv->dev,
									queue));
				/* This is a hard error, log it. */
				netdev_err(priv->dev,
					   "%s: Tx Ring full when queue awake\n",
					   __func__);
			}
			return NETDEV_TX_BUSY;
	}
	...
}

由于没有发现 "Tx Ring full when queue awake" 内核日志,,并且前面 net:net_dev_xmit 中也没有观察到 rc=16,所以这个点可以排除。

第二个是当前 skb 已成功提交,但 TX ring 剩余空间低于水位,提前 stop queue:

c 复制代码
static netdev_tx_t stmmac_xmit(struct sk_buff *skb, struct net_device *dev)
{
	...
	if (unlikely(stmmac_tx_avail(priv, queue) <= (MAX_SKB_FRAGS + 1))) {
		netif_dbg(priv, hw, priv->dev, "%s: stop transmitted packets\n",
			  __func__);
		netif_tx_stop_queue(netdev_get_tx_queue(priv->dev, queue));
	}
	...
}

为了验证,在 stmmac_xmit() 中增加 trace 点:

c 复制代码
static netdev_tx_t stmmac_xmit(struct sk_buff *skb, struct net_device *dev)
{
	...
	if (unlikely(stmmac_tx_avail(priv, queue) <= (MAX_SKB_FRAGS + 1))) {
		netif_dbg(priv, hw, priv->dev, "%s: stop transmitted packets\n",
			  __func__);
		trace_printk("stmmac_xmit_low_stop q=%u avail=%u thresh=%u cur=%u dirty=%u len=%u nfrags=%d\n",
		     queue, stmmac_tx_avail(priv, queue),
		     MAX_SKB_FRAGS + 1,
		     tx_q->cur_tx, tx_q->dirty_tx,
		     skb->len, nfrags);
		netif_tx_stop_queue(netdev_get_tx_queue(priv->dev, queue));
	}
	...
}

测试后统计:

sh 复制代码
grep -c 'stmmac_xmit_low_stop' /data/trace/stmmac-qdisc-requeue-30s.trace

结果:

text 复制代码
stmmac_xmit_low_stop       0

这说明:

text 复制代码
qdisc requeue 很多
但不是前面提到的 stmmac_xmit() 里的两个 stop queue 分支触发的

另外,同时也在 dev_requeue_skb() 调用前后、以及 dev_requeue_skb() 内添加了 trace:

c 复制代码
int sch_direct_xmit(struct sk_buff *skb, struct Qdisc *q,
		    struct net_device *dev, struct netdev_queue *txq,
		    spinlock_t *root_lock, bool validate)
{
	...
	if (likely(skb)) {
		...
		if (!netif_xmit_frozen_or_stopped(txq))
			skb = dev_hard_start_xmit(skb, dev, txq, &ret); /* dev_hard_start_xmit() 会改变 ret */
		...
	} else {
		...
	}
	...
	if (dev_xmit_complete(ret)) {
		...
	} 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);

		trace_printk("qdisc_xmit_after dev=%s ret=%d stopped=%d frozen_or_stopped=%d qlen=%u skb=%p len=%u\n",
		     dev->name, ret,
		     netif_xmit_stopped(txq),
		     netif_xmit_frozen_or_stopped(txq),
		     qdisc_qlen(q),
		     skb,
		     skb ? skb->len : 0);
		ret = dev_requeue_skb(skb, q);
		trace_printk("qdisc_xmit_after dev=%s ret=%d stopped=%d frozen_or_stopped=%d qlen=%u skb=%p len=%u\n",
		     dev->name, ret,
		     netif_xmit_stopped(txq),
		     netif_xmit_frozen_or_stopped(txq),
		     qdisc_qlen(q),
		     skb,
		     skb ? skb->len : 0);
	}
	...
}

static inline int dev_requeue_skb(struct sk_buff *skb, struct Qdisc *q)
{
	trace_printk("qdisc_requeue dev=%s qlen=%u requeues=%u skb=%p len=%u\n",
	     qdisc_dev(q)->name,
	     qdisc_qlen(q),
	     q->qstats.requeues,
	     skb,
	     skb ? skb->len : 0);
	q->gso_skb = skb;
	q->qstats.requeues++; /* 增加 qdisc requeue 计数 */
	qdisc_qstats_backlog_inc(q, skb);
	q->q.qlen++;	/* it's still part of the queue */
	__netif_schedule(q);

	return 0;
}

继续看 sch_direct_xmit() 中 requeue 前后的 trace:

text 复制代码
sch_direct_xmit: qdisc_xmit_after dev=eth0 ret=16 stopped=1 frozen_or_stopped=1 qlen=0 skb=... len=1514
sch_direct_xmit: qdisc_requeue dev=eth0 qlen=0 requeues=... skb=... len=1514
sch_direct_xmit: qdisc_xmit_after dev=eth0 ret=0 stopped=1 frozen_or_stopped=1 qlen=1 skb=... len=1514

这里 ret=16 表示 NETDEV_TX_BUSY。但是它不是驱动直接返回的,而是 dev_hard_start_xmit() 在批量发送 skb list 时发现 TX queue stopped 后设置的,即:

c 复制代码
struct sk_buff *dev_hard_start_xmit(struct sk_buff *first, struct net_device *dev,
				    struct netdev_queue *txq, int *ret)
{
	struct sk_buff *skb = first;
	int rc = NETDEV_TX_OK;

	while (skb) {
		struct sk_buff *next = skb->next;

		skb->next = NULL;
		rc = xmit_one(skb, dev, txq, next != NULL); /* 调用网卡驱动接口 .ndo_start_xmit = stmmac_xmit() 传送数据 */
		if (unlikely(!dev_xmit_complete(rc))) {
			skb->next = next;
			goto out;
		}

		skb = next;
		if (netif_xmit_stopped(txq) && skb) {
			rc = NETDEV_TX_BUSY; /* 发现 txq stopped,返回 NETDEV_TX_BUSY */
			break;
		}
	}

out:
	*ret = rc;
	return skb; /* 剩余 skb 链表, 把剩余 skb 链表返回给 qdisc */
}

5.3 第三轮验证:是不是进入 sch_direct_xmit 时 txq 已经 stopped

sch_direct_xmit() 里真正决定是否进入 dev_hard_start_xmit() 的逻辑如下:

c 复制代码
int sch_direct_xmit(struct sk_buff *skb, struct Qdisc *q,
		    struct net_device *dev, struct netdev_queue *txq,
		    spinlock_t *root_lock, bool validate)
{
	...
	if (likely(skb)) {
		HARD_TX_LOCK(dev, txq, smp_processor_id());
		if (!netif_xmit_frozen_or_stopped(txq))
			skb = dev_hard_start_xmit(skb, dev, txq, &ret);
		HARD_TX_UNLOCK(dev, txq);
	}
	...
}

如果进入这里时 txq 已经 stopped,那么根本不会调用 dev_hard_start_xmit()ret 会保持初始值 NETDEV_TX_BUSY

另外,sch_direct_xmit() 末尾还有如下逻辑:

c 复制代码
if (ret && netif_xmit_frozen_or_stopped(txq))
	ret = 0;

它的含义是:如果硬件/软件队列已经处于 stopped 状态,那么本轮不继续跑 qdisc,等待后续 wake/schedule。

为此增加 trace:

c 复制代码
int sch_direct_xmit(struct sk_buff *skb, struct Qdisc *q,
		    struct net_device *dev, struct netdev_queue *txq,
		    spinlock_t *root_lock, bool validate)
{
	int ret = NETDEV_TX_BUSY;

	...

	if (likely(skb)) {
		HARD_TX_LOCK(dev, txq, smp_processor_id());
		trace_printk("qdisc_before_hard_xmit dev=%s stopped=%d frozen_or_stopped=%d qlen=%u skb=%p len=%u\n",
		     dev->name,
		     netif_xmit_stopped(txq),
		     netif_xmit_frozen_or_stopped(txq),
		     qdisc_qlen(q),
		     skb,
		     skb ? skb->len : 0);
		if (!netif_xmit_frozen_or_stopped(txq)) {
			skb = dev_hard_start_xmit(skb, dev, txq, &ret);
		} else {
			trace_printk("qdisc_skip_hard_xmit dev=%s stopped=%d frozen_or_stopped=%d qlen=%u skb=%p len=%u\n",
			     dev->name,
			     netif_xmit_stopped(txq),
			     netif_xmit_frozen_or_stopped(txq),
			     qdisc_qlen(q),
			     skb,
			     skb ? skb->len : 0);
		}	

		HARD_TX_UNLOCK(dev, txq);
	} else {
		...
	}

	...

	if (ret && netif_xmit_frozen_or_stopped(txq))
		ret = 0;

	return ret;
}

测试结果:

text 复制代码
qdisc_skip_hard_xmit       13
qdisc_requeue              21312
qdisc_before_hard_xmit     26276

qdisc_skip_hard_xmit 远小于 qdisc_requeue,因此大部分 requeue 不是因为进入 sch_direct_xmit() 前 txq 就已经 stopped。

那大部分 requeue 更可能来自:

c 复制代码
struct sk_buff *dev_hard_start_xmit(struct sk_buff *first, struct net_device *dev,
				    struct netdev_queue *txq, int *ret)
{
	...
	while (skb) {
		...
		if (netif_xmit_stopped(txq) && skb) {
			rc = NETDEV_TX_BUSY;
			break;
		}
	}
	...
}

也就是说:

text 复制代码
某个 skb 已经通过 stmmac_xmit() 成功提交
但随后 txq 状态变成 stopped
dev_hard_start_xmit() 不再继续发送 skb list 中剩余 skb,返回 NETDEV_TX_BUSY
剩余 skb 返回 qdisc
sch_direct_xmit() 看到 dev_hard_start_xmit() 返回 NETDEV_TX_BUSY 调用 dev_requeue_skb()

即:

c 复制代码
int sch_direct_xmit(struct sk_buff *skb, struct Qdisc *q,
		    struct net_device *dev, struct netdev_queue *txq,
		    spinlock_t *root_lock, bool validate)
{
	...
	if (dev_xmit_complete(ret)) {
		...
	} else {
		...
		/*
		 * NETDEV_TX_BUSY 表示驱动暂时没法接收这个 skb,例如 TX ring 满了、tx queue stopped。
		 * dev_requeue_skb() 会把 skb 放回 qdisc 。
		 */
		ret = dev_requeue_skb(skb, q);
	}
	...
}

新的问题变成:

text 复制代码
如果不是 stmmac_xmit() 调用 netif_tx_stop_queue()
那是谁把 txq 设置成 stopped?

5.4 第四轮验证:BQL/DQL

查看 include/linux/netdevice.h,可以看到网络栈还有一套 BQL(Byte Queue Limits) 机制。

驱动成功提交 skb 后,stmmac 会调用 netdev_tx_sent_queue()

c 复制代码
static netdev_tx_t stmmac_xmit(struct sk_buff *skb, struct net_device *dev)
{
	...
	netdev_tx_sent_queue(netdev_get_tx_queue(dev, queue), skb->len);
	...
}

该函数内部在 CONFIG_BQL 开启时会调用 DQL:

c 复制代码
static inline void netdev_tx_sent_queue(struct netdev_queue *dev_queue,
					unsigned int bytes)
{
#ifdef CONFIG_BQL
	dql_queued(&dev_queue->dql, bytes);

	if (likely(dql_avail(&dev_queue->dql) >= 0))
		return;

	set_bit(__QUEUE_STATE_STACK_XOFF, &dev_queue->state);

	/*
	 * The XOFF flag must be set before checking the dql_avail below,
	 * because in netdev_tx_completed_queue we update the dql_completed
	 * before checking the XOFF flag.
	 */
	smp_mb();

	/* check again in case another CPU has just made room avail */
	if (unlikely(dql_avail(&dev_queue->dql) >= 0))
		clear_bit(__QUEUE_STATE_STACK_XOFF, &dev_queue->state);
#endif
}

这意味着:

text 复制代码
如果 DQL 判断当前 in-flight bytes 超过 limit
网络栈会设置 __QUEUE_STATE_STACK_XOFF
此后 netif_xmit_stopped(txq) 为 true

TX completion 清理时(即 stmmac_tx_clean()),stmmac 会调用 netdev_tx_completed_queue()

c 复制代码
static void stmmac_tx_clean(struct stmmac_priv *priv, u32 queue)
{
	...
	/* TX completion 清理,重启 stmmac_xmit() 中 netdev_tx_sent_queue() stop 的 tx queue */
	netdev_tx_completed_queue(netdev_get_tx_queue(priv->dev, queue),
				  pkts_compl, bytes_compl);
	...
}

netdev_tx_completed_queue() 内部逻辑:

c 复制代码
static inline void netdev_tx_completed_queue(struct netdev_queue *dev_queue,
					     unsigned int pkts, unsigned int bytes)
{
#ifdef CONFIG_BQL
	if (unlikely(!bytes))
		return;

	dql_completed(&dev_queue->dql, bytes);

	/*
	 * Without the memory barrier there is a small possiblity that
	 * netdev_tx_sent_queue will miss the update and cause the queue to
	 * be stopped forever
	 */
	smp_mb();

	if (dql_avail(&dev_queue->dql) < 0)
		return;

	if (test_and_clear_bit(__QUEUE_STATE_STACK_XOFF, &dev_queue->state))
		netif_schedule_queue(dev_queue);
#endif
}

也就是说,BQL/DQL 会在 TX in-flight bytes 过高时 stop queue,在 TX completion 回收后 wake/schedule queue。

为验证这一点,在 netdev_tx_sent_queue()netdev_tx_completed_queue() 增加 trace:

c 复制代码
static inline void netdev_tx_sent_queue(struct netdev_queue *dev_queue,
					unsigned int bytes)
{
#ifdef CONFIG_BQL
	...
	if (likely(dql_avail(&dev_queue->dql) >= 0))
		return;

	trace_printk("bql_stack_xoff dev=%s bytes=%u dql_avail=%d queued=%u completed=%u limit=%u\n",
	     dev_queue->dev->name,
	     bytes,
	     dql_avail(&dev_queue->dql),
	     dev_queue->dql.num_queued,
	     dev_queue->dql.num_completed,
	     dev_queue->dql.limit);
	set_bit(__QUEUE_STATE_STACK_XOFF, &dev_queue->state);
	...
#endif
}

static inline void netdev_tx_completed_queue(struct netdev_queue *dev_queue,
					     unsigned int pkts, unsigned int bytes)
{
#ifdef CONFIG_BQL
	...

	if (dql_avail(&dev_queue->dql) < 0)
		return;

	if (test_and_clear_bit(__QUEUE_STATE_STACK_XOFF, &dev_queue->state)) {
		trace_printk("bql_stack_wake dev=%s pkts=%u bytes=%u dql_avail=%d queued=%u completed=%u limit=%u\n",
		     dev_queue->dev->name,
		     pkts, bytes,
		     dql_avail(&dev_queue->dql),
		     dev_queue->dql.num_queued,
		     dev_queue->dql.num_completed,
		     dev_queue->dql.limit);
		netif_schedule_queue(dev_queue);
	}
#endif
}

测试结果:

text 复制代码
qdisc_requeue              19394
qdisc_xmit_after           38788 (38788 / 2 = 19394)
qdisc_skip_hard_xmit       49
bql_stack_xoff             20104
bql_stack_wake             20103
stmmac_xmit_low_stop       0

关键 trace 片段:

text 复制代码
stmmac_xmit: bql_stack_xoff dev=eth0 bytes=1514 dql_avail=-1514 queued=262137298 completed=262040402 limit=95382
sch_direct_xmit: qdisc_xmit_after dev=eth0 ret=16 stopped=1 frozen_or_stopped=1 qlen=0 skb=... len=1514
sch_direct_xmit: qdisc_requeue dev=eth0 qlen=0 requeues=44888 skb=... len=1514
stmmac_tx_clean: bql_stack_wake dev=eth0 pkts=3 bytes=4542 dql_avail=3028 queued=262137298 completed=262044944 limit=95382

这个时序已经非常清楚:

text 复制代码
stmmac_xmit()
  -> netdev_tx_sent_queue()
  -> dql_avail < 0
  -> 设置 __QUEUE_STATE_STACK_XOFF

回到 dev_hard_start_xmit()
  -> netif_xmit_stopped(txq) == true
  -> ret = NETDEV_TX_BUSY
  -> 返回 skb list 中剩余 skb

sch_direct_xmit()
  -> dev_requeue_skb()
  -> qdisc requeues++

stmmac_tx_clean()
  -> netdev_tx_completed_queue()
  -> dql_avail >= 0
  -> 清除 __QUEUE_STATE_STACK_XOFF
  -> netif_schedule_queue()

到此,已经厘清了 qdisc requeue 数大的问题,在于 stmmac_xmit() 调用 netdev_tx_sent_queue() 触发的 BQL 限流。接下来,我们简单的探讨下什么是 BQL/DQL。

6. BQL/DQL 是什么

BQL 是 Byte Queue Limits,DQL 是 Dynamic Queue Limits。

可以简单理解为:

text 复制代码
BQL/DQL 是网络 TX 路径中限制驱动/硬件队列 in-flight bytes 的机制。

它关心的是:

text 复制代码
queued bytes     已提交给驱动/硬件的字节数
completed bytes  已经完成发送并回收的字节数
in-flight bytes  queued - completed 的字节数
limit            当前动态允许的 in-flight 上限

当:

text 复制代码
in-flight bytes > limit

也就是:

c 复制代码
dql_avail(&txq->dql) < 0

BQL 会设置:

c 复制代码
__QUEUE_STATE_STACK_XOFF

使得:

c 复制代码
netif_xmit_stopped(txq) == true

于是 qdisc 不会继续无限制地向驱动/硬件 TX ring 塞 skb,而是把后续 skb 留在 qdisc 中。

6.1 BQL 和 qdisc 的关系

一句话总结:

text 复制代码
Qdisc 决定"下一个该发谁",BQL 决定"现在还能往硬件里塞多少"。

Qdisc 管的是驱动之前的软件队列:

text 复制代码
分类
排队
调度
丢包
ECN
fairness

BQL 管的是驱动/硬件队列的 in-flight 数据量:

text 复制代码
已经提交给硬件但尚未完成发送的字节数不能太多

没有 BQL 时:

text 复制代码
qdisc 可能一次性 dequeue 很多 skb
驱动 TX ring 被塞满
真实排队转移到驱动/硬件
qdisc 算法失去部分控制力

有 BQL 后:

text 复制代码
qdisc dequeue 若干 skb
BQL 发现 in-flight 到达 limit
设置 STACK_XOFF
后续 skb 被挡回 qdisc
等待 TX completion 后继续发送

这样做的意义是:

text 复制代码
减少驱动/硬件队列中的隐藏排队
避免 bufferbloat
让 fq_codel 等 qdisc 仍然能控制排队位置

因此,在本次测试中,qdisc requeues 高并不代表网络异常。它更多说明 BQL 正在正常工作。

6.2 对实时延迟的影响

虽然 BQL requeue 本身不是故障,但它揭示了另一个问题:

text 复制代码
TX 完成清理 stmmac_tx_clean()
BQL wake
qdisc 重新调度
NET_TX / NET_RX softirq

这些工作可能集中在网卡 IRQ CPU 上执行。

在 H3 上,通过网卡 IRQ CPU 绑定实验可以看到(有兴趣的读者可自行找一台 ARM 机器实验):

text 复制代码
eth0 IRQ 绑定到哪个 CPU,哪个 CPU 的 NET_TX / NET_RX / TASKLET / stmmac_tx_clean 压力就明显增加。

因此,对于实时任务,优化重点不是"消灭 requeues",而是:

text 复制代码
网络 IRQ/softirq 放到非实时 CPU
实时线程避开 eth0 IRQ CPU
必要时继续研究 BQL limit、TX clean 频率、TX coalescing 对延迟的影响

7. 本次实验结论

本次排查可以得到以下结论:

  1. H3 TX 测试中,qdisc requeues 高不是因为 stmmac_xmit() 直接返回 NETDEV_TX_BUSY

  2. "Tx Ring full when queue awake" 内核日志 与 stmmac_xmit_low_stop 均未触发,说明不是 stmmac 驱动主动 netif_tx_stop_queue() 造成的。

  3. 大部分 requeue 来自 dev_hard_start_xmit() 批量发送 skb list 时,发现 netif_xmit_stopped(txq) 为 true,于是将剩余 skb 返回(即 requeue 回) qdisc。

  4. netif_xmit_stopped(txq) 的主要来源是 BQL/DQL 设置的 __QUEUE_STATE_STACK_XOFF

  5. stmmac_tx_clean() 调用 netdev_tx_completed_queue() 后,BQL/DQL 清除 STACK_XOFF 并重新调度 qdisc。

  6. 在 100Mbps 网口已经跑满、TCP Retr 为 0、qdisc drop/backlog 为 0 的情况下,高 requeues 更像是正常的 BQL 限流行为,而不是性能故障。

8. 后续计划

后续继续围绕实时延迟和网络软中断布局做实验:

text 复制代码
1. 网络 IRQ/softirq 放到非实时 CPU
2. 实时线程避开 eth0 IRQ CPU
3. 深入观察 BQL limit、TX clean 频率、TX coalescing 对 latency tail 的影响

下一阶段重点不是单纯追求更高吞吐,而是建立:

text 复制代码
吞吐
softirq CPU 分布
BQL XOFF/Wake 频率
cyclictest tail latency

之间的对应关系。