我就获取个时间,机器就down了

1. 背景

linux 时间管理,包含clocksource,clockevent,timer,tick,timekeeper等等概念 ,这些概念有机地组成了完整的时间代码系统运维的主要任务体系后端。当然,是代码就会有bug,本文通过一个bug入手,在实战中加深对理论的认识。获取时间,但是crash了。

2. 故障现象

OPPO系统运维工资一般多少云内核团队接到连通性告警报障,发现机器复位:
```PID: 0 TASK: ffff8d2b3775b0c0 CPU: 1linux操作系统基础知识 COMMAND: "swapper/1"
#0 [ffff8d597f6489f0] machine_kexec at ffffffffa5a63后端语言b34
#1 [ffff8d597f648a50] crash_kexec at fffffff后端和前端有什么区别fa5b1e242
#2 [ffff8d597f648b20] panic at ffffffffa615后端和前端有什么区别d85b
#后端开发工资一般多少3 [ffff8d597f648ba0] nmi_panic at ffffffffa5a985后端语言9f
#4 [ffff8d597f648bb0] watchdog_overflow_callback at ffffffffa5b4a881
#5linux创建文件 [ffff8d597f648bc8]
perf_event_linux必学的60个命令overflow at ffffffffa5ba26b7
#linux必学的60个命令6 [ffff8d597f648c00] perf_event_overflow at ffffffffa5babd24
#7 [ffff8d597f648c10] intel_pmu_handle系统/运维_irq at ffffffffa5a0a后端语言850
#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 ffffffffa后端开发616dd89
[exception RIP: getnstimeofday64+144]
RIP: fffff系统运维的主要任务ff后端开发是干什么的fa5b03940 RSP: f系统/运维fff8d597f643c后端是做什么的78 RFLAGS: 00000212
RAX: 15b5c8320b8602cd RBX: ffff8d597f643cb0 RC后端是做什么的X: 000000005f89ee29
RDX: 00000000ee4479fe RSI: 0000012b5478f3b2 RDI: 0009709c7629b240
RBP: ffff8d597f643c90 R8: 00000000007001de R9: ffff8d596d5c0000
R10: 000000000000007a R11: 000000000000000e R12: ffffffffa662ea80
R13: 000000003ccbcfb6 R1后端开发需要学什么4: ffff8d895de08000 R15: 0000000000000081
ORIG_RAX: ffffffffffffffff CS: 0010 SS: 0018
--- <NMI exceplinuxtion stack> ---
#12 [ffff8d597f643c78] __getnstimeofday64 at ffffffffa5b03940
#13 [ffff8d597f643c98] getnstimeofday64 at ffffffffa5b0398e
#14 [ffff8d597f643ca8] ktime_get_real at ffffflinux必学的60个命令fffa5b03a45
#15 [ffff8d597f643cd0] netif_receive_skb_internal at ffffffffa6后端开发03b936
#16 [ffff8d597f643d00] napi_gro_receive at fffffff系统运维工资一般多少fa603c588
#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 ffff后端开发需要学什么ffffc05304c6 [mlx5_core]
#20 [ffff8d597f643e78] net_rx_action at ffffffffa603bf1f
#21 [ffff8d597f643ef8]
do_softirq at ffffffffa5aa2155
#22 [ffff8d597f643f68] call_softirq at ffffffffalinux常用命令617a32c
#23 [ffff8d597f643f80] do_soflinux重启命令tirq at ffffffffa5a2e675

从堆栈看,我们的0号进程在处理软中断收包的过程中,因为获取个时间,导致了crash。hardlock的分析之前已经给出了很多了,无非是关中断时间长了,具体关中断的地方,可以看call_softirq函数即可。
## 3. 故障现象分析
### 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 __getnstimeof后端工程师day64(struct timespec64 ts)
{
struct timekeeper
tk = &timekeeper;
unsigned long seq;
s64 nsecs = 0;

do {
seq = read_seqcolinux创建文件unt_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;
timespec系统运维工程师面试问题及答案64_add_ns(ts, nsecs);//linux必学的60个命令caq:这里面还有个循环呢,

/*

  • Do not baillinux常用命令 out early, in case there were callers still using
  • the value, evelinux创建文件n in the face of the WARN_ON.
    */
    if (unl系统运维主要做什么ikely(timelinux中文乱码视频keepi后端ng_suspended))
    return -EAGAIN;
    return 0;
    }
    但是从汇编展开来看:

    0xffffffffa5b0393b系统运维主要做什么 <getnstimeofday64+139>: xor %edx,%edx----清零 u32 ret = 0;
    0xffffffffa5b0393d <
    getnstimeofday64+1后端工程师41>: nopl (%rax)
    0xffffffffa5b03940 <getnstimeofday64+144>: sub $0x3b9aca00,%rax---------------------1s就是1000000000 ns,循坏在这,而栈中的rax后端开发工资一般多少为 15linuxb5c8320b8602cd
    0xffffffffa5b03946 <
    getnstimeofday64+150>: add $0x1,%edx---------ret++;edx is the lower 32 bit of rdx,rdx为00000000ee4479fe,所以elinux重启命令dx为系统运维包括哪些内容 0xee4479fe,也就是3997465086
    0xffffffffa5b03949 <getnstim后端语言eofday64+153>: cmp $0x3b9ac9ff,%rax-------------------------------------剩余是系统运维包括哪些内容否小于1ns
    0xffffffffa5b0394f <
    getnstimeofday64+159>: ja 0xffffffffa5系统运维工资一般多少b03940 <get后端开发nstimeofday64+144>
    /include/linux/time后端开发工程师.h: 215---linux中文乱码视频对应 timespec_add_ns
    0xffffffffa5b03951 <
    getnstimeofday64+161>: add %rcx,%rdx---delta算出的秒值+之前保存的秒值,就是最后端开发需要学什么新的秒值
    0xffffffffa5b03954 <getnstimeofday64+164>: mov %rax,0x8(%rbx)---linux常用命令-剩余的ns,赋值给a->tv后端开发是干什么的_nsec = ns;
    0xfff后端开发工程师fffffa5b03958 <__getnstimeofday64+168>: mov %rdx,(%rbx)---加完delta秒值的最新的秒值,赋值给a->tv_sec
    0xffffffffa5b0395b <
    getnstimeofday64+171>: cmpl $0x1,0xc55702(%rip) # 0xffffffffa6759064----if(timekeeping_suspended)
    /kernel/time/time后端是什么工作keeping.c: 512
    0xffffffffa5b03962 <getnstimeofday6后端4+178>: pop %rbx
    0xfff后端工程师fffffa5b03963 <
    gelinux常用命令tnstimeofday64+179>: pop %r12
    0xffffffflinux是什么操作系统fa5b03965 <__getnst系统运维的主要任务imeofday64+181&g后端工程师t后端;: pop %r13

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

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

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

    static al后端和前端有什么区别ways_in后端line void t后端工程师imespe后端工程师c64_add_ns(struct timespec64 *a, u64 ns)
    {
    a->tv_selinux必学的60个命令c +=
    iter_div_u64_rem(a->tv_nsec + ns, NSEC_PER_SEC, &ns);
    a->tv_nsec = ns;
    }
    __系统运维工资一般多少iter_div_u64_rem展开如下:

static系统运维工资一般多少 __always_inlilinux中文乱码视频n后端e u32
_后端开发是干什么的_iter_div_u64_rem(u64 dividend, u32 divisor, u64 *rlinux常用命令emainder)
{
u32 ret = 0;

while (dividend >= divisor) {//这个循环
/ The following asm() prevents the compiler from
optimising this loop into a mlinux重启命令odulo operation.
/后端是什么工作
asm("" : &系统运维工程师面试问题及答案quot;+rm"(dividend));

dividend -= divisor;
ret++;

}

*系统运维包括哪些内容remainder = divide后端开发需要学什么nd;

rlinux必学的60个命令eturn ret;
}

我们的入参divisor是 NSEC_PER_SEC,也就是10的9次方,16进制为0x3b9aca00,既然在循环,那么我们的dividend是rax,请注意看值为:

RAX: 15b5c8320b8602cd

crash> p 0x15b5c8320b8602c后端d/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 ti后端是做什么的mekeeping_delta_to_ns(tkr, delta);//caq:linux重启命令差值转换系统运维的主要任务为ns
}

delta的来源是:
static inline u64 timekeeping_get_delta(stlinuxruct tk_read_base tkr)
{
u64后端是做什么的 cycle_now, delinux删除文件命令lta;
struct clocksource
clock;

/ read cloc后端开发ksource: /
clock = tkr-&g系统运维工作内容t;clock;
cycle_now = tkr->clock->read(clock);//当前值是通过读取来的

/ calinuxlculate 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
timekelinuxeper = $1 = {
tkr_mono = {------------------------------timekeeping_get_ns(&tk->tkr_mono)
clock = 0xffffffflinux删除文件命令fa662ea80,后端语言 ------------这个就是 clocksource,这个值当前就是 clocksource_tsc
cycle_last = 16728674596502256,
mult = 73405linux删除文件命令10,
shift = 24,
xtime_nse后端是做什么的c = 2657092090049088, 这个值并不是ns,而后端开发需要学什么是要 >>tkr->shift
ba系统运维工程师面试问题及答案se = {
tv64 = 2788453640047242
}
},
tkr_raw = {
clock = 0xffffffffa662ea80,
cycle_last = 16728674596502256,
mult = 8007931linux中文乱码视频,
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,
mlinuxult = 8007931,
shift = 24, ----------------------注意位数
max_idle_ns = 204347035648,
maxadj = 880872,
archdata = {
vclock_mode = 1
},
name = 0xffffffffa647c1cd "tsc", ---名称
list = {
next = 0xflinux中文乱码视频fffffffa6633ff8,
prev = 0xffffffffa665c9b0
},
rating = 300, -----系统运维的主要任务---------优先级,
enable = 0x0,
dilinux常用命令sable = 0x0,
flags = 35,linux创建文件 ---没有CLOCK_SOURCE_UNSTABLE标志
suspend = 0x0,
resume = 0x0,
owner = 0x0
}

差值的计算分析如下:

static inline s64 timekeeping_deltalinux删除文件命令_to_ns(struct tk_linuxread_base *tlinuxkr,
u64 delta)
{
s64 nsec;//注linux系统意,这系统运维主要做什么里是带符号数

nsec = delta * tkr->mult + tkr->xtime_nsec后端工程师;
nselinux操作系统基础知识c >>= tkr->shift;//换算成ns

/ If arch requires, add in get_arch_timeoffset() /
return nsec + arch_gettimeoffset();
}linux系统

timekeeping_delta_to_ns返回值过大,有两种可能:
一种是delta的偏大,delta * tkr->mult 对s64的值产生溢出,这个算是个bug。还有一种可能是,直接前后读取的delta值太大,这涉及到 update_wall_time 并没有及时调用去读取当前clocksource的cycle。
## 4. 故障复现
这个s64溢出的bug,在社区已经修复了。

-static inline s64 timekeeping_delta_to_ns(struct tk_read_balinux是什么操作系统se tkr,
+static inline u64linux必学的60个命令 timekeeping_delta_to_ns(struct tk_read_base
tkr,
cycle_t delta)
{

  • s64系统运维主要做什么 nsec;
  • u64 nsec;
    
    而且查看红帽的changelog,也按照上游这样修复,但是我觉得风险还在的,因为 update_wall_time 有时候更新就不是那么及时,而哪怕从s64改到u64,并没有解决溢出问题,因为 timekeeping_delta_to_ns函数中明显可以看到,u64的64位并没有全部用到cycle的差值上。我相信社区最终应该会有人爆这个问题的。
    ## 5. 故障规避或解决
    可能的解决方案是:
    增加告警,对于softlock的要及时介入,有可能导致update_wall_time 更新不及时。
    ## 作者简介
    Anqing  高级后端工程师
    目前主要负责linux内核及容器,虚拟机等虚拟化方面的工作。
    获取更多精彩内容,扫码关注[OPPO数智技术]公众号
    ![qrcode_for_gh_7bc48466f080_258.jpg](https://static.wpfen.cn/uploads/2021/11/1637219359173651.jpg?x-oss-process=image/resize,m_fill,h_258,w_258?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=)