网络设备发送超时定时器

在注册网络设备函数register_netdevice中将调用Qdisc初始化函数dev_init_scheduler,这里将创建设备的watchdog定时器,超时处理函数设置为dev_watchdog,用来监控网络设备的发送队列传输超时。

void dev_init_scheduler(struct net_device *dev)
{
    dev->qdisc = &noop_qdisc;
    netdev_for_each_tx_queue(dev, dev_init_scheduler_queue, &noop_qdisc);
    if (dev_ingress_queue(dev))
        dev_init_scheduler_queue(dev, dev_ingress_queue(dev), &noop_qdisc);

    timer_setup(&dev->watchdog_timer, dev_watchdog, 0);
}

watchdog的开启

内核函数__netdev_watchdog_up负责开启watchdog定时器,如下所示,如果超时时长小于等于0,内核默认成5秒钟。网卡驱动程序可修改此时长,例如Mellanox的mlx5的驱动将此值设置为15秒;Intel的网卡驱动基本都是使用5秒的默认值。

另外,如果设备驱动中没有实现超时处理函数ndo_tx_timeout,这里并不启动watchdog定时器。驱动中为实现超时处理,可能是驱动并不能由导致发送超时的错误中进行恢复。

void __netdev_watchdog_up(struct net_device *dev)
{
    if (dev->netdev_ops->ndo_tx_timeout) {
        if (dev->watchdog_timeo <= 0)
            dev->watchdog_timeo = 5*HZ;
        if (!mod_timer(&dev->watchdog_timer, round_jiffies(jiffies + dev->watchdog_timeo)))
            dev_hold(dev);

在检测到设备的物理链路UP之后,将开启设备的watchdog定时器。

void netif_carrier_on(struct net_device *dev)
{
    if (test_and_clear_bit(__LINK_STATE_NOCARRIER, &dev->state)) {
        if (dev->reg_state == NETREG_UNINITIALIZED)
            return;
        atomic_inc(&dev->carrier_up_count);
        linkwatch_fire_event(dev);
        if (netif_running(dev))
            __netdev_watchdog_up(dev);

或者,在检测到网络设备由节能状态恢复回来,重新接入系统时,开启watchdog定时器,参见以下函数。

void netif_device_attach(struct net_device *dev)
{
    if (!test_and_set_bit(__LINK_STATE_PRESENT, &dev->state) && netif_running(dev)) {
        netif_tx_wake_all_queues(dev);
        __netdev_watchdog_up(dev);

对于Intel的i40e网卡驱动程序,其实现了ndo_tx_timeout函数,即i40e_tx_timeout,并且,其显式指定了网络设备的watchdog定时器超时时间为5秒(这里与默认的相同)。

static const struct net_device_ops i40e_netdev_ops = {
    ...
    .ndo_tx_timeout     = i40e_tx_timeout,
	
static int i40e_config_netdev(struct i40e_vsi *vsi)
{	
    ...
    netdev->watchdog_timeo = 5 * HZ;

watchdog的关闭

如下,在函数dev_watchdog_down中,内核删除网络设备的watchdog定时器。

static void dev_watchdog_down(struct net_device *dev)
{
    netif_tx_lock_bh(dev);
    if (del_timer(&dev->watchdog_timer))
        dev_put(dev);
    netif_tx_unlock_bh(dev);
}

以上函数的调用位于函数dev_deactivate_many中,其的调用有两处,一是__dev_close_many函数,即用户shutdown网络设备时,停止watchdog计时器。另一处是封装函数dev_deactivate。

void dev_deactivate_many(struct list_head *head)
{   
    struct net_device *dev;
    
    list_for_each_entry(dev, head, close_list) {
        ...
        dev_watchdog_down(dev);

函数dev_deactivate在设备链路状态发生变化时,参见netif_carrier_on和netif_carrier_off函数,linkwatch功能将在link事件处理函数linkwatch_do_dev中根据链路状态调用设备的活动和非活动函数。如果链路down,调用dev_deactivate,其中会删除设备watchdog定时器;反之,链路UP,将调用dev_activate函数,其中将开启watchdog计时器。

static void linkwatch_do_dev(struct net_device *dev)
{    
    if (dev->flags & IFF_UP && netif_device_present(dev)) {
        if (netif_carrier_ok(dev))
            dev_activate(dev);
        else
            dev_deactivate(dev);

另外,以上节介绍的netif_carrier_on函数功能不同,在函数netif_carrier_off中并没有显示的关闭watchdog定时器,而是调用了linkwatch功能的函数linkwatch_fire_event,添加链路事件,最终也是由linkwatch_do_dev在事件处理时,关闭watchdog定时器。

void netif_carrier_off(struct net_device *dev)
{
    if (!test_and_set_bit(__LINK_STATE_NOCARRIER, &dev->state)) {
        if (dev->reg_state == NETREG_UNINITIALIZED)
            return;
        atomic_inc(&dev->carrier_down_count);
        linkwatch_fire_event(dev);

watchdog超时处理

首先看一下队列发送事件的计算,由函数txq_trans_update完成,记录在发送队列结构的成员trans_start中。

static inline void txq_trans_update(struct netdev_queue *txq)
{
    if (txq->xmit_lock_owner != -1)
        txq->trans_start = jiffies;
}

在内核的设备核心发送函数netdev_start_xmit中,发送成功之后,更新发送时间。

static inline netdev_tx_t netdev_start_xmit(struct sk_buff *skb, struct net_device *dev,
                        struct netdev_queue *txq, bool more)
{
    rc = __netdev_start_xmit(ops, skb, dev, more);
    if (rc == NETDEV_TX_OK)
        txq_trans_update(txq);

超时处理函数dev_watchdog如下所示,如果设备在所有发送队列上都使用Qdisc的noop类型,不进行处理。否则,变量每个队列,检查发送停止的队列,通过比较最近一次的发送时间和当前时间的差值,如果停止时间超过设置的超时时间watchdog_timeo,即认为此队列发送超时。

函数dev_watchdog将打印部分出错的队列信息,并且调用设备驱动的超时处理函数ndo_tx_timeout。

static void dev_watchdog(struct timer_list *t)
{
    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)) {

            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;
                }
            }

            if (some_queue_timedout) {
                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);
            }
            if (!mod_timer(&dev->watchdog_timer, round_jiffies(jiffies + dev->watchdog_timeo)))
                dev_hold(dev);

驱动超时处理

此处以Intel的网卡驱动i40e为例,以下为其超时处理函数i40e_tx_timeout,可见,其第一部分的处理与上一节函数dev_watchdog中的基本相同,这里找出第一个超时的队列。

static void i40e_tx_timeout(struct net_device *netdev)
{
    pf->tx_timeout_count++;

    /* find the stopped queue the same way the stack does */
    for (i = 0; i < netdev->num_tx_queues; i++) {
        struct netdev_queue *q;
        unsigned long trans_start;

        q = netdev_get_tx_queue(netdev, i);
        trans_start = q->trans_start;
        if (netif_xmit_stopped(q) &&
            time_after(jiffies, (trans_start + netdev->watchdog_timeo))) {
            hung_queue = i;
            break;
        }
    }

变量tx_timeout_last_recovery控制超时处理的时间间隔,不能小于watchdog_timeo的值,即处理完成一个队列之后才能处理下一个队列的超时。另外,函数中定义了恢复等级tx_timeout_recovery_level,超出20秒钟,由等级1开始。

    if (time_after(jiffies, (pf->tx_timeout_last_recovery + HZ*20)))
        pf->tx_timeout_recovery_level = 1;  /* reset after some time */
    else if (time_before(jiffies,
              (pf->tx_timeout_last_recovery + netdev->watchdog_timeo)))
        return;   /* don't do any new action before the next timeout */

    /* don't kick off another recovery if one is already pending */
    if (test_and_set_bit(__I40E_TIMEOUT_RECOVERY_PENDING, pf->state))
        return;

    pf->tx_timeout_last_recovery = jiffies;

以下,每次超时处理,将恢复等级加一,等级越高说明问题越严重,需要执行的恢复操作分别为PF、CORE和GLOBAL三个级别。

    netdev_info(netdev, "tx_timeout recovery level %d, hung_queue %d\n",
            pf->tx_timeout_recovery_level, hung_queue);

    switch (pf->tx_timeout_recovery_level) {
    case 1:
        set_bit(__I40E_PF_RESET_REQUESTED, pf->state);
        break;
    case 2:
        set_bit(__I40E_CORE_RESET_REQUESTED, pf->state);
        break;
    case 3:
        set_bit(__I40E_GLOBAL_RESET_REQUESTED, pf->state);
        break;
    default:
        netdev_err(netdev, "tx_timeout recovery unsuccessful\n");
        break;
    }

    i40e_service_event_schedule(pf);
    pf->tx_timeout_recovery_level++; 

内核版本 5.0

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页