我就获取个时间,机器就down了
本文主要讲解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了的更多相关文章
- Windows下获取高精度时间注意事项
Windows下获取高精度时间注意事项 [转贴 AdamWu] 花了很长时间才得到的经验,与大家分享. 1. RDTSC - 粒度: 纳秒级 不推荐优势: 几乎是能够获得最细粒度的计数器抛弃理由: ...
- VC中如何获取当前时间(精度达到毫秒级)
标 题: VC中如何获取当前时间(精度达到毫秒级)作 者: 0xFFFFCCCC时 间: 2013-06-24链 接: http://www.cnblogs.com/Y4ng/p/Millisecon ...
- iOS获取网络时间与转换格式
[NSDate date]可以获取系统时间,但是会造成一个问题,用户可以自己修改手机系统时间,所以有时候需要用网络时间而不用系统时间.获取网络标准时间的方法: 1.先在需要的地方实现下面的代码,创 ...
- iOS中获取当前时间,设定时间,并算出差值
NSDate *date = [NSDate date];//获取当前时间 NSTimeZone *zone = [NSTimeZone systemTimeZone];//修改时区 NSIntege ...
- Js获取当前日期时间及其它操作
Js获取当前日期时间及其它操作var myDate = new Date();myDate.getYear(); //获取当前年份(2位)myDate.getFullYear(); //获取完整的年份 ...
- 【转】Js获取当前日期时间及格式化操作
(转自:http://www.cnblogs.com/qinpengming/archive/2012/12/03/2800002.html) var myDate = new Date(); myD ...
- PHP 获取中国时间,即上海时区时间
/** * 获取中国时间,即上海时区时间 * @param <type> $format * @return <type> */ function getChinaTime($ ...
- C#获取北京时间与设置系统时间
获取北京时间 public static DateTime GetBeijingTime() { DateTime dt; // 返回国际标准时间 // 只使用 timeServers 的 IP 地址 ...
- jquery 获取日期时间
获取JavaScript 的时间使用内置的Date函数完成 var mydate = new Date();mydate.getYear(); //获取当前年份(2位)mydate.getFullYe ...
随机推荐
- Python中plt.plot()、plt.scatter()和plt.legend函数的用法示例
参考:http://www.cppcns.com/jiaoben/python/471948.html https://blog.csdn.net/weixin_44825185/article/de ...
- @vue/cli3+配置build命令构建测试包&正式包
上一篇博客介绍了vue-cli2.x配置build命令构建测试包和正式包,但现在前端开发vue项目大多数使用新版@vue/cli脚手架搭建vue项目(vue create project-name) ...
- Jenkins之配置GitHub-Webhook
前提条件1: 运行Jenkins的机器需要安装git,并且git.exe文件在系统的环境变量里面,或者手动在 Manage Jenkins -> Global Tool Configuratio ...
- 解决maven依赖冲突,这篇就够了!
一.前言 什么是依赖冲突 依赖冲突是指项目依赖的某一个jar包,有多个不同的版本,因而造成了包版本冲突. 依赖冲突的原因 我们在maven项目的pom中 一般会引用许许多多的dependency.例如 ...
- BUUCTF-RAR
rar 看提示知道爆破压缩包的题,纯数字4位数拿出ARCHPR爆破即可.
- 使用AndroidKiller进行APK反编译
安装环境 JDK 最好用JDK8,问就是坑太多了 Android Studio 官网安装即可,安装教程如下 https://www.runoob.com/android/android-studio- ...
- python基础知识-day7(文件操作)
1.文件IO操作: 1)操作文件使用的函数是open() 2)操作文件的模式: a.r:读取文件 b.w:往文件里边写内容(先删除文件里边已有的内容) c.a:是追加(在文件基础上写入新的内容) d. ...
- UiPath文本操作Get OCR Text的介绍和使用
一.Get OCR Text操作的介绍 使用OCR屏幕抓取方法从指示的UI元素或图像中提取字符串及其信息.执行屏幕抓取操作时,还可以自动生成此活动以及容器.默认情况下,使用Google OCR引擎. ...
- [MRCTF2020]Ezpop-1|php序列化
1.打开题目获取到源代码信息,如下: Welcome to index.php <?php //flag is in flag.php //WTF IS THIS? //Learn From h ...
- MySql查看索引以及各字段含义
查看表的索引: show index from userInfo(表名) show index from 数据库名.表名 查看某表某一列上的索引使用下面的SQL语句: show index from ...