现在的位置: 首页 > 综合 > 正文

linux内核动态时钟分析

2016年07月23日 ⁄ 综合 ⁄ 共 7410字 ⁄ 字号 评论关闭
在早期的linux内核版本的时间概念都是由周期时钟提供的。虽然比较有效,但是,对于关注能耗电量的系统上,就不能满足长时间休眠的需求,因为周期系统要求必须在一定的频率下,周期性的处于活动状态。因此,linux提出了tickless system,即无时钟系统。其关键就是判定系统当前是否无事可做,若是则禁用时钟系统。判定系统当前无事可做的依据是:如果运行队列时没有活动进程,内核将选择idle进程来运行,而此时动态时钟发挥作用。
一、动态时钟使用的数据结构tick_sched

点击(此处)折叠或打开

  1. struct tick_sched {
  2.     struct hrtimer            sched_timer;//用于实现时钟的定时器
  3.     unsigned long            check_clocks;
  4.     enum tick_nohz_mode        nohz_mode;
  5.     ktime_t                idle_tick;//禁用周期时钟之前,上一个时钟信号到期时间。
  6.     int                inidle;
  7.     int                tick_stopped;//周期时钟是否已经停用,若停用,则置为1
  8.     unsigned long            idle_jiffies;//存储周期时钟禁用时的jiffy值
  9.     unsigned long            idle_calls;//内核试图停用周期时钟次数。
  10.     unsigned long            idle_sleeps;//成功停用周期时钟次数。
  11.     int                idle_active;
  12.     ktime_t                idle_entrytime;
  13.     ktime_t                idle_waketime;
  14.     ktime_t                idle_exittime;
  15.     ktime_t                idle_sleeptime;//周期时钟上一次禁用的准确时间
  16.     ktime_t                idle_lastupdate;
  17.     ktime_t                sleep_length;//周期时钟禁用的时间长度
  18.     unsigned long            last_jiffies;
  19.     unsigned long            next_jiffies;//下一个定时器到期的jiffy值
  20.     ktime_t                idle_expires;//下一个将到期的经典定时器到期时间的jiffy值
  21.     int                do_timer_last;
  22. }



二、低分辨率下的动态时钟

每个定时软中断中会判断是否启用动态时钟,具体调用序列如下:
run_timer_softirq--》hrtimer_run_pending--》tick_check_oneshot_change--》tick_nohz_switch_to_nohz
而其tick_nohz_switch_to_nohz具体实现如下:

点击(此处)折叠或打开

  1. static void tick_nohz_switch_to_nohz(void)
  2. {
  3.     struct tick_sched *ts = &__get_cpu_var(tick_cpu_sched);
  4.     ktime_t next;

  5.     if (!tick_nohz_enabled)//若没有启动动态时钟直接返回
  6.         return;

  7.     local_irq_disable();
  8.     if (tick_switch_to_oneshot(tick_nohz_handler)) {//切换时钟设备的处理函数为tick_nohz_switch_to_nohz
  9.         local_irq_enable();
  10.         return;
  11.     }

  12.     ts->nohz_mode = NOHZ_MODE_LOWRES;//设置为低分辨率

  13.     /*
  14.      * Recycle the hrtimer in ts, so we can share the
  15.      * hrtimer_forward with the highres code.
  16.      */
  17. //更新jiffy值
  18.     hrtimer_init(&ts->sched_timer, CLOCK_MONOTONIC, HRTIMER_MODE_ABS);
  19.     /* Get the next period */
  20.     next = tick_init_jiffy_update();
  21. //设置下一个时钟事件
  22.     for (;;) {
  23.         hrtimer_set_expires(&ts->sched_timer, next);
  24.         if (!tick_program_event(next, 0))
  25.             break;
  26.         next = ktime_add(next, tick_period);
  27.     }
  28.     local_irq_enable();

  29.     printk(KERN_INFO "Switched to NOHz mode on CPU #%d\n",
  30.      smp_processor_id());
  31. }

动态时钟处理程序tick_nohz_handler的实现如下:


点击(此处)折叠或打开

  1. static void tick_nohz_handler(struct clock_event_device *dev)
  2. {
  3.     struct tick_sched *ts = &__get_cpu_var(tick_cpu_sched);
  4.     struct pt_regs *regs = get_irq_regs();
  5.     int cpu = smp_processor_id();
  6.     ktime_t now = ktime_get();

  7.     dev->next_event.tv64 = KTIME_MAX;

  8.   //设置当前cpu负责全局时钟设备
  9.     if (unlikely(tick_do_timer_cpu == TICK_DO_TIMER_NONE))
  10.         tick_do_timer_cpu = cpu;

  11. //若是全局时钟设备,则更新jiffy值
  12.     if (tick_do_timer_cpu == cpu)
  13.         tick_do_update_jiffies64(now);
  14. //若是启动禁用全局时钟,则更新watchdog的时间戳
  15.     if (ts->tick_stopped) {
  16.         touch_softlockup_watchdog();
  17.         ts->idle_jiffies++;
  18.     }

  19.     update_process_times(user_mode(regs));
  20.     profile_tick(CPU_PROFILING);
  21. //定时下一个时钟周期,并且更新jiffy
  22.     while (tick_nohz_reprogram(ts, now)) {
  23.         now = ktime_get();
  24.         tick_do_update_jiffies64(now);
  25.     }
  26. }
更新jiffy值tick_do_update_jiffies64得具体实现如下:

点击(此处)折叠或打开

  1. static void tick_do_update_jiffies64(ktime_t now)
  2. {
  3. ......
  4. //更新经过的jiffy时间,判断是否在一个周期内,若是则直接返回
  5.     delta = ktime_sub(now, last_jiffies_update);
  6.     if (delta.tv64 < tick_period.tv64)
  7.         return;

  8. //计算经过的jiffy差值,判断是否大于一个周期,若是大于,则先更新一个周期的jiffy值
  9.     delta = ktime_sub(now, last_jiffies_update);
  10.     if (delta.tv64 >= tick_period.tv64) {

  11.         delta = ktime_sub(delta, tick_period);
  12.         last_jiffies_update = ktime_add(last_jiffies_update,
  13.                         tick_period);
  14. //若是大于一个周期,则再计算差值,再加上这个差值的jiffy值
  15.         /* Slow path for long timeouts */
  16.         if (unlikely(delta.tv64 >= tick_period.tv64)) {
  17.             s64 incr = ktime_to_ns(tick_period);

  18.             ticks = ktime_divns(delta, incr);

  19.             last_jiffies_update = ktime_add_ns(last_jiffies_update,
  20.                              incr * ticks);
  21.         }
  22.         do_timer(++ticks);
  23. //更新下一个周期的jiffy值
  24.         /* Keep the tick_next_period variable up to date */
  25.         tick_next_period = ktime_add(last_jiffies_update, tick_period);
  26.     }
  27.     write_sequnlock(&xtime_lock);
  28. }
三、高分辨率下的动态时钟
高分辨率下的动态时钟实现比较容易,具体涉及的地方如下:
1、确定全局时钟的责任,在tick_sched_timer中,如下:

点击(此处)折叠或打开

  1. #ifdef CONFIG_NO_HZ
  2.     /*
  3.      * Check if the do_timer duty was dropped. We don't
    care about
  4.      * concurrency: This happens only when the cpu in charge went
  5.      * into a long sleep. If two cpus happen to assign
    themself to
  6.      * this duty, then the jiffies update is still
    serialized by
  7.      * xtime_lock.
  8.      */
  9.     if (unlikely(tick_do_timer_cpu == TICK_DO_TIMER_NONE))
  10.         tick_do_timer_cpu = cpu;
  11. #endif
2、宣布高精度定时器下启用动态时钟,在tick_setup_sched_timer函数中,如下:

点击(此处)折叠或打开

  1. #ifdef CONFIG_NO_HZ
  2.     if (tick_nohz_enabled)
  3.         ts->nohz_mode = NOHZ_MODE_HIGHRES;
  4. #endif
三、前面介绍的是在高低分辨率情况下,对动态时钟的支持,也即如何做一些更新jiffy类的操作。那么,何时启用和禁止时钟呢?前面已经说过,就是在idle任务中完成这样的操作,因为各个架构内容不同,但原理类似。我们举x86下面的实现片段,如下:

