源码追溯:网卡队列超时

在Linux网络编程和系统管理中,网卡队列超时机制是一个至关重要的组成部分,它对于确保网络通信的稳定性和效率起着关键作用。

网卡队列概念

在Linux中,网卡队列(TXQ)是网卡驱动程序用于管理待发送数据包的缓冲区。当系统需要发送数据时,数据包会被添加到网卡队列中,等待网络适配器驱动模块的处理。网卡队列的引入使得数据包发送过程更加高效,因为系统可以在数据包被实际发送之前,将多个数据包放入队列中,从而实现批量处理。

一个CPU处理一个队列的数据, 这个叫中断. 默认是cpu0(第一个CPU)处理. 一旦流量特别大, 这个CPU负载很高, 性能存在瓶颈. 因此网卡逐步开发了多队列功能, 即一个网卡有多个队列, 收到的包根据TCP四元组信息hash后放入其中一个队列, 后面该链接的所有包都放入该队列. 每个队列对应不同的中断, 使用irqbalance将不同的中断绑定到不同的核. 充分利用了多核并行处理特性. 提高了效率。于是乎一块物理网卡,一般有63个队列。经过SR-IOV后的VF网卡有9个队列。

查看队列数量

使用ethtool -l 网卡的命令查看combined的数量,即为队列数量,总计63个

[root@server01]# ethtool -l eth4
Channel parameters for eth4:
Pre-set maximums:
RX:             0
TX:             0
Other:          0
Combined:       63  (最大值)
Current hardware settings:
RX:             0
TX:             0
Other:          0
Combined:       63 (当前设定值)

查看队列明细

队列分为RX和TX,根据网卡驱动特性,TX和RX的数量会动态调整。查看命令如下

ls  /sys/class/net/eth0/queues/
rx-0    rx-103  rx-109  rx-114  rx-12   rx-125  rx-18  rx-23  rx-29  rx-34  rx-4   rx-45  rx-50  rx-56  rx-61  rx-67  rx-72  rx-78  rx-83  rx-89  rx-94  tx-0   tx-14  tx-2   tx-25  tx-30  tx-36  tx-41  tx-47  tx-52  tx-58  tx-7
rx-1    rx-104  rx-11   rx-115  rx-120  rx-13   rx-19  rx-24  rx-3   rx-35  rx-40  rx-46  rx-51  rx-57  rx-62  rx-68  rx-73  rx-79  rx-84  rx-9   rx-95  tx-1   tx-15  tx-20  tx-26  tx-31  tx-37  tx-42  tx-48  tx-53  tx-59  tx-8
rx-10   rx-105  rx-110  rx-116  rx-121  rx-14   rx-2   rx-25  rx-30  rx-36  rx-41  rx-47  rx-52  rx-58  rx-63  rx-69  rx-74  rx-8   rx-85  rx-90  rx-96  tx-10  tx-16  tx-21  tx-27  tx-32  tx-38  tx-43  tx-49  tx-54  tx-6   tx-9
rx-100  rx-106  rx-111  rx-117  rx-122  rx-15   rx-20  rx-26  rx-31  rx-37  rx-42  rx-48  rx-53  rx-59  rx-64  rx-7   rx-75  rx-80  rx-86  rx-91  rx-97  tx-11  tx-17  tx-22  tx-28  tx-33  tx-39  tx-44  tx-5   tx-55  tx-60
rx-101  rx-107  rx-112  rx-118  rx-123  rx-16   rx-21  rx-27  rx-32  rx-38  rx-43  rx-49  rx-54  rx-6   rx-65  rx-70  rx-76  rx-81  rx-87  rx-92  rx-98  tx-12  tx-18  tx-23  tx-29  tx-34  tx-4   tx-45  tx-50  tx-56  tx-61
rx-102  rx-108  rx-113  rx-119  rx-124  rx-17   rx-22  rx-28  rx-33  rx-39  rx-44  rx-5   rx-55  rx-60  rx-66  rx-71  rx-77  rx-82  rx-88  rx-93  rx-99  tx-13  tx-19  tx-24  tx-3   tx-35  tx-40  tx-46  tx-51  tx-57  tx-62

修改队列

