![tun 设备上的流量整形无效](https://linux22.com/image/227069/tun%20%E8%AE%BE%E5%A4%87%E4%B8%8A%E7%9A%84%E6%B5%81%E9%87%8F%E6%95%B4%E5%BD%A2%E6%97%A0%E6%95%88.png)
我正在开发一个隧道应用程序,它将提供低延迟、可变带宽链接。这将在需要流量优先级的系统中运行。然而,虽然流向 tun 设备的流量显然已由内核排队,但无论我对设备应用什么 qdisc,它都没有额外的效果,包括默认的 pfifo_fast,即应该是高优先级的流量没有与正常流量分开处理交通。
我制作了一个小型测试应用程序来演示该问题。它创建两个 tun 设备并具有两个线程,每个线程都有一个循环,分别将数据包从一个接口传递到另一个接口并传回。在接收和发送之间,每个字节循环延迟 1us,大致模拟 8Mbps 双向链路:
void forward_traffic(int src_fd, int dest_fd) {
char buf[BUFSIZE];
ssize_t nbytes = 0;
while (nbytes >= 0) {
nbytes = read(src_fd, buf, sizeof(buf));
if (nbytes >= 0) {
usleep(nbytes);
nbytes = write(dest_fd, buf, nbytes);
}
}
perror("Read/write TUN device");
exit(EXIT_FAILURE);
}
将每个 tun 接口放置在自己的命名空间中,我可以运行 iperf3 并获得大约 8Mbps 的吞吐量。 ip link 报告的默认 txqlen 是 500 个数据包,当我同时运行 iperf3 (-P 20) 和 ping 时,我看到大约 670-770 毫秒的 RTT,大致相当于 500 x 1500 字节的队列。事实上,改变 txqlen 会成比例地改变延迟。到目前为止,一切都很好。
使用默认的 pfifo_fast qdisc,我希望具有正确 ToS 标记的 ping 会跳过该正常队列并给我一个低延迟,例如 ping -Q 0x10 我认为应该具有更低的 RTT,但事实并非如此(我已经尝试过其他 ToS) /DSCP 值也相同 - 它们都有相同的约 700 毫秒 RTT。此外,我尝试了各种其他 qdisc,得到了相同的结果,例如 fq_codel 对延迟没有显着影响,无论 qdisc 是什么,tc -s qdisc 总是显示。无论链路是否拥塞,积压均为 0(但我确实看到 ip -s link 显示拥塞情况下丢包)。
我是否从根本上误解了这里的某些内容,或者我需要做些什么才能使 qdisc 有效?
答案1
因此,在阅读和翻阅内核源代码之后,qdisc 似乎无效,因为 tun 驱动程序不会告诉网络堆栈它正忙。它只是将数据包保存在自己的本地队列中(其大小由 txqlen 设置),当队列已满时,它只是丢弃多余的数据包。
以下是 drivers/net/tun.c 中传输函数的相关位,当堆栈想要发送数据包时,将调用该函数:
/* Net device start xmit */
static netdev_tx_t tun_net_xmit(struct sk_buff *skb, struct net_device *dev)
{
struct tun_struct *tun = netdev_priv(dev);
int txq = skb->queue_mapping;
struct tun_file *tfile;
int len = skb->len;
rcu_read_lock();
tfile = rcu_dereference(tun->tfiles[txq]);
....... Various unrelated things omitted .......
if (ptr_ring_produce(&tfile->tx_ring, skb))
goto drop;
/* Notify and wake up reader process */
if (tfile->flags & TUN_FASYNC)
kill_fasync(&tfile->fasync, SIGIO, POLL_IN);
tfile->socket.sk->sk_data_ready(tfile->socket.sk);
rcu_read_unlock();
return NETDEV_TX_OK;
drop:
this_cpu_inc(tun->pcpu_stats->tx_dropped);
skb_tx_error(skb);
kfree_skb(skb);
rcu_read_unlock();
return NET_XMIT_DROP;
}
}
典型的网络接口驱动程序应调用 netif_stop_queue() 和 netif_wake_queue() 函数来停止和启动来自网络堆栈的数据包流。当流量停止时,数据包将按照附加的队列规则进行排队,从而使用户能够更灵活地管理流量和确定优先级。
无论出于何种原因,tap/tun 驱动程序都不会执行此操作 - 可能是因为大多数隧道只是简单地封装数据包并将其发送到真实的网络接口,而无需任何额外的流量控制。
为了验证我的发现,我通过停止上面函数中的流量控制尝试了一个简单的测试:
if (ptr_ring_produce(&tfile->tx_ring, skb)) {
netif_stop_queue(dev);
goto drop;
} else if (ptr_ring_full(&tfile->tx_ring)) {
netif_stop_queue(dev);
tun_debug(KERN_NOTICE, tun, "tun_net_xmit stop %lx\n", (size_t)skb);
}
并对 tun_ring_recv 进行了类似的添加,以根据数据包出队后队列是否为空来停止/唤醒队列:
empty = __ptr_ring_empty(&tfile->tx_ring);
if (empty)
netif_wake_queue(tun->dev);
else
netif_stop_queue(tun->dev);
这不是一个很好的系统,并且不能与多队列隧道一起使用,但它工作得足够好,我可以看到 qdisc 报告积压,并且当链接时在不同 ToS 级别使用 pfifo_fast 的 ping 时间和丢失率有明显差异已满负荷。