点击(此处)折叠或打开

  1. void cpu_idle(void)
  2. {
  3. ......

  4.     /* endless idle loop with no priority at all */
  5.     while (1) {
  6.         tick_nohz_stop_sched_tick(1);//停止时钟
  7.         while (!need_resched()) {
  8. ......
  9. //pm_idle的实现依赖具体架构而定,x86支持的一种实现是mwait实现,这种实现真正的是haunt住cpu,cpu切实不运转了,可以实现节能的目的,而ARM上可以使用wfi指令,cpu也haunt住,通过中断可以唤醒。
  10.             /* Don't trace irqs off for idle */
  11.             stop_critical_timings();
  12.             pm_idle();//节能的关键部分
  13.             start_critical_timings();
  14.         }
  15.         tick_nohz_restart_sched_tick();//启动时钟
  16.         preempt_enable_no_resched();
  17.         schedule();//调度切换
  18.         preempt_disable();
  19.     }
  20. }
而停止时钟tick_nohz_stop_sched_tick的本质实现:检查下一个定时器事件是否在一个时钟之后,若是,则重新编程时钟设备,忽略下一个时钟周期信号,直至有必要时候才恢复,而且在tick_sched中更新统计信息。重启时钟的原理也很简单,在此不再累述。
四、介绍一下广播模式
在某些省电模式启用时,时候总时间设备进入睡眠,而系统中不只有一个时钟事件设备,仍然可用另一个可工作的设备替换停止设备。在这种情况下,APIC是不工作的,但广播设备仍然可以工作,tick_handle_periodic_broadcast用作事件处理程序,该程序在每个tick_period之后。下面介绍tick_handle_periodic_broadcast的具体实现,如下:

点击(此处)折叠或打开

  1. static void tick_handle_periodic_broadcast(struct clock_event_device *dev)
  2. {
  3.     ktime_t next;

  4.     tick_do_periodic_broadcast();

  5.     /*
  6.      * The device is in periodic mode. No
    reprogramming necessary:
  7.      */
  8.     if (dev->mode == CLOCK_EVT_MODE_PERIODIC)//必须为oneshot模式
  9.         return;

  10.     /*
  11.      * Setup the next period for devices, which do not have
  12.      * periodic mode. We read dev->next_event
    first and add to it
  13.      * when the event alrady expired. clockevents_program_event()
  14.      * sets dev->next_event only when the event is really
  15.      * programmed to the device.
  16.      */
  17.     for (next = dev->next_event; ;) {
  18.         next = ktime_add(next, tick_period);
  19. //重新编程下一个事件
  20.         if (!clockevents_program_event(dev, next, ktime_get()))
  21.             return;
  22.         tick_do_periodic_broadcast();//处理本cpu事件和向其他cpu发送ipi中断,从而调用其他cpu的事件处理程序
  23.     }
  24. }

tick_do_periodic_broadcast --》tick_do_broadcast ,而tick_do_broadcast是关键,其实现如下:


点击(此处)折叠或打开

  1. static void tick_do_broadcast(struct cpumask *mask)
  2. {
  3.     int cpu = smp_processor_id();
  4.     struct tick_device *td;

  5.     //调用本cpu的事件处理程序
  6.     if (cpumask_test_cpu(cpu, mask)) {
  7.         cpumask_clear_cpu(cpu, mask);
  8.         td = &per_cpu(tick_cpu_device, cpu);
  9.         td->evtdev->event_handler(td->evtdev);
  10.     }

  11.     if (!cpumask_empty(mask)) {
  12.         //向其他cpu发送ipi中断
  13.         td = &per_cpu(tick_cpu_device, cpumask_first(mask));
  14.         td->evtdev->broadcast(mask);
  15.     }
  16. }
向其他cpu发送ipi中断实现如下:

点击(此处)折叠或打开

  1. static void lapic_timer_broadcast(const struct cpumask *mask)
  2. {
  3. #ifdef CONFIG_SMP
  4.     apic->send_IPI_mask(mask, LOCAL_TIMER_VECTOR);
  5. #endif
  6. }

最终ipi中断导致其他cpu调用本cpu的时钟事件设备的处理函数。

抱歉!评论已关闭.