通过ethtool命令可以在线修改队列的数值,修改过程中会持续3-4秒后才会返回。

[root@server01 ~]# ethtool -L eth0 combined 60
[root@server01 ~]# ethtool -l eth0
Channel parameters for eth0:
Pre-set maximums:
RX:             0
TX:             0
Other:          0
Combined:       63(系统默认值)
Current hardware settings:
RX:             0
TX:             0
Other:          0
Combined:       60  (修改后的值)

修改后通过ethtool命令可以看到对应的值动态调整了。调整为了60, 查看队列,RX和TX都动态进行了调整。

ls  /sys/class/net/eth4/queues/
rx-0    rx-102  rx-107  rx-111  rx-116  rx-13  rx-18  rx-22  rx-27  rx-31  rx-36  rx-40  rx-45  rx-5   rx-54  rx-59  rx-63  rx-68  rx-72  rx-77  rx-81  rx-86  rx-90  rx-95  tx-0   tx-13  tx-18  tx-22  tx-27  tx-31  tx-36  tx-40  tx-45  tx-5   tx-54  tx-59
rx-1    rx-103  rx-108  rx-112  rx-117  rx-14  rx-19  rx-23  rx-28  rx-32  rx-37  rx-41  rx-46  rx-50  rx-55  rx-6   rx-64  rx-69  rx-73  rx-78  rx-82  rx-87  rx-91  rx-96  tx-1   tx-14  tx-19  tx-23  tx-28  tx-32  tx-37  tx-41  tx-46  tx-50  tx-55  tx-6
rx-10   rx-104  rx-109  rx-113  rx-118  rx-15  rx-2   rx-24  rx-29  rx-33  rx-38  rx-42  rx-47  rx-51  rx-56  rx-60  rx-65  rx-7   rx-74  rx-79  rx-83  rx-88  rx-92  rx-97  tx-10  tx-15  tx-2   tx-24  tx-29  tx-33  tx-38  tx-42  tx-47  tx-51  tx-56  tx-7
rx-100  rx-105  rx-11   rx-114  rx-119  rx-16  rx-20  rx-25  rx-3   rx-34  rx-39  rx-43  rx-48  rx-52  rx-57  rx-61  rx-66  rx-70  rx-75  rx-8   rx-84  rx-89  rx-93  rx-98  tx-11  tx-16  tx-20  tx-25  tx-3   tx-34  tx-39  tx-43  tx-48  tx-52  tx-57  tx-8
rx-101  rx-106  rx-110  rx-115  rx-12   rx-17  rx-21  rx-26  rx-30  rx-35  rx-4   rx-44  rx-49  rx-53  rx-58  rx-62  rx-67  rx-71  rx-76  rx-80  rx-85  rx-9   rx-94  rx-99  tx-12  tx-17  tx-21  tx-26  tx-30  tx-35  tx-4   tx-44  tx-49  tx-53  tx-58  tx-9

查看网卡中断号

在/proc/inteerupts内记录了所有硬件设备的中断号,类似eth0有63个队列,对应就有63个中断号。分别为eth-0 到eth-62,总计63个中断号。

[root@syn45053305 ~]# cat /proc/interrupts | grep eth0 | wc -l
63

网卡涉及的中断号为连续的中断号,从456到518。

[root@server01 ~]# cat /proc/interrupts | grep eth0- | awk '{print $1,$66,$67}'
456: IR-PCI-MSI-edge eth0-0
457: IR-PCI-MSI-edge eth0-1
......
518: IR-PCI-MSI-edge eth0-62

查看网卡中断亲缘性

针对中断456查看对应负责的CPU

[root@server ~]# cat /proc/irq/456/smp_affinity
00000000,00000200

返回值通过中断掩码的十六进制表示,用于指定特定 CPU 核心来处理特定的中断。具体解释如下:

每个逗号之前的八位十六进制数(例如 00000200 和 00000000)表示一个字节(8位),对应系统中的 CPU 核心或者一组 CPU 核心。

  • 00000200 表示 CPU 9 (或者是一个高位位于 16 位中的一个 CPU 核心,具体取决于系统配置)被设置为处理这个中断。这个设置表明中断 456 只会被配置在一个 CPU 核心上处理,即 CPU 9。

因此,这个配置表明中断 456 只会在 CPU 9 上进行处理。这种配置有助于优化系统性能,避免中断处理的竞争和效率问题,特别是在多核系统中分配中断处理任务。

如果看不懂十六进制,可以直接查看smp_affinity_list, 这里显示的是十进制的负责处理的CPU核

[root@server ~]# cat /proc/irq/456/smp_affinity_list
9

tips:十六进制掩码转换

00000200 每四位一组为 0000 和 0200。
将 0200 转换为二进制: 0000 0010 0000 0000。
然后将二进制数 0000 0010 0000 0000
因为对应就是第十位为1,对应的就是CPU9

设置中断均衡

可以在线把网卡对应的中断号绑定到单独的CPU核上,设置中断号绑核需要将绑定CPU的信息设置到对应中断号上,命令如下

#将中断号46绑在CPU0上
echo 00000001 > /proc/irq/46/smp_affinity
##CPU ID对应关系
00000001 -----> 对应CPU0
00000002 -----> 对应CPU1
00000004 -----> 对应CPU2
00000008 -----> 对应CPU3
00000010 -----> 对应CPU4

如果希望多个CPU参与中断处理的话,可以使用类似下面的语法:

//绑定CPU3和CPU5
echo 3,5 > /proc/irq/45/smp_affinity_list
//绑定CPU0-7,共8个
echo 0-7 > /proc/irq/45/smp_affinity_list

网卡超时机制作用

超时机制在网卡队列中扮演着重要的角色。当数据包在队列中等待过长时间而无法发送时,超时机制会触发相应的处理流程,以避免数据包长时间占用队列资源,导致系统性能下降。具体来说,超时机制可以实现以下功能:

  1. 释放资源:当数据包超时未发送时,系统会将其从队列中移除,释放相应的内存资源,以便其他数据包使用。
  2. 错误处理:超时机制可以检测数据包发送过程中的异常情况,如网络故障、驱动错误等,并触发相应的错误处理流程。
  3. 流量控制:通过调整超时时间,系统可以实现对网络流量的控制,防止因网络拥塞导致的性能下降。

源码分析

内核源码

在内核模块中,网卡的超时机制一般通过watchdog机制实现,

通过dev_watchdog函数是监控网络设备传输队列的超时情况,当某个传输队列超时时,会触发相应的处理逻辑,并定时更新 watchdog 定时器以继续监控传输队列状态。

dev_watchdog

static void dev_watchdog(struct timer_list *t)
{
    //使用 from_timer 宏将 timer_list 结构转换为包含该结构的容器结构体 net_device
    //其中watchdog_timer 是net_device 结构中的一个计时器。
    struct net_device *dev = from_timer(dev, t, watchdog_timer);
    //锁定网络设备的传输,以防止其他线程同时修改传输队列。
    netif_tx_lock(dev);
    //检查是否设置了传输队列处理器,并且设备存在且运行中,并且连接状态正常。
    if (!qdisc_tx_is_noop(dev)) {
        if (netif_device_present(dev) &&
            netif_running(dev) &&
            netif_carrier_ok(dev)) {
            int some_queue_timedout = 0;
            unsigned int i;
            unsigned long trans_start;
            //遍历设备的所有传输队列,对每个队列进行超时检查。
            for (i = 0; i < dev->num_tx_queues; i++) {
                struct netdev_queue *txq;
                txq = netdev_get_tx_queue(dev, i);
                trans_start = txq->trans_start;
                if (netif_xmit_stopped(txq) &&
                    time_after(jiffies, (trans_start +
                             dev->watchdog_timeo))) {
                    some_queue_timedout = 1;
                    txq->trans_timeout++;
                    break;
                }
            }
            //如果某个队列超时,打印警告信息,调用网络设备操作的ndo_tx_timeout 方法处理传输超时。
            if (some_queue_timedout) {
                trace_net_dev_xmit_timeout(dev, i);
                WARN_ONCE(1, KERN_INFO "NETDEV WATCHDOG: %s (%s): transmit queue %u timed out\n",
                       dev->name, netdev_drivername(dev), i);
                dev->netdev_ops->ndo_tx_timeout(dev, i);
            }
            //更新设备的 watchdog 定时器,以在设定的时间后再次触发 watchdog 函数。
            if (!mod_timer(&dev->watchdog_timer,
                       round_jiffies(jiffies +
                             dev->watchdog_timeo)))
                dev_hold(dev);
        }
    }
    //解锁网络传输并释放对设备的引用,以允许其他操作可以继续对设备进行操作。
    netif_tx_unlock(dev);
    dev_put(dev);
}

dev_watchdog()代码中通过dev->netdev_ops->ndo_tx_timeout将具体的超时机制处理过程关联至对应的网卡驱动上。核心处理逻辑如下:

dev_watchdog函数负责对网卡的传输队列进行超时监控,并通过变量some_queue_timedout用于判断是否超时,如果超时,some_queue_timedout的值会增加,并进入超时处理,调用ndo_tx_timeout进行处理。

        //- 遍历设备的所有传输队列,获取每个队列的传输开始时间 trans_start。
        for (i = 0; i < dev->num_tx_queues; i++) {
            struct netdev_queue *txq;

            txq = netdev_get_tx_queue(dev, i);
            trans_start = txq->trans_start;
            //- 检查队列是否停止传输且当前时间是否超过传输开始时间加上设备的看门狗超时时间 dev->watchdog_timeo(15秒)。
            if (netif_xmit_stopped(txq) &&
                time_after(jiffies, (trans_start +
                         dev->watchdog_timeo))) {
                //如果发现超时,则标记 some_queue_timedout 并增加该队列的超时计数 txq->trans_timeout,然后退出循环
                some_queue_timedout = 1;
                txq->trans_timeout++;
                break;

mellanox网卡

如果Linux系统中使用的是mlx5的网卡,则会调用到如下驱动文件

初始化配置mlx5e_build_nic_netdev

在编译网卡驱动时,在代码中会写死超时时间,mellanox的网卡超时为15秒。

static void mlx5e_build_nic_netdev(struct net_device *netdev)
{
    ......
    //15*时钟频率,一般为1000毫秒
    netdev->watchdog_timeo    = 15 * HZ;
    .......
}

关联函数net_device_ops

内核函数调用时,ndo_tx_timeout会关联到 mlx5e_tx_timeout

const struct net_device_ops mlx5e_netdev_ops = {
    .ndo_open                = mlx5e_open,
    .ndo_stop                = mlx5e_close,
    ......
    .ndo_tx_timeout          = mlx5e_tx_timeout,
    ......
};

ndo_tx_timeout直接关联到mlx5_core驱动中的mlx5e_tx_timeout函数。

mlx5e_tx_timeout

mlx5e_tx_timeout函数会打印报错内容,通知内核存在异常,同时调用queue_work函数进行处理,调用mlx5e_tx_timeout_work

static void mlx5e_tx_timeout(struct net_device *dev)
{
    struct mlx5e_priv *priv = netdev_priv(dev);
         //打印对应报错信息
    netdev_err(dev, "TX timeout detected\n"); 
         //调用mlx5e_tx_timeout_work(等同于&priv->tx_timeout_work)
    queue_work(priv->wq, &priv->tx_timeout_work);
}

mlx5e_tx_timeout_work

该函数会首先尝试进行中断恢复来处理队列中的数据,进一步调用函数mlx5e_tx_timeout_eq_recover进行中断恢复。如果无法修复就会调用clear_bit进行清理并重置队列

static void mlx5e_tx_timeout_work(struct work_struct *work)
{
......
    for (i = 0; i < priv->channels.num * priv->channels.params.num_tc; i++) {
        struct netdev_queue *dev_queue = netdev_get_tx_queue(dev, i);
        struct mlx5e_txqsq *sq = priv->txq2sq[i];

        if (!netif_xmit_stopped(dev_queue))
            continue;
        netdev_err(dev,
               "TX timeout on queue: %d, SQ: 0x%x, CQ: 0x%x, SQ Cons: 0x%x SQ Prod: 0x%x, usecs since last trans: %u\n",
               i, sq->sqn, sq->cq.mcq.cqn, sq->cc, sq->pc,
               jiffies_to_usecs(jiffies - dev_queue->trans_start));
               //如果丢失的中断恢复了,TX timeout的事件会被解决,可以跳过重置通道缓解               
        if (!mlx5e_tx_timeout_eq_recover(dev, sq)) {
            clear_bit(MLX5E_SQ_STATE_ENABLED, &sq->state);
            reopen_channels = true;
        }
    }
    if (!reopen_channels)
        goto unlock;

    mlx5e_close_locked(dev);
    err = mlx5e_open_locked(dev);
    if (err)
        netdev_err(priv->netdev,
               "mlx5e_open_locked failed recovering from a tx_timeout, err(%d).\n",
               err);
unlock:
    mutex_unlock(&priv->state_lock);
    rtnl_unlock();
}

mlx5e_tx_timeout_eq_recover

mlx5e_tx_timeout_eq_recover通过调用mlx5_eq_poll_irq_disabled进行恢复操作,恢复成功后打印"Recover xxxxxxxxxx"关键字。

static bool mlx5e_tx_timeout_eq_recover(struct net_device *dev,
                    struct mlx5e_txqsq *sq)
{
    //从 sq(发送队列上下文)中获取 cq(完成队列)中的 eq(事件队列)指针。
    struct mlx5_eq_comp *eq = sq->cq.mcq.eq;
    u32 eqe_count;
    //使用 netdev_err 函数输出当前 Event Queue 的状态信息,包括 Event Queue 编号、消费者索引和中断号。
    netdev_err(dev, "EQ 0x%x: Cons = 0x%x, irqn = 0x%x\n",
           eq->core.eqn, eq->core.cons_index, eq->core.irqn);
    //调用 mlx5_eq_poll_irq_disabled 函数对 Event Queue 进行轮询,检查是否有待处理的事件。这个函数在禁用中断的情况下执行轮询操作,并返回事件的数量。
    eqe_count = mlx5_eq_poll_irq_disabled(eq);
    //如果 eqe_count 为 0,表示没有检测到事件,函数返回 false,表明无需进行恢复操作。
    if (!eqe_count)
        return false;
    //如果有事件被检测到,记录恢复的事件数量,并在日志中输出恢复的事件数量信息。
    netdev_err(dev, "Recover %d eqes on EQ 0x%x\n", eqe_count, eq->core.eqn);
    //更新 sq 所在通道的统计信息中的 eq_rearm 字段,表示已对 Event Queue 进行重新装填(rearm)
    sq->channel->stats->eq_rearm++;
    return true;
}

mlx5_eq_poll_irq_disabled

具体执行recovery的函数,调用mlx5_eq_comp_int函数恢复中断处理。

u32 mlx5_eq_poll_irq_disabled(struct mlx5_eq_comp *eq)
{
    u32 count_eqe;
    //通过 eq->core.irqn 禁用与该事件队列相关联的中断
    disable_irq(eq->core.irqn);
    //将当前事件队列的消费者索引 eq->core.cons_index 赋值给 count_eqe,用于后续计算事件数量的基准
    count_eqe = eq->core.cons_index;
    //调用 mlx5_eq_comp_int 函数来触发处理与该事件队列相关的中断。该函数通常用于在禁用中断期间仍能处理事件。
    mlx5_eq_comp_int(&eq->irq_nb, 0, NULL);
    //计算禁用中断期间处理的事件数量,通过当前的消费者索引与初始索引 (count_eqe 的原始值) 的差值得出。
    count_eqe = eq->core.cons_index - count_eqe;
    //,重新启用与该事件队列相关联的中断,以继续正常的中断处理流程
    enable_irq(eq->core.irqn);
    //函数返回禁用中断期间处理的事件数量
    return count_eqe;
}

主机对应内核报错日志

从LInux的内核日志来看,从发现watchdog超时到完成修复,内核日志信息如下:

Jul 14 23:59:14 linux-kernel kernel: ------------[ cut here ]------------
Jul 14 23:59:14 linux-kernel kernel: WARNING: CPU: 20 PID: 0 at net/sched/sch_generic.c:356 dev_watchdog+0x248/0x260
Jul 14 23:59:14 linux-kernel kernel: NETDEV WATCHDOG: eth0 (mlx5_core): transmit queue 4 timed out
Jul 14 23:59:14 linux-kernel kernel: CPU: 20 PID: 0 Comm: swapper/20 Kdump: loaded Tainted: 5.10.0-x86_64 #1
Jul 14 23:59:14 linux-kernel kernel: Call Trace:
Jul 14 23:59:14 linux-kernel kernel:  <IRQ>  [<ffffffff975865d1>] dump_stack+0x19/0x1b
Jul 14 23:59:14 linux-kernel kernel:  [<ffffffff96e9b488>] __warn+0xd8/0x100
Jul 14 23:59:14 linux-kernel kernel:  [<ffffffff96e9b50f>] warn_slowpath_fmt+0x5f/0x80
Jul 14 23:59:14 linux-kernel kernel:  [<ffffffff97486f68>] dev_watchdog+0x248/0x260
Jul 14 23:59:14 linux-kernel kernel:  [<ffffffff97486d20>] ? dev_deactivate_queue.constprop.27+0x60/0x60
Jul 14 23:59:14 linux-kernel kernel:  [<ffffffff96eabfc8>] call_timer_fn+0x38/0x110
Jul 14 23:59:14 linux-kernel kernel:  [<ffffffff97486d20>] ? dev_deactivate_queue.constprop.27+0x60/0x60
Jul 14 23:59:14 linux-kernel kernel:  [<ffffffff96eae5dd>] run_timer_softirq+0x25d/0x340
Jul 14 23:59:14 linux-kernel kernel:  [<ffffffff96ea4e05>] __do_softirq+0xf5/0x280
Jul 14 23:59:14 linux-kernel kernel:  [<ffffffff9759d4ec>] call_softirq+0x1c/0x30
Jul 14 23:59:14 linux-kernel kernel:  [<ffffffff96e2f715>] do_softirq+0x65/0xa0
Jul 14 23:59:14 linux-kernel kernel:  [<ffffffff96ea5185>] irq_exit+0x105/0x110
Jul 14 23:59:14 linux-kernel kernel:  [<ffffffff9759ea28>] smp_apic_timer_interrupt+0x48/0x60
Jul 14 23:59:14 linux-kernel kernel:  [<ffffffff9759afba>] apic_timer_interrupt+0x16a/0x170
Jul 14 23:59:14 linux-kernel kernel:  <EOI>  [<ffffffff9758e2a0>] ? __cpuidle_text_start+0x8/0x8
Jul 14 23:59:14 linux-kernel kernel:  [<ffffffff9758e4eb>] ? native_safe_halt+0xb/0x20
Jul 14 23:59:14 linux-kernel kernel:  [<ffffffff9758e2be>] default_idle+0x1e/0xc0
Jul 14 23:59:14 linux-kernel kernel:  [<ffffffff96e37ca0>] arch_cpu_idle+0x20/0xc0
Jul 14 23:59:14 linux-kernel kernel:  [<ffffffff96f019ea>] cpu_startup_entry+0x14a/0x1e0
Jul 14 23:59:14 linux-kernel kernel:  [<ffffffff96e5a8d7>] start_secondary+0x1f7/0x270
Jul 14 23:59:14 linux-kernel kernel:  [<ffffffff96e000d5>] start_cpu+0x5/0x14
Jul 14 23:59:14 linux-kernel kernel: ---[ end trace  ]---
Jul 14 23:59:14 linux-kernel kernel: mlx5_core 0000:10:00.7 eth0: TX timeout detected
Jul 14 23:59:14 linux-kernel kernel: mlx5_core 0000:10:00.7 eth0: TX timeout on queue: 4, SQ: 0x2d1, CQ: 0x102a, SQ Cons: 0x5122 SQ Prod: 0x51ba, usecs since last trans: 1
Jul 14 23:59:14 linux-kernel kernel: mlx5_core 0000:10:00.7 eth0: EQ 0xf: Cons = 0x20f17888, irqn = 0x352
Jul 14 23:59:14 linux-kernel kernel: mlx5_core 0000:10:00.7 eth0: Recover 1 eqes on EQ 0xf

上一篇:代码随想录训练营第三十五天 416分割等和子集


下一篇:object-C 解答算法:合并两个有序数组(leetCode-88)-解题方法一: