- 背景
linux 时间管理,包含clocksource,clockevent,timer,tick,timekeeper等等概念 ,这些概念有机地组成了完整的时间代码体系。当然,是代码就会有bug,本文通过一个bug入手,在实战中加深对理论的认识。获取时间,但是crash了。
- 故障现象
OPPO云内核团队接到连通性告警报障,发现机器复位:
1 | yaml复制代码PID: 0 TASK: ffff8d2b3775b0c0 CPU: 1 COMMAND: "swapper/1" |
从堆栈看,我们的0号进程在处理软中断收包的过程中,因为获取个时间,导致了crash。hardlock的分析之前已经给出了很多了,无非是关中断时间长了,具体关中断的地方,可以看call_softirq函数即可。
- 故障现象分析
1)理论知识
在处理网络包的软中断过程中,会打时间戳,也就是说,对于oppo云的机器来说,以上的调用栈路径是一个热点且成熟的路径。成熟的路径出问题比较少见,所以有必要分享一下。在timekeeping初始化的时候,很难选择一个最好的clock source,因为很有可能最好的那个还没有初始化呢。因此,策略就是采用一个在timekeeping初始化时一定是ready的clock source,比如基于jiffies 的那个clocksource。
一般而言,timekeeping模块是在tick到来的时候更新各种系统时钟的时间值,ktime_get调用很有可能发生在两次tick之间,这时候,仅仅依靠当前系统时钟的值精度就不够了,毕竟那个时间值是per tick更新的。因此,为了获得高精度,ns值的获取是通过timekeeping_get_ns完成的,timekeeping_get_ns就是本文的主角,该函数获取了real time clock的当前时刻的纳秒值,而这是通过上一次的tick时候的real time clock的时间值(xtime_nsec)加上当前时刻到上一次tick之间的delta时间值计算得到的。系统运行之后,real time clock+ wall_to_monotonic是系统的uptime,而
real time clock+ wall_to_monotonic + sleep time也就是系统的boot time。
2)实战分析
根据调用堆栈,简单地看,__getnstimeofday64只有一个循环,那就是读取timekeeper_seq的顺序锁,代码分析如下:
1 | ini复制代码int __getnstimeofday64(struct timespec64 *ts) |
但是从汇编展开来看:
1 | perl复制代码0xffffffffa5b0393b <__getnstimeofday64+139>: xor %edx,%edx----清零 u32 ret = 0; |
从堆栈看出,我们循环在__getnstimeofday64+144
1 | lua复制代码0xffffffffa5b03940 <__getnstimeofday64+144>: sub $0x3b9aca00,%rax---------------------1s就是1000000000 ns,循坏在这,而栈中的rax为 15b5c8320b8602cd |
原来我们循环在timespec64_add_ns 函数里面:
1 | rust复制代码static __always_inline void timespec64_add_ns(struct timespec64 *a, u64 ns) |
我们的入参divisor是 NSEC_PER_SEC,也就是10的9次方,16进制为0x3b9aca00,既然在循环,那么我们的dividend是rax,请注意看值为:
1 | ini复制代码RAX: 15b5c8320b8602cd |
按照这样计算,要计算完毕,还得循环 1564376562 这么多次。
这么大的一个值,确实不知道循环到猴年马月去。
那么这个值怎么来的呢?原来这个值是前后两次读取closk_source的cycle差值计算出来的。
1 | scss复制代码static inline s64 timekeeping_get_ns(struct tk_read_base *tkr) |
原来,delta的获取是线读取当前clocksource的cycle值,然后通过clocksource_delta 计算对应的差值,根据以上代码,首先我们得知道当前的clocksource是哪个:
1 | ini复制代码crash> timekeeper |
timekeeper是选择当前精度最高的clocksource来工作的:
1 | ini复制代码crash> dis -l 0xffffffffa662ea80 |
差值的计算分析如下:
1 | rust复制代码static inline s64 timekeeping_delta_to_ns(struct tk_read_base *tkr, |
timekeeping_delta_to_ns返回值过大,有两种可能:一种是delta的偏大,delta * tkr->mult 对s64的值产生溢出,这个算是个bug。还有一种可能是,直接前后读取的delta值太大,这涉及到 update_wall_time 并没有及时调用去读取当前clocksource的cycle。
- 故障复现
这个s64溢出的bug,在社区已经修复了。
1 | arduino复制代码-static inline s64 timekeeping_delta_to_ns(struct tk_read_base *tkr, |
而且查看红帽的changelog,也按照上游这样修复,但是我觉得风险还在的,因为 update_wall_time 有时候更新就不是那么及时,而哪怕从s64改到u64,并没有解决溢出问题,因为 timekeeping_delta_to_ns函数中明显可以看到,u64的64位并没有全部用到cycle的差值上。我相信社区最终应该会有人爆这个问题的。
- 故障规避或解决
可能的解决方案是:
增加告警,对于softlock的要及时介入,有可能导致update_wall_time 更新不及时。
作者简介
Anqing 高级后端工程师
目前主要负责linux内核及容器,虚拟机等虚拟化方面的工作。
获取更多精彩内容,扫码关注[OPPO数智技术]公众号
本文转载自: 掘金