本文主要讲解linux 时间管理系统中的一个问题

背景:linux 时间管理,包含clocksource,clockevent,timer,tick,timekeeper等等概念 ,

这些概念有机地组成了完整的时间代码体系。当然,是代码就会有bug,本文通过一个bug入手,

在实战中加深对理论的认识。

获取时间,但是crash了。

一、故障现象

oppo云内核团队接到连通性告警报障,发现机器复位:

PID: 0      TASK: ffff8d2b3775b0c0  CPU: 1   COMMAND: "swapper/1"
#0 [ffff8d597f6489f0] machine_kexec at ffffffffa5a63b34
#1 [ffff8d597f648a50] __crash_kexec at ffffffffa5b1e242
#2 [ffff8d597f648b20] panic at ffffffffa615d85b
#3 [ffff8d597f648ba0] nmi_panic at ffffffffa5a9859f
#4 [ffff8d597f648bb0] watchdog_overflow_callback at ffffffffa5b4a881
#5 [ffff8d597f648bc8] __perf_event_overflow at ffffffffa5ba26b7
#6 [ffff8d597f648c00] perf_event_overflow at ffffffffa5babd24
#7 [ffff8d597f648c10] intel_pmu_handle_irq at ffffffffa5a0a850
#8 [ffff8d597f648e38] perf_event_nmi_handler at ffffffffa616d031
#9 [ffff8d597f648e58] nmi_handle at ffffffffa616e91c
#10 [ffff8d597f648eb0] do_nmi at ffffffffa616ebf8
#11 [ffff8d597f648ef0] end_repeat_nmi at ffffffffa616dd89
[exception RIP: __getnstimeofday64+144]
RIP: ffffffffa5b03940 RSP: ffff8d597f643c78 RFLAGS: 00000212
RAX: 15b5c8320b8602cd RBX: ffff8d597f643cb0 RCX: 000000005f89ee29
RDX: 00000000ee4479fe RSI: 0000012b5478f3b2 RDI: 0009709c7629b240
RBP: ffff8d597f643c90 R8: 00000000007001de R9: ffff8d596d5c0000
R10: 000000000000007a R11: 000000000000000e R12: ffffffffa662ea80
R13: 000000003ccbcfb6 R14: ffff8d895de08000 R15: 0000000000000081
ORIG_RAX: ffffffffffffffff CS: 0010 SS: 0018
--- <NMI exception stack> ---
#12 [ffff8d597f643c78] __getnstimeofday64 at ffffffffa5b03940
#13 [ffff8d597f643c98] getnstimeofday64 at ffffffffa5b0398e
#14 [ffff8d597f643ca8] ktime_get_real at ffffffffa5b03a45
#15 [ffff8d597f643cd0] netif_receive_skb_internal at ffffffffa603b936
#16 [ffff8d597f643d00] napi_gro_receive at ffffffffa603c588
#17 [ffff8d597f643d28] mlx5e_handle_rx_cqe_mpwrq at ffffffffc052ef1d [mlx5_core]
#18 [ffff8d597f643db8] mlx5e_poll_rx_cq at ffffffffc052f4b8 [mlx5_core]
#19 [ffff8d597f643e08] mlx5e_napi_poll at ffffffffc05304c6 [mlx5_core]
#20 [ffff8d597f643e78] net_rx_action at ffffffffa603bf1f
#21 [ffff8d597f643ef8] __do_softirq at ffffffffa5aa2155
#22 [ffff8d597f643f68] call_softirq at ffffffffa617a32c
#23 [ffff8d597f643f80] do_softirq at ffffffffa5a2e675

从堆栈看,我们的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

的顺序锁,代码分析如下:

int __getnstimeofday64(struct timespec64 *ts)
{
struct timekeeper *tk = &timekeeper;
unsigned long seq;
s64 nsecs = 0; do {
seq = read_seqcount_begin(&timekeeper_seq); ts->tv_sec = tk->xtime_sec;//caq:秒值赋值
nsecs = timekeeping_get_ns(&tk->tkr_mono); } while (read_seqcount_retry(&timekeeper_seq, seq)); ts->tv_nsec = 0;
timespec64_add_ns(ts, nsecs);//caq:这里面还有个循环呢, /*
* Do not bail out early, in case there were callers still using
* the value, even in the face of the WARN_ON.
*/
if (unlikely(timekeeping_suspended))
return -EAGAIN;
return 0;
}

但是从汇编展开来看:


0xffffffffa5b0393b <__getnstimeofday64+139>: xor %edx,%edx----清零 u32 ret = 0;
0xffffffffa5b0393d <__getnstimeofday64+141>: nopl (%rax)
0xffffffffa5b03940 <__getnstimeofday64+144>: sub $0x3b9aca00,%rax---------------------1s就是1000000000 ns,循坏在这,而栈中的rax为 15b5c8320b8602cd
0xffffffffa5b03946 <__getnstimeofday64+150>: add $0x1,%edx---------ret++;edx is the lower 32 bit of rdx,rdx为00000000ee4479fe,所以edx为 0xee4479fe,也就是3997465086
0xffffffffa5b03949 <__getnstimeofday64+153>: cmp $0x3b9ac9ff,%rax-------------------------------------剩余是否小于1ns
0xffffffffa5b0394f <__getnstimeofday64+159>: ja 0xffffffffa5b03940 <__getnstimeofday64+144>
/include/linux/time.h: 215---对应 timespec_add_ns
0xffffffffa5b03951 <__getnstimeofday64+161>: add %rcx,%rdx---delta算出的秒值+之前保存的秒值,就是最新的秒值
0xffffffffa5b03954 <__getnstimeofday64+164>: mov %rax,0x8(%rbx)----剩余的ns,赋值给a->tv_nsec = ns;
0xffffffffa5b03958 <__getnstimeofday64+168>: mov %rdx,(%rbx)---加完delta秒值的最新的秒值,赋值给a->tv_sec
0xffffffffa5b0395b <__getnstimeofday64+171>: cmpl $0x1,0xc55702(%rip) # 0xffffffffa6759064----if(timekeeping_suspended)
/kernel/time/timekeeping.c: 512
0xffffffffa5b03962 <__getnstimeofday64+178>: pop %rbx
0xffffffffa5b03963 <__getnstimeofday64+179>: pop %r12
0xffffffffa5b03965 <__getnstimeofday64+181>: pop %r13

从堆栈看出,我们循环在__getnstimeofday64+144

0xffffffffa5b03940 <__getnstimeofday64+144>:    sub    $0x3b9aca00,%rax---------------------1s就是1000000000 ns,循坏在这,而栈中的rax为 15b5c8320b8602cd

原来我们循环在timespec64_add_ns 函数里面:

static __always_inline void timespec64_add_ns(struct timespec64 *a, u64 ns)
{
a->tv_sec += __iter_div_u64_rem(a->tv_nsec + ns, NSEC_PER_SEC, &ns);
a->tv_nsec = ns;
}
__iter_div_u64_rem展开如下: static __always_inline u32
__iter_div_u64_rem(u64 dividend, u32 divisor, u64 *remainder)
{
u32 ret = 0; while (dividend >= divisor) {//这个循环
/* The following asm() prevents the compiler from
optimising this loop into a modulo operation. */
asm("" : "+rm"(dividend)); dividend -= divisor;
ret++;
} *remainder = dividend; return ret;
}

我们的入参divisor是 NSEC_PER_SEC,也就是10的9次方,16进制为0x3b9aca00,

既然在循环,那么我们的dividend是rax,请注意看值为:

 RAX: 15b5c8320b8602cd

crash> p 0x15b5c8320b8602cd/0x3b9aca00
$7 = 1564376562

按照这样计算,要计算完毕,还得循环 1564376562 这么多次。

这么大的一个值,确实不知道循环到猴年马月去。

那么这个值怎么来的呢?原来这个值是前后两次读取closk_source的cycle差值计算出来的。

static inline s64 timekeeping_get_ns(struct tk_read_base *tkr)
{
u64 delta; delta = timekeeping_get_delta(tkr);//caq:上次读取与本次读取之间的差值
return timekeeping_delta_to_ns(tkr, delta);//caq:差值转换为ns
} delta的来源是:
static inline u64 timekeeping_get_delta(struct tk_read_base *tkr)
{
u64 cycle_now, delta;
struct clocksource *clock; /* read clocksource: */
clock = tkr->clock;
cycle_now = tkr->clock->read(clock);//当前值是通过读取来的 /* calculate the delta since the last update_wall_time */
delta = clocksource_delta(cycle_now, clock->cycle_last, clock->mask);//计算差值 return delta;
}

原来,delta的获取是线读取当前clocksource的cycle值,然后通过clocksource_delta

计算对应的差值,

根据以上代码,首先我们得知道当前的clocksource是哪个:

crash> timekeeper
timekeeper = $1 = {
tkr_mono = {------------------------------timekeeping_get_ns(&tk->tkr_mono)
clock = 0xffffffffa662ea80, ------------这个就是 clocksource,这个值当前就是 clocksource_tsc
cycle_last = 16728674596502256,
mult = 7340510,
shift = 24,
xtime_nsec = 2657092090049088, 这个值并不是ns,而是要 >>tkr->shift才是ns
base = {
tv64 = 2788453640047242
}
},
tkr_raw = {
clock = 0xffffffffa662ea80,
cycle_last = 16728674596502256,
mult = 8007931,
shift = 24,
xtime_nsec = 0,
base = {
tv64 = 2788490058099290
}
},
xtime_sec = 1602874921, ------------------当前的秒数

timekeeper是选择当前精度最高的clocksource来工作的:

crash> dis -l 0xffffffffa662ea80
0xffffffffa662ea80 <clocksource_tsc>: addb $0xa5,-0x5d(%rcx)--------------就是 clocksource_tsc ,tsc就是一个clock_source crash> clocksource_tsc
clocksource_tsc = $2 = {
read = 0xffffffffa5a34180, -----------read_tsc
cycle_last = 16728674596502256, ------上次更新墙上时间的时刻取的cycle值
mask = 18446744073709551615,
mult = 8007931,
shift = 24, ----------------------注意位数
max_idle_ns = 204347035648,
maxadj = 880872,
archdata = {
vclock_mode = 1
},
name = 0xffffffffa647c1cd "tsc", ---名称
list = {
next = 0xffffffffa6633ff8,
prev = 0xffffffffa665c9b0
},
rating = 300, --------------优先级,
enable = 0x0,
disable = 0x0,
flags = 35, ---没有CLOCK_SOURCE_UNSTABLE标志
suspend = 0x0,
resume = 0x0,
owner = 0x0
}

差值的计算分析如下:

static inline s64 timekeeping_delta_to_ns(struct tk_read_base *tkr,
u64 delta)
{
s64 nsec;//注意,这里是带符号数 nsec = delta * tkr->mult + tkr->xtime_nsec;
nsec >>= tkr->shift;//换算成ns /* If arch requires, add in get_arch_timeoffset() */
return nsec + arch_gettimeoffset();
}

timekeeping_delta_to_ns返回值过大,

就是delta的偏大,delta * tkr->mult 对s64的值产生较大值导致转s64时变为负数,

这个算是个bug。

三、故障复现

这个s64溢出的bug,在社区已经修复了

-static inline s64 timekeeping_delta_to_ns(struct tk_read_base *tkr,
+static inline u64 timekeeping_delta_to_ns(struct tk_read_base *tkr,
cycle_t delta)
{
- s64 nsec;
+ u64 nsec;

而且查看红帽的changelog,也按照上游这样修复的,具体升级到

kernel-3.10.0-1160.15.2.el7 及以上。

但同时,作者也说明了哪怕从s64改到u64,并没有根本上解决溢出问题,

因为 timekeeping_delta_to_ns

函数中明显可以看到,就算patch之后,u64的64位并没有全部用到ns的差值上。

比如当 update_wall_time 有时候更新就不是那么及时时,还是会导致u64产生溢出。

四、故障规避或解决

可能的解决方案是:

1、增加告警,对于softlock的要及时介入,有可能导致 update_wall_time 更新不及时。

2、访问HPET的时间开销为访问TSC时间开销的好几倍,如果内核日志有Clocksource tsc unstable

之类的,会带来潜在的开销,需要增加监控告警或者指定cmdline选项 tsc=reliable。

我就获取个时间,机器就down了的更多相关文章

  1. Windows下获取高精度时间注意事项

    Windows下获取高精度时间注意事项 [转贴 AdamWu]   花了很长时间才得到的经验,与大家分享. 1. RDTSC - 粒度: 纳秒级 不推荐优势: 几乎是能够获得最细粒度的计数器抛弃理由: ...

  2. VC中如何获取当前时间(精度达到毫秒级)

    标 题: VC中如何获取当前时间(精度达到毫秒级)作 者: 0xFFFFCCCC时 间: 2013-06-24链 接: http://www.cnblogs.com/Y4ng/p/Millisecon ...

  3. iOS获取网络时间与转换格式

      [NSDate date]可以获取系统时间,但是会造成一个问题,用户可以自己修改手机系统时间,所以有时候需要用网络时间而不用系统时间.获取网络标准时间的方法: 1.先在需要的地方实现下面的代码,创 ...

  4. iOS中获取当前时间,设定时间,并算出差值

    NSDate *date = [NSDate date];//获取当前时间 NSTimeZone *zone = [NSTimeZone systemTimeZone];//修改时区 NSIntege ...

  5. Js获取当前日期时间及其它操作

    Js获取当前日期时间及其它操作var myDate = new Date();myDate.getYear(); //获取当前年份(2位)myDate.getFullYear(); //获取完整的年份 ...

  6. 【转】Js获取当前日期时间及格式化操作

    (转自:http://www.cnblogs.com/qinpengming/archive/2012/12/03/2800002.html) var myDate = new Date(); myD ...

  7. PHP 获取中国时间,即上海时区时间

    /** * 获取中国时间,即上海时区时间 * @param <type> $format * @return <type> */ function getChinaTime($ ...

  8. C#获取北京时间与设置系统时间

    获取北京时间 public static DateTime GetBeijingTime() { DateTime dt; // 返回国际标准时间 // 只使用 timeServers 的 IP 地址 ...

  9. jquery 获取日期时间

    获取JavaScript 的时间使用内置的Date函数完成 var mydate = new Date();mydate.getYear(); //获取当前年份(2位)mydate.getFullYe ...

随机推荐

  1. 【生成对抗网络学习 其一】经典GAN与其存在的问题和相关改进

    参考资料: 1.https://github.com/dragen1860/TensorFlow-2.x-Tutorials 2.<Generative Adversarial Net> ...

  2. SAP Using Text Modules in Adobe Forms

    In this demo we will create an adobe form which displays text in two different languages (English or ...

  3. Kubebuilder简介与架构

    什么是Kubebuilder Kubebuilder是一个用Go原因构建Kubernetes APIs的框架,通过使用KubeBuilder,用户可以遵循一套简单的编程框架,使用CRD构建API.Co ...

  4. NC21181 重返小学

    NC21181 重返小学 题目 题目描述 ​ 时光依旧,岁月匆匆.转眼间,曾经的少年郭嘉烜已经长大成人,考上了一所优秀的大学--兰州大学.在经历了一年来自牛顿.莱布尼茨.拉普拉斯的精神洗礼后,他终于决 ...

  5. SHT11和SHT21传感器

    1.传感器概述 SHT11和SHT21为瑞士Sensirion公司生产,精度和测量范围较广,但价格较高.SHT11和SHT21是具有IIC总线接口的单片全校准数字式相对湿度和温度传感器.该传感器采用独 ...

  6. 【cartographer_ros】五: 发布和订阅陀螺仪Imu信息

    上一节介绍了里程计Odometry传感数据的订阅和发布. 本节会介绍陀螺仪Imu数据的发布和订阅.陀螺仪在cartographer中主要用于前端位置预估和后端优化. 目录 1:sensor_msgs/ ...

  7. supervisor 按天备份日志

    参考使用 logrotate 配置 supervisor 进行日志管理按天备份 supervisor默认的日志备份策略是按大小备份,经常不知道去哪个文件查日志的烦恼.本文通过设置 logrotate ...

  8. 总结vue 需要掌握的知识点

    使用的开发工具是webstorm,它是默认就安装好了vuejs插件,idea要使用的话,需要安装一下该插件 一.快速搭建项目vue-cli 脚手架(Vue2.0) 1.Vue CLI使用前提 –Nod ...

  9. 2020.7.19 区间 dp 阶段测试

    打崩了-- 事先说明,今天没有很在状态,所以题解就直接写在代码注释里的,非常抱歉 T1 颜色联通块 此题有争议,建议跳过 题目描述 N 个方块排成一排,第 i 个颜色为 Ci .定义一个颜色联通块 [ ...

  10. 教你使用CANN将照片一键转换成卡通风格

    摘要:这次是将AnimeGAN部署到Ascend 310,从而实现对自己想要图片的一键转换为我们想看到的卡通风格. 本文分享自华为云社区<[CANN训练营]CANN训练营_昇腾AI趣味应用实现A ...