sem_timedwait() 阻塞导致网口单通故障分析

作者: yestyle | 来源:发表于2017-01-07 17:21 被阅读1002次

这几天遇到一个网口单通故障,最后定位发现是 sem_timedwait() 阻塞导致,现记录下定位过程。

故障现象

在测试过程中发现,使用 SGMII 对接的两个设备,设备 D1 上的端口显示为 link down,而设备 D2 上的端口显示为 link up,出现了单通的现象。

故障分析

1、为什么会出现单通现象?

对于使用背板实现的 SGMII 接口,出现单通现象的概率极低,首先需要排查的是两边设备是否准确地获取到了端口的 link 状态。通过比较两边设备源码发现,设备 D2 获取端口状态时是直接读取交换芯片的寄存器,而设备 D1 获取端口状态时是读取保存在交换芯片 SDK 中的软件变量。各个端口的 link 状态是通过一个线程定时从硬件读取并保存在上述软件变量中的。

于是在设备 D1 上手动调用交换芯片 SDK 中直接读取端口底层 SerDes 状态的 API,结果确实是 link up 的。也就是说,在底层 SerDes 层面,两边的端口都是 link up 的。由此可见,是设备 D1 的软件层面保存的状态与硬件 SerDes 状态不同步,导致出现了单通的「假象」。

2、为什么硬件状态没有同步到软件变量中?

上述的端口 link 状态同步线程,会每隔 250 ms 将底层 SerDes 状态同步到软件变量中,应用系统在调用相应的 API 获取端口状态时,就是直接返回软件变量中保存的对应端口的 link 状态等信息。从第 1 步的分析可知,在出现故障时,这个线程并没有成功把底层 SerDes 状态同步到软件变量中。

查看 SDK 源码可知,link 状态同步线程在循环中通过调用 sal_sem_take() 获取一个信号量,并设置了 250ms 的超时时间,可能有两种结果:

  • 假如在这 250ms 之内,其它线程调用了修改端口速率、接口模式等 API 导致端口状态改变而进一步调用 sal_sem_give() 释放了信号量,则 link 状态同步线程立即获取到信号量并继续执行将状态有变化的端口的最新状态同步到软件变量中,以此识别本端的状态变化。
  • 假如在这 250ms 之内都没有线程调用 sal_sem_give() 做释放信号量操作,则 250ms 之后,sal_sem_take() 将以超时的形式返回,并主动获取底层 SerDes 状态并与保存的状态比较,有变化则相应地更新保存的状态,以此识别诸如对端设备断开导致的状态变化。

通过在故障设备上设置断点逐步缩小故障范围发现,link 状态同步线程正是阻塞在 sal_sem_take() 上而没有继续执行循环,导致没有把硬件状态同步到软件变量中的。

3、为什么 sal_sem_take() 操作会阻塞?

再深入到 sal_sem_take() 的内部,获取信号量并做超时等待的部分代码如下:

if (_sal_compute_timeout(&ts, usec)) {
    while (1) {
        if (!sem_timedwait(&s->s, &ts)) {
            err = 0;
            break;
        }
        if (errno != EAGAIN && errno != EINTR) {
            err = errno;
            break;
        }
    }
}

其中,sem_timedwait() 是 POSIX 标准接口,通过传入未来的某个时钟实现超时等待信号量的获取,具体请参考 sem_timedwait(3)。而 _sal_compute_timeout() 的实现如下:

static int _sal_compute_timeout(struct timespec *ts, int usec)
{
    int sec;
    uint32 nsecs;

    if (clock_gettime(CLOCK_REALTIME, ts) == 0) {
        ;
    }
    else
    {
        struct timeval  ltv;

        /* Fall back to RTC if realtime clock unavailable */
        gettimeofday(&ltv, 0);
        ts->tv_sec = ltv.tv_sec;
        ts->tv_nsec = ltv.tv_usec * 1000;
    }
    /* Add in the delay */
    ts->tv_sec += usec / SECOND_USEC;

    /* compute new nsecs */
    nsecs = ts->tv_nsec + (usec % SECOND_USEC) * 1000;

    /* detect and handle rollover */
    if (nsecs < ts->tv_nsec) {
        ts->tv_sec += 1;
        nsecs -= SECOND_NSEC;
    }
    ts->tv_nsec = nsecs;

    /* Normalize if needed */
    sec = ts->tv_nsec / SECOND_NSEC;
    if (sec) {
        ts->tv_sec += sec;
        ts->tv_nsec = ts->tv_nsec % SECOND_NSEC;
    }

    /* indicate that we successfully got the time */
    return 1;
}

可以看到,_sal_compute_timeout() 先调用了 clock_gettime(CLOCK_REALTIME, ts) 获取当前的实时时钟,再据此计算超时的时钟(本例中为 250ms 之后的时钟)。

分析 sal_sem_take() 的实现可知,有三种可能导致其阻塞的原因:

  • 调用 sem_timedwait() 时返回 EAGAINEINTR 导致 while 进入死循环。
  • 调用 _sal_compute_timeout() 计算的超时时钟不准,导致 sem_timedwait() 要等待的超时时钟向后大幅度偏移而阻塞。
  • 系统实时时钟被修改,导致 sem_timedwait() 用于计算是否达到超时时钟的基准时钟向前大幅度偏移而阻塞。

通过在 sal_sem_take() 中添加打印排除了前两种可能性,并结合「link 状态同步线程每次阻塞都是发生在上层子系统开始初始化时」这一现象,将怀疑点转换到上层子系统的代码中。

果然,通过搜索 clock_settime 发现,上层子系统为了对系统时钟做统一管理,在初始化时会运行如下代码初始化系统实时时钟:

struct timespec tTime;
tTime.tv_sec  = 0;
tTime.tv_nsec = 0;
clock_settime(CLOCK_REALTIME, &tTime);

也就是说,将系统实时时钟设置成全 0,即 1970 年 1 月 1 日 00:00:00.000。由于正常情况下 link 状态同步线程每 250ms 就会调用一次 _sal_compute_timeout()sem_timedwait(),假如上层子系统设置实时时钟的代码在调用这两个接口之间执行了,就会使得 sem_timedwait() 长时间阻塞,也就是上述第三种导致 sal_sem_take() 阻塞的原因。

举个具体的例子。假如 link 状态同步线程调用 _sal_compute_timeout()clock_gettime(CLOCK_REALTIME, ts) 的时间是 2017 年 1 月 7 日 15:44:45.000(时钟 1),则计算出来的 250ms 之后的时间是 2017 年 1 月 7 日 15:44:45.250(时钟 2),此后上层子系统调用了 clock_settime(CLOCK_REALTIME, &tTime) 将系统实时时钟设置成了 1970 年 1 月 1 日 00:00:00.000。当 link 状态同步线程继续执行到 sem_timedwait() 时,基于当前的系统实时时钟判断是否到达超时时间(时钟 2),所以需要等待 47 年零 6 天多!

这就是故障的根本原因。

解决方法

虽然「罪魁祸首」是上层子系统的代码,但他们的一整套管理机制肯定不会轻易修改,那只好修改我们自己的代码,具体来说就是修改 link 状态同步线程的超时等待实现机制。幸运的是,交换芯片的 SDK 对此已有支持,只需定义相关的宏即可打开另外两种实现分支。这两种实现分别简述如下:

1. 不使用 sem_timedwait(),而改用 sem_trywait()。

sem_trywait() 是和 sem_timedwait() 同一系列的 POSIX 标准接口。在实现时,可先调用 sem_trywait() 尝试获取信号量,假如获取不到,再使用 usleep() 等待 1us 后再次尝试获取,如此循环,直到超时的时间(本例中为 250ms)到达为止。参考代码如下:

int     time_wait = 1;
for (;;) {
    if (sem_trywait(&s->s) == 0) {
    err = 0;
    break;
    }

    if (errno != EAGAIN && errno != EINTR) {
    err = errno;
    break;
    }

    if (time_wait > usec) {
    time_wait = usec;
    }

    sal_usleep(time_wait);

    usec -= time_wait;

    if (usec == 0) {
    err = ETIMEDOUT;
    break;
    }

    if ((time_wait *= 2) > 100000) {
    time_wait = 100000;
    }
}

2. 不使用系统实时时钟,而改用单调递增时钟。

除了实时时钟之外,还有一个单调递增时钟,此时钟从某一时刻开始单调递增而不会被修改,详见 clock_gettime(3)。具体实现时,使用 clock_gettime(CLOCK_MONOTONIC, &ts) 获取单调递增时钟,再基于此时钟计算超时时间。此时同样不能使用 sem_timedwait(),因为这个接口就是使用系统实时时钟计算超时的,需要改用 pthread_cond_timedwait() 实现。参考代码如下:

if (err == 0) {
    err = pthread_mutex_lock(mutex);

    while ((*val == 0) && (err == 0)) {
        if (forever) {
            err = pthread_cond_wait(cond, mutex);
        } else {
            err = pthread_cond_timedwait(cond, mutex, &ts);
        }
        
    }

    if (err == 0) {
        *val -= 1;
    }

    /* even if there's an error, try to unlock this... */
    pthread_mutex_unlock(mutex);
}

总结

在会修改系统实时时钟的应用中,需要谨慎使用 sem_timedwait()。

以上。

相关文章

  • sem_timedwait() 阻塞导致网口单通故障分析

    这几天遇到一个网口单通故障,最后定位发现是 sem_timedwait() 阻塞导致,现记录下定位过程。 故障现象...

  • C/C++ 修改系统时间,sem_timedwait导致一直

    修改系统时间,导致sem_timedwait 一直阻塞的问题解决和分析 介绍 最近修复项目问题时,发现当系统时间往...

  • 主管手记(8)

    产品故障分析要点 1、确认故障现象 2、利用故障树穷举可能导致故障的原因 3、基于故障树确认排故方案 4、依据排故...

  • 3-1 ES集群介绍

    ES单节点服务存在问题? a.无法在出现故障时,自动完成故障转移b.当整个网站中诮求数过于多时,导致单节点处理诸求...

  • 本田8代雅阁节气门继电器故障

    故障现象发动机亮点 爆故障 换挡冲击大 图中的标志的继电器为节气门电源继电器 次继电器长通导致出故障

  • 百度 智能监控与自愈

    问题根因分类 改进为根据单源故障来分析 下面主要关注单机房故障自愈 要求具有故障容灾能力 机房故障,不记录了,看ppt吧

  • Oralce Rac数据库宕机问题

    1.故障现象和描述 1.1 相关信息 16号17时,由于专网1的Rac数据库宕机,导致程序故障,数据无法上传至专网...

  • Redis 如何高效安全删除大 Hash Key

    使用 SCAN 和 Pipeline 命令删除 Redis 的大 Key 删除操作会导致 Redis 线程阻塞,网...

  • Java基础面试高频问答题

    1、什么导致线程阻塞 一般线程中的阻塞: Socket客户端的阻塞: Socket服务器的阻塞: 什么导致线程阻塞...

  • 分析日志线程阻塞导致项目假死

    本例分析中所用的工具为jvisualvm(以下简称工具),在打开工具之前需要先启动fmm项目。本次排查问题的方式主...

网友评论

    本文标题:sem_timedwait() 阻塞导致网口单通故障分析

    本文链接:https://www.haomeiwen.com/subject/tzpfbttx.html