• mips64高精度时钟引起ktime_get时间不准,导致饿狗故障原因分析【转】


    重点关注关中断的情况。临时做了一个版本,在CPU 0上监控所有非0 CPU的时钟中断计数,检查他们在100ms内是否增加。如果否,则认为此
    CPU关中断。另外,在高精度时钟中断处理函数hrtimer_interrupt以及时钟中断总入口打点,记录最长耗时。
    关中断检测代码如下:
    void check_timer_start(void)
    {
    int i = 1;
    for(;i<32;++i)
    cpu_timer_tick[i]=kstat_irqs_cpu(7, i);
    }


    void check_timer_end(void)
    {
    int i = 1;
    int save_console_level = console_loglevel;
    for(;i<32;++i){
    /*should I dump only once?*/
    if(cpu_flag_dump[i]==1)
    continue;
    if((kstat_irqs_cpu(7, i)>MIN_TICK_START)&&
    (cpu_timer_tick[i]==kstat_irqs_cpu(7, i))){
     
    console_loglevel = 4;
    mips_send_nmi_single(i);
    printk("send nmi to cpu[%d] ",i);
    cpu_flag_dump[i] = 1;
    system_is_down = 1;
    dump_max_data(); 
    }
    }
    console_loglevel = save_console_level;
    }
    void check_dead_cpu(void)
    {
    if(system_is_down)
    return;
    if(time_after64(jiffies_64, last_tick_cpu0+sysctl_dead_time)){
            last_tick_cpu0=jiffies_64;
    if(cpu0_checking){
    cpu0_checking = 0;
    check_timer_end();
    }else{
       cpu0_checking = 1;
    check_timer_start();
    }
    }
    }
    通常情况下,上述代码正常运行的前提是,CPU 0的时钟中断运作正常。实际上,如果CPU 0关中断超过一定时间,这种检测是否还可靠?首先
    ,CPU 0负责更新jiffies。根据关中断检测代码的逻辑,每隔100ms为一个周期,如果这段时间某非0的CPU的时钟中断计数不变,则认为此CPU
    关中断。假设CPU 0的时钟中断关闭了一段时间,则这段时间内jiffies可能未更新,在下一次CPU 0的时钟中断到来时,会进行jiffies补偿。
    那么是否可能存在时钟只过去了一个tick,而jiffies却跳变了100 tick的情况?也就是说,jiffies一次补偿达到了100 ticks?根据
    tick_do_update_jiffies64的实现,jiffies进行补偿的本次中断距离上次中断之间,一定是发生了问题,不可能发生时间只经历了1个tick,
    而却补偿超过1个tick。(注意32bit中需要用get_jiffies64()获取jiffies64)
    当检测到有CPU关中断后,则往此CPU发送nmi中断,打印其回溯。
    通过上述检测方式,抓到的典型现场如下:
    一个core内的4个子线程全部挂死。其中一个死在中断入口,另外三个在用户态。


    CPU 20:
    errepc:0x806011a0
    epc:0x773160
    eirr:0x88
    status:6000fff7 KX SX UX USER ERL EXL IE


    CPU 21:
    errepc:0x7719e4
    epc:0x7585b4
    eirr:0x0
    status:6000fff5 KX SX UX USER ERL IE


    CPU 22:
    errepc:0xf64f6cc
    epc:0x7585b4
    eirr:0x0
    status:6000fff5 KX SX UX USER ERL IE


    CPU 23:
    errepc:0x4cb474
    epc:0x7585b4
    eirr:0x0
    status:6000fff5 KX SX UX USER ERL IE

    CPU 20被nmi中断打断的地方,位于外部中断入口handle_int。
    虽然跟故障无关,这里也顺带讲一下,xlp的handle_int是经过rollback_handle_int包装的。
    这个是什么意思? xlp的idle是这么设计的:
    LEAF(r4k_wait)
    /* start of rollback region */
    LONG_L  t0, TI_FLAGS($28)
    nop
    andi t0, _TIF_NEED_RESCHED
    bnez t0, 1f
    nop
    wait
    /* end of rollback region (the region size must be power of two) */
    .set pop
    1:
    jr ra
    END(r4k_wait)
    考虑这种情况,如果cpu在wait前被中断,返回现场后,将继续在wait处等待下一个时钟周期到来,才可能再次有调度点检查。为了增强实时性
    ,在handle_int前包装一个rollback的检测:
    rollback_handle_int
    MFC0 k0, CP0_EPC
    PTR_LA  k1, r4k_wait
    ori k0, 0x1f/* 32 byte rollback region */
    xori k0, 0x1f
    bne k0, k1, 9f
    MTC0 k0, CP0_EPC
    9:
        NESTED(handle_int, PT_SIZE, sp)
    这段代码检查如果cpu是在idle里被中断,则恢复现场时直接将pc指向r4k_wait的入口,进行调度点检测,这样可以避免之前说的wait延时。
    再回到故障信息,status显示在用户态。这就有点说不过去了。按道理,既然是在外部中断被nmi打断的,那么status自然应该是在内核态而不
    是用户态。打印出USER的条件是status的KSU为10,至于KSU是硬件还是内核代码我们这里暂且不管,继续往下分析。根据内核的中断流程:
    a) SAVE_ALL保存现场
    b) 置status寄存器的CU0位表示可以访问协处理器0。
    在出问题时的status现场,CU0还是0,说明可能在SAVE_ALL过程中或者之前就被nmi打断了。
    于是做了一个实验,写了一个用户态死循环程序,不断的给此进程发nmi ipi中断。回溯发现,系统有一定几率会出现被在handle_int里被nmi
    打断的情况,并且此时status显示KSU为10,即用户态。由此推测,可能出现这种情况:
    程序在用户态死循环
    来了一个中断,刚进入口,系统来不及更新status的KSU为内核态
    被nmi打断,显示此时在用户态

    当然,上述的分析只是推测,对故障定位本身没有影响,只是提出在此以备将来参考。
    其实抓到的现场,除了在handle_int入口外,还有在tlb refill入口处的现场。
    因此问题就变成了,为什么会在handle_int停住,或者是否发生了中断嵌套?
    于是在handle_int以及tlb refill添加统计计数,看cpu此时到底在做什么。

    4.4 在中断入口打点

    用一个每cpu变量来记录各个cpu的handle_int计数。
    uasm_i_dmfc0(p, K0 C0_CONTEXT);
    uasm_i_dsrl(p, K0, K0, 23);
    UASM_i_LA(p, K1, cpu_int_count);
    uasm_i_daddu(p, K1, K0, K1);
    uasm_i_ld(p, K0, 0, K1);
    uasm_i_daddiu(p, K0, K0, 1);
    uasm_i_sd(p, K0, 0, K1);
    实际上xlp有一个scratch寄存器可以保存每个cpu的tlb miss次数。
    当故障复现时,发现handle_int次数没有变化。说明不是外部中断嵌套。
    说明一下xlp的寻址流程。
    在开启pagewalker和extent tlb时,不考虑cache,当给定一个虚拟地址vaddr,cpu首先会同时检查所有的tlb,如果没有匹配的条目,则引发
    一个tlb miss,pagewalker将对应页表项填入random的一个tlb条目。通常情况下,在linux设计框架里,这个过程是不会发生二次异常的。如
    果填入的是一个无效的条目,再次访址引发tlb load/modify/store异常,走page fault执行写时复制,按需分配等。
    根据故障现场时的epc,显示是在用户态,那么有怀疑是用户态正在执行指令,发生tlb miss进入了pagewalker流程,但在pagewalker重填过程
    中流水线halt住,导致不响应中断。从我们实验的结果来看,当系统出问题时,我们给此cpu发送一个nmi,此cpu的handle_int计数就开始增加
    。因此pagewalker的硬件设计值得怀疑,与boardcom交流后,fae承认工程部已发现pagewalker的问题,并正在调试。
    由于pagewalker对我们来说是一个黑盒,因此下面我们关闭pagewalker进行测试。

    4.5 关中断超过100ms

    继续用之前的检测代码,在关闭pagewalker的情况下测试。大约拷机进行了3天左右,系统检测到有个CPU关中断超过100ms。往此CPU发nmi时,
    发现挂死的位置不定,有可能在update_process_times,double_rq_lock,hrtimer_interrupts里的红黑树操作等。
    这几个现场基本都和hrtimer_interrupts相关,而且表象是hrtimer_interrupt耗时,因此我们在hrtimer_interrupts,7号中断里,添加计时
    代码。
    下面是抓到的现场:
    NMI triggered on CPU 6
    errepc : 0xffffffffc0699cfc
    epc   : ffffffffc0699cfc rcu_check_callbacks+0x4/0x190
    ra    : ffffffffc066b320 update_process_times+0x68/0x140
    [<ffffffffc0699cfc>] rcu_check_callbacks+0x4/0x190 
    [<ffffffffc066b320>] update_process_times+0x68/0x140 
    [<ffffffffc0684a84>] tick_sched_timer+0x6c/0x198 
    [<ffffffffc067bd1c>] __run_hrtimer.clone.33+0x7c/0x220 
    [<ffffffffc067c648>] hrtimer_interrupt+0xf8/0x248 
    [<ffffffffc06310f0>] c0_compare_interrupt+0x60/0xa8 
    [<ffffffffc069475c>] handle_IRQ_event+0x94/0x238 
    [<ffffffffc06949a8>] __do_IRQ+0xa8/0x230 
    [<ffffffffc0629c10>] do_IRQ+0x48/0x50 
    [<ffffffffc06197ac>] nlm_common_timer_interrupt+0x44/0xb8 
    [<ffffffffc0600f00>] ret_from_irq+0x0/0x4 
    [<ffffffffc060ea0c>] _spin_unlock_irq+0x24/0x58 
    [<ffffffffc066f1a8>] SyS_rt_sigtimedwait+0x190/0x280 
    [<ffffffffc06035a4>] handle_sys64+0x44/0x60


    cpu[2]'s irq7 max time:8ms 
    cpu[2]'s hrtimer_func callback max time:3413ms 
    cpu[2]'s callback func:0xffffffffc067b8b8


    cpu[7]'s irq7 max time:8ms 
    cpu[7]'s hrtimer_func callback max time:3413ms 
    cpu[7]'s callback func:0xffffffffc067b8b8


    cpu[9]'s irq7 max time:17592186041002ms 
    cpu[9]'s hrtimer_func callback max time:0ms 
    cpu[9]'s callback func:0xffffffffc06771e0


    cpu[10]'s irq7 max time:7ms 
    cpu[10]'s hrtimer_func callback max time:3413ms 
    cpu[10]'s callback func:0xffffffffc067b8b8


    cpu[11]'s irq7 max time:3412ms 
    cpu[11]'s hrtimer_func callback max time:0ms 
    cpu[11]'s callback func:0xffffffffc06771e0
     
    cpu[12]'s irq7 max time:6ms 
    cpu[12]'s hrtimer_func callback max time:17592186041002ms 
    cpu[12]'s callback func:0xffffffffc067b8b8


    上面的irq7代表的是7号中断耗时,hrtimer_func callback代表的是单次hrtimer回调的执行耗时,callback func即此回调的函数地址。
    打点记录耗时的函数用的是ktime_get,为什么会出现3413ms和17592186041002ms?我们在第4.7节分析。
    在继续之前,先简单介绍一下高精度时钟的计时原理。

    4.6 高精度时钟回顾

    既然说到高精度时钟,就顺便整理一下高精度时钟从初始化到正常运行的流程,熟悉的同学可以跳过这一节。首先,在2.6.32内核里,高精度
    时钟是放在硬中断里做的,这就带来一个问题,提高实时性的同时也导致中断时间过长,一把双刃剑。
    先说明两个概念,clock event 和 clock source。前者用来触发下一次中断,不管是否配置CONFIG_HIGH_RES_TIMERS此框架都会在代码里存在
    ,一般是用cpu自带的counter和compare实现;后者是要求系统提供一个64位累加计数,具体的实现可能有多种,对于mips64而言,要么用
    jiffies,要么用32位的counter,要么用64位的pic timer。

    4.6.1 高精度模式的切换

    系统刚初始化时,clock_device dev的 event_handler是一个空函数,后面被替换成tick_handle_periodic,接着主动触发一个时钟中断,在
    tick_handle_periodic里临时完成一次时钟中断的工作,当运行到时钟软中断时,尝试进行真正的高精度模式切换。不管切换是否成功,都会
    走原来的wheel定时器处理流程。作者注释说这种方法不好,每次都会在软中断里尝试切换到高精度时钟。要改进的话也有方案,例如
    那么就可能有以下疑问:
    1)满足什么条件时,系统会尝试切换到高精度模式?
    2)为什么要在软中断里不停的尝试切换?
    当clock source或者clock event变化(注册)时,将会给系统置位,并尝试在下一个时钟软中断里切换至高精度时钟,如果切换失败,则置位
    等待下一次clock source或者clock event的变化。需要说明的是,clock event的变化是针对某个cpu的,clock source的变化则是针对所有
    cpu。也就是说,如果某个cpu的clock event发生了变化,则只在此cpu的软中断里切换到高精度模式,反之clock source发生改变后,所有的
    cpu都必须切换到高精度模式。如果确实有clock event或者clock source发生了变化(很有可能是模块注册了新的clock source),则进行下
    一步检查,要求系统未使能no_hz模式,也就是说,不允许从no_hz模式切换到高精度模式。接下来是核心的判断,如果目前使用的clock 
    source支持高精度模式,则可以切换到高精度模式。
    关于高精度模式的切换我们就说到这里,现在假设系统已运行在高精度模式下。

    4.6.2 ktime_get和hrtimer_interrupt

    有两点需要注意:
    A)时间获取用的是ktime_get,这是高精度定时器超时比较的基准;
    B)判断红黑树上高精度timer是否超时的基准时间,是第一次进入到hrtimer_interrupt时,取的当前时刻。

    先看ktime_get。 ktime_get设计的目的是获取是系统从启动到现在的时间。大体实现原理是根据墙上时钟xtime,根据wall_to_mono转换成单
    调递增时间。
    ktime_get在mips64 xlp上的实现,靠的是每cpu的32位counter寄存器。实现的原理是,每个cpu上counter寄存器,与打点时间做差算出偏移,
    然后根据cpu频率换算出具体的纳秒值。
    static inline s64 timekeeping_get_ns(void)
    {
    cycle_t cycle_now, cycle_delta;
    struct clocksource *clock;


    /* read clocksource: */
    clock = timekeeper.clock;
    cycle_now = clock->read(clock);


    /* calculate the delta since the last update_wall_time: */
    cycle_delta = (cycle_now - clock->cycle_last) & clock->mask;


    /* return delta convert to nanoseconds using ntp adjusted mult. */
    return clocksource_cyc2ns(cycle_delta, timekeeper.mult,
     timekeeper.shift);
    }
    核心代码即上面的红字部分。cycle_now是调用ktime_get的cpu的counter值,clock->cycle_last是cpu 0负责每个tick更新。通过这个差值,
    可以算出每个cpu上的当前时刻。这里存在的问题是什么?(分析在4.7)


    再分析hrtimer_interrupt函数的实现,这里参考的内核版本是2.6.32.41。hrtimer_interrupt在某些时候,会打印hrtimer: interrupt took 
    3456 ns的告警,我们来看看这是为什么。
    在每次进入hrtimer_interrupt时,用ktime_get获取当前时间,摘下当前红黑树的最左节点,判断该节点的到期时间是否小于当前时刻,如果
    是,则认为需要执行定时器回调,并取下一个最左节点进行超时比较;如果否,则退出遍历流程。要强调的是,刚进入hrtimer_interrupt时取
    得的时间点,是判断所有timer是否超时的基准。假设有如下场景:
    1)tick 4来了一个时钟中断,红黑树上最早到期的timer 1超时时间为tick 1,于是执行此timer的回调函数,执行长度为4个tick。
    2)timer 1的回调结束后取下timer 2,发现超时时间为tick 3,小于tick 4,于是继续执行timer 2的回调,timer 2耗时5个tick。
    3)timer 3的回调结束后取下timer 3,发现超时时间为tick 18,大于tick 4,因此退出循环。
    4)取当前时间,得到tick 13,则设置下一次超时时间为距离当前(18-13)=5 tick后触发。示意图如下:
     







    5)如果第3步的timer 3,超时时间为tick 9,则设置超时时间时会出错,如果连续出现这种状况3次,则打印告警。之所以会出现这种现象,
    是因为timer过多,并且每个回调比较耗时。hrtimer_interrupt告警示意图如下:
     




    4.7 超时数据分析与复现

    有了4.6节的基础,再来看故障现场。
    故障发生时,有两个现象值得注意:
    A)系统曾经打印过hrtimer: interrupt took 3456 ns之类的告警;
    B)4.5节的数据,关中断时间出现了3413ms和17592186041002ms。
    A现象,说明应该出现过高精度时钟比较频繁,并且单个耗时的现象,观察跑流的单板现场,/proc/timer_list,控制面很多posix timer和
    nanosleep timer。写了一个小程序,尽量模拟出高负载的高精度定时器同时处理的场景,即开启poxis timer,以及调用nanosleep。目的是让
    其模拟出hrtimer_interrupt耗时过长的告警。最终的测试程序,启动了5000个线程,每个线程启动一个posix timer(RESTART性质,可以加重
    hrtimer_interrupt单次处理的负担),以及nanosleep调用,大约3分钟左右就会复现一次故障。
    关于B现象的这两个数据,一个是7号时钟中断的最大耗时,一个是高精度时钟的回调函数最大耗时。其中,17592186041002大的有点离谱,怀
    疑是溢出。
    尝试加一下他们的和:3413+17592186041002 = 0xFFFFFFFFFFF
    感觉互相有关联,分析一个就够了。
    我们先来分析3413的来历。
    4.6.2的我们曾提到,ktime_get的实现最核心的一句为:
    cycle_delta = (cycle_now - clock->cycle_last) & clock->mask;
    由于cycle_now取得是本地cpu的32 位counter值, clock->cycle_last是CPU 0负责更新,因此就带来同步的风险。
    考虑如下状况:
    如果CPU 1的counter比CPU 0的counter稍滞后一点。CPU 0的tick来临,并更新cycle_last,此时在CPU 1上调用了ktime_get,由于CPU 1的
    counter滞后于CPU 0,此时cycle_now正好小于cycle_last,相减得到一个接近2^32的数值,从而得到的ktime_get值极大。一旦发生这种跳变
    ,则引起的关中断超时的现象都不足为奇了,因为高精度定时器处理的基准是ktime_get,如果取的时间过大,可能导致处理较多的timer,引
    起耗时。另外,counter寄存器的频率大约是1.3G,32bits能表示的最大数为4*10^9,
     约为4*10^9/1.3*10^9 = 3.3s,与报错的数据也大体吻合。
          下图说明了窗口造成ktime_get异常的原因:
     


    5 总结

    按照4.7分析的结果,在hrtimer_interrupt添加验证代码,进一步说明是因为counter的窗口问题导致ktime_get跳变,引起高精度时钟处理时
    间过长,再加上之前说的高精度时钟都在硬中断下完成,导致这个关中断时间过长的问题。
    最初想到有如下解决方案:
    1) 采用counter 64位寄存器作为时钟源;
    1.1) __asm__ __volatile__("DMFUR $16, %0" :"=r"(tmp));
    2) 去掉高精度时钟,采用jiffies做时钟源;
    2.1) 去除CONFIG_HIGH_RES_TIMERS(会自动去除CONFIG_TICK_ONESHOT);
    2.2) 去除CONFIG_CSRC_R4K时钟源,这样会自动选中jiffies时钟源。
    3) 去掉高精度时钟,采用pic timer做时钟源。

    但实际上,除了3,剩下的两种方案都有缺陷。
    如果采用counter64做寄存器,虽然避免了32位相减溢出,但由于counter64是每cpu寄存器,仍然存在窗口风险;
    如果只是把高精度时钟关掉,以jiffies为时钟源的话,会出现系统时间比实际时间慢的情况。具体原因我们下面讨论。
    首先用户态取时间gettimeofday,而内核函数是do_gettimeofday:
    void getnstimeofday(struct timespec *ts)
    {
    *ts = xtime;
    nsecs = timekeeping_get_ns();
    timespec_add_ns(ts, nsecs);
    }
    最终的时间是xtime加上当前时刻距离上次中断的偏移。
    xtime的更新速度以HZ为基准,若在两个tick之间调用了gettimeofday,则timekeeping_get_ns可以算出当前时刻距上次中断过了多长时间(精
    确到时钟源,而不是时钟tick),再加到xtime里去,得到精确的时间。
    如果说用getnstimeofday获取的时间比实际的时间慢,根据上面的代码,有两种可能:
    a) xtime比实际的小
    b) timekeeping_get_ns比实际的小

    a.1) xtime在update_wall_time里更新,
       offset = (clock->read(clock) - clock->cycle_last) & clock->mask;
       while (offset >= timekeeper.cycle_interval) {
           u64 nsecps = (u64)NSEC_PER_SEC << timekeeper.shift; 
           offset -= timekeeper.cycle_interval;
    clock->cycle_last += timekeeper.cycle_interval;


           timekeeper.xtime_nsec += timekeeper.xtime_interval;
    if (timekeeper.xtime_nsec >= nsecps) {
       timekeeper.xtime_nsec -= nsecps;
       xtime.tv_sec++;
       second_overflow();
    }
       }
       xtime.tv_nsec = ((s64) timekeeper.xtime_nsec >> timekeeper.shift) + 1;
    原理是根据当前时钟源的计数,减去上次时钟tick到来时的时钟源计数,根据这个差值,
    换算成纳秒,然后依次进位累加到xtime的秒,以及剩下的加到纳秒单位上。
    场景:
    1)如果中断被关了一会,则带来的影响是,jiffies没有及时更新。
    由于以jiffies为时钟源,linux不会对jiffies进行补偿,所以关中断对时钟系统来说是透明的。如果jiffies没有及时更新,此时又调用了
    getnstimeofday,则xtime滞后了,得到的时间自然要少。
    2)以jiffies为时钟源,就算没有关中断,但是每次时钟中断都有一定的耗时,由于tick_handle_periodic设置下一次时钟中断的时间,为相
    对当前时刻加上tick_period
    所以如果每次时钟中断耗时x, 则n次时钟中断后,将会落后标准时间n*x。
     根源还是jiffies不准确,在GSU上应该也有这个问题(GSU是jiffies),但由于GSU会从主控来同步时间,所以这个问题没有表现出来。


    所以,最终的解决方案是,暂时关闭高精度时钟,并采用pic timer做时钟源,具体的修改即关闭CONFIG_HIGH_RES_TIMERS,并注册一个rating
    高于r4k时钟源的pic timer时钟源即可。
  • 相关阅读:
    unable to start kestrel System.Net.Sockets.SocketException (10013): 以一种访问权限不允许的方式做了一个访问套接字的尝试。
    c# 复制文件夹内所有文件到另外一个文件夹
    git初始化
    c# 递归获取所有目录,所有文件,并替换文件
    新增项目 提交到gitee
    netcore3.1 跨域请求
    netcore appsettings.json 绑定对象
    nuget安装包
    做人六字诀:静,缓,忍,让,淡,平
    docker安装部署
  • 原文地址:https://www.cnblogs.com/sky-heaven/p/5381859.html
Copyright © 2020-2023  润新知