文章目录
- [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 eth0 中 requeues 增长非常明显。例如 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. 本次实验结论
本次排查可以得到以下结论:
-
H3 TX 测试中,
qdisc requeues高不是因为stmmac_xmit()直接返回NETDEV_TX_BUSY。 -
"Tx Ring full when queue awake" 内核日志 与
stmmac_xmit_low_stop均未触发,说明不是 stmmac 驱动主动netif_tx_stop_queue()造成的。 -
大部分 requeue 来自
dev_hard_start_xmit()批量发送 skb list 时,发现netif_xmit_stopped(txq)为 true,于是将剩余 skb 返回(即 requeue 回) qdisc。 -
netif_xmit_stopped(txq)的主要来源是 BQL/DQL 设置的__QUEUE_STATE_STACK_XOFF。 -
stmmac_tx_clean()调用netdev_tx_completed_queue()后,BQL/DQL 清除STACK_XOFF并重新调度 qdisc。 -
在 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
之间的对应关系。