Tick,即周期性产生的 timer 中断事件,可用于系统时间管理、进程信息统计、低精度 timer 处理等等。这样就会有一个问题,那就是在系统空闲的时候也还是周期性的产生中断,系统会被周期性的唤醒导致功耗的增加,这对于追求低功耗的嵌入式设备来说是很难接受的。为此,内核开发者提出了动态时钟的概念,即在系统空闲阶段停掉周期性的时钟达到节省功耗的目的。内核可以通过配置项 CONFIG_NO_HZ 及 CONFIG_NO_HZ_IDLE 来打开该功能,这样在系统空闲的时候就可以停掉 tick 一段时间,但并不是完全没有 tick 了,当有除了 idle 进程之外的其它进程运行的时候会恢复 tick 。
clock_event_device,代表一个可以产生时钟事件的硬件时钟设备,这样的时钟设备就像单片机的定时器,可以对它编程设置要触发的定时时间,在定时时间到达的时候产生中断,它可以工作在周期模式或者单触发模式。周期模式就是周期性的产生 timer 中断事件,这和 tick 的定义很像。
clock_event_device 结构定义如下:
struct clock_event_device { /* 回调函数指针,在硬件时钟设备的中断服务程序中调用 */ void (*event_handler)(struct clock_event_device *); /* 设置下一个定时时间,以 counter 的 cycle 数值为参数 */ int (*set_next_event)(unsigned long evt, struct clock_event_device *); /* 设置下一个定时时间,要设置的定时时间是 ktime 格式,要配合 CLOCK_EVT_FEAT_KTIME 标记使用 */ int (*set_next_ktime)(ktime_t expires, struct clock_event_device *); ktime_t next_event; u64 max_delta_ns; u64 min_delta_ns; u32 mult; u32 shift; enum clock_event_mode mode; unsigned int features; unsigned long retries; void (*broadcast)(const struct cpumask *mask); void (*set_mode)(enum clock_event_mode mode, struct clock_event_device *); void (*suspend)(struct clock_event_device *); void (*resume)(struct clock_event_device *); unsigned long min_delta_ticks; unsigned long max_delta_ticks; const char *name; int rating; int irq; const struct cpumask *cpumask; struct list_head list; struct module *owner; }
SMP 系统,每个 CPU 都有一个只属于自己的 local timer 用于提供时钟事件服务,在 CPU 启动的时候通过调用 percpu_timer_setup 函数完成初始化工作。以三星 exynos7420 平台为例,percpu_timer_setup 函数最终调用 exynos4_local_timer_setup 函数来初始化每个 CPU 的 local timer ,配置和注册 clock_event_device。
static irqreturn_t exynos4_mct_tick_isr(int irq, void *dev_id) { struct mct_clock_event_device *mevt = dev_id; struct clock_event_device *evt = mevt->evt; exynos4_mct_tick_stop(mevt, 0); /* 调用 clock_event_device 的 event_handler */ evt->event_handler(evt); return IRQ_HANDLED; } static DEFINE_PER_CPU(struct irqaction, percpu_mct_irq) = { .flags = IRQF_TIMER | IRQF_NOBALANCING, /* 中断处理程序 */ .handler = exynos4_mct_tick_isr, }; static int exynos4_local_timer_setup(struct clock_event_device *evt) { struct mct_clock_event_device *mevt; unsigned int cpu = smp_processor_id(); mevt = this_cpu_ptr(&percpu_mct_tick); mevt->evt = evt; mevt->base = EXYNOS4_MCT_L_BASE(cpu); /* 每个 CPU local timer 的名字分别为 mct_tick0, mct_tick1 ... mct_tick7 */ snprintf(mevt->name, sizeof(mevt->name), "mct_tick%d", cpu); evt->name = mevt->name; evt->cpumask = cpumask_of(cpu); /* 属于哪个 CPU */ /* 设置下次触发的时间,即把时间编程到定时器 */ evt->set_next_event = exynos4_tick_set_next_event; /* 设置工作模式,例如周期模式,单触发模式等 */ evt->set_mode = exynos4_tick_set_mode; /* 支持周期模式和单触发模式 */ evt->features = CLOCK_EVT_FEAT_PERIODIC | CLOCK_EVT_FEAT_ONESHOT; evt->rating = 450; tick_base_cnt = 0; if (!soc_is_exynos5433()) { tick_base_cnt = 1; exynos4_mct_write(tick_base_cnt, mevt->base + MCT_L_TCNTB_OFFSET); } /* exynos7420 每个 CPU 的 local timer 都是使用的 SPI 类型中断 */ if (mct_int_type == MCT_INT_SPI) { struct irqaction *mct_irq = this_cpu_ptr(&percpu_mct_irq); mct_irq->dev_id = mevt; evt->irq = mct_irqs[MCT_L0_IRQ + cpu]; irq_set_affinity(evt->irq, cpumask_of(cpu)); enable_irq(evt->irq); } else { enable_percpu_irq(mct_irqs[MCT_L0_IRQ], 0); } /* 配置和注册 clock_event_device */ clockevents_config_and_register(evt, clk_rate / (tick_base_cnt + 1), 0xf, 0x7fffffff); return 0; }
Exynos7420 平台 local timer 使用的是 SPI 类型的中断,它可以直接唤醒 idle 状态的 CPU,这样就不需要使用 broadcast framework 了。MTK 平台 CPU 的 local timer 一般使用 PPI 中断,它不具有唤醒处于 idle 状态的 CPU 的能力,所以需要一个 global HW timer 作为 broadcast tick,由它来服务每个 CPU,例如将 tick 事件广播到 CPU,唤醒 CPU 等等。
tick_device,tick 设备,是对 clock_event_device 及其工作模式的封装:
struct tick_device { struct clock_event_device *evtdev; enum tick_device_mode mode; };
tick_device_mode 只有两种模式,TICKDEV_MODE_PERIODIC 和 TICKDEV_MODE_ONESHOT,即周期模式和单触发模式。在 clock_event_device 注册的时候,tick_device 通过 tick_check_new_device 和 tick_setup_device 函数绑定一个属于该 CPU 且精度最高的 clock_event_device。这样,tick_device 工作在 TICKDEV_MODE_PERIODIC 模式时可以产生周期性的时钟事件,传统意义上的 tick 就是这么来的。周期模式下,clock_event_device 的 event_handler 被设置为 tick_periodic ,每个 tick 事件到来时 tick_periodic 就会被调用,它会通过 update_process_times 函数进行系统时间的更新、到期 hrtimer 的处理、TIMER_SOFTIRQ 软中断处理、进程信息更新及负载均衡等等。
低分辨率定时器是基于 HZ 来实现的,精度为 1/HZ,内核 HZ 一般配置为 100,那么低分辨率定时器的精度就是 10ms。对定时器精度要求不高的内核模块还在大量使用低分辨率定时器,例如 CPU DVFS,CPU Hotplug 等。内核通过 time_list 结构体来描述低分辨率定时器。
高精度定时器可以提供纳秒级别的定时精度,以满足对时间精度要求严格的内核模块,例如音频模块,内核通过 hrtimer 结构体来描述高精度定时器。在系统启动的开始阶段,高精度定时器只能工作在低精度周期模式,在条件满足之后的某个阶段就会切换到高精度单触发模式。上面所说的 tick_periodic 函数,最后会调用 hrtimer_run_pending 函数来判断是否可以切换到高精度模式。另外,动态时钟也是在这里判断和切换的。流程如下:
tick_periodic -> update_process_times -> run_local_timers -> raise_softirq(TIMER_SOFTIRQ) == run_timer_softirq -> hrtimer_run_pending
void hrtimer_run_pending(void) { /* 如果 hrtimer 已经是高精度模式则返回 */ if (hrtimer_hres_active()) return; if (tick_check_oneshot_change(!hrtimer_is_hres_enabled())) hrtimer_switch_to_hres(); } int tick_init_highres(void) { return tick_switch_to_oneshot(hrtimer_interrupt); } static int hrtimer_switch_to_hres(void) { int i, cpu = smp_processor_id(); struct hrtimer_cpu_base *base = &per_cpu(hrtimer_bases, cpu); unsigned long flags; /* 如果已经是高精度模式了,则返回 */ if (base->hres_active) return 1; local_irq_save(flags); if (tick_init_highres()) { local_irq_restore(flags); printk(KERN_WARNING "Could not switch to high resolution " "mode on CPU %d/n", cpu); return 0; } /* 表示工作在高精度模式了 */ base->hres_active = 1; for (i = 0; i < HRTIMER_MAX_CLOCK_BASES; i++) base->clock_base[i].resolution = KTIME_HIGH_RES; /* 创建一个 hrtimer 来模拟 tick,初始化这个 hrtimer */ tick_setup_sched_timer(); /* "Retrigger" the interrupt to get things going */ retrigger_next_event(NULL); local_irq_restore(flags); return 1; }
最终,通过 hrtimer_switch_to_hres 完成低精度周期模式到高精度单触发模式的切换。tick_device 的工作模式变成了 TICKDEV_MODE_ONESHOT,其 clock_event_device 的 event_handler 被替换为 hrtimer_interrupt。至此,tick_device 不能再定期产生 tick 事件了,但是系统还离不开 tick 事件,所以内核通过一个 hrtimer 模拟了 tick,这个是在 tick_setup_sched_timer 函数中完成的。
void tick_setup_sched_timer(void) { struct tick_sched *ts = &__get_cpu_var(tick_cpu_sched); ktime_t now = ktime_get(); /* * Emulate tick processing via per-CPU hrtimers: */ /* 初始化用作模拟 tick 的 hrtimer */ hrtimer_init(&ts->sched_timer, CLOCK_MONOTONIC, HRTIMER_MODE_ABS); /* 设置该 hrtimer 的回调函数 */ ts->sched_timer.function = tick_sched_timer; /* Get the next period (per cpu) */ hrtimer_set_expires(&ts->sched_timer, tick_init_jiffy_update()); for (;;) { /* tick_period 就是一个 tick 周期,HZ 为 100 的话就是 10ms */ hrtimer_forward(&ts->sched_timer, now, tick_period); /* 启动 hrtimer,加入 hrtimer 红黑树,如果这个 hrtimer 的到期时间是最近的,*/ /* 还会把它编程到硬件时钟设备,即 clock_event_device */ hrtimer_start_expires(&ts->sched_timer, HRTIMER_MODE_ABS_PINNED); /* Check, if the timer was already in the past */ if (hrtimer_active(&ts->sched_timer)) break; now = ktime_get(); } #ifdef CONFIG_NO_HZ_COMMON if (tick_nohz_enabled) /* NO_HZ 的工作模式,表明是使用的 hrtimer 实现 NO_HZ */ ts->nohz_mode = NOHZ_MODE_HIGHRES; #endif }
可以看到,sched_timer 就是模拟 tick 使用的 hrtimer,在其回调函数 tick_sched_timer 中会设置下次触发时间为 tick_period,这样就可以定期产生 tick 事件了。另外,还会通过 tick_sched_handle 函数调用 update_process_times 函数,相信很多人对 u pdate_process_times 很眼熟,前面说的系统时间的更新、到期 hrtimer 的处理、TIMER_SOFTIRQ 软中断处理、进程信息更新及负载均衡等等就是由它完成的。所以说利用 hrtimer 很完美的模拟了 tick 。
static enum hrtimer_restart tick_sched_timer(struct hrtimer *timer) { struct tick_sched *ts = container_of(timer, struct tick_sched, sched_timer); struct pt_regs *regs = get_irq_regs(); ktime_t now = ktime_get(); tick_sched_do_timer(now); /* 更新时间 */ /* * Do not call, when we are not in irq context and have * no valid regs pointer */ if (regs) /* tick_sched_handle 会调用 update_process_times 函数 */ tick_sched_handle(ts, regs); /* 设置下次触发时间为 tick_period */ hrtimer_forward(timer, now, tick_period); return HRTIMER_RESTART; }
tickless,即上面所说的动态时钟,之所以被称为 tickless,估计是为了更好的和 tick 联系起来。另外,并不是真的没有 tick 了,只是在系统空闲的时候停掉 tick 一段时间。使能了动态时钟之后,周期时钟的开关就由 idle 进程控制,当满足条件时就可以停掉 tick 若干时间,这个流程如下:
cpu_idle_loop -> tick_nohz_idle_enter -> __tick_nohz_idle_enter -> tick_nohz_stop_sched_tick
最后,通过 tick_nohz_stop_sched_tick 停止掉若干 tick。
static void cpu_idle_loop(void) { while (1) { /* 开始进入 NO_HZ idle */ tick_nohz_idle_enter(); while (!need_resched()) { check_pgt_cache(); rmb(); if (cpu_is_offline(smp_processor_id())) arch_cpu_idle_dead(); local_irq_disable(); arch_cpu_idle_enter(); if (cpu_idle_force_poll || tick_check_broadcast_expired()) { cpu_idle_poll(); } else { if (!current_clr_polling_and_test()) { stop_critical_timings(); rcu_idle_enter(); arch_cpu_idle(); WARN_ON_ONCE(irqs_disabled()); rcu_idle_exit(); start_critical_timings(); } else { local_irq_enable(); } __current_set_polling(); } arch_cpu_idle_exit(); } /* 有其它进程运行,退出 NO_HZ,恢复之前的周期时钟 */ tick_nohz_idle_exit(); schedule_preempt_disabled(); } }
停掉 tick 若干时间,那么这个若干时间是怎么得来的呢?系统此时处于空闲状态只有 idle 进程在运行,还要处理可能产生的中断,但是无法获知除了定时器中断以外的其它中断何时产生,而定时器第一个即将到期的中断时间是可以得到的,在这个时间到期之前都可以停掉 tick,由此得到需要停掉的 tick 数。另外,停掉 tick 的时间不能超过 clock_event_device 的 max_delta_ns,不然可能会造成 clocksource 的溢出。
回顾一下 tick 事件产生时的工作流程。首先是中断处理函数 exynos4_mct_tick_isr 被运行,它会调用 clock_event_device 的 event_handler,即 hrtimer_interrupt,hrtimer_interrupt 会调用 __run_hrtimer 处理到期的 hrtimer, tick_sched_timer 这个 hrtimer 的回调函数 tick_sched_timer 被调用,tick_sched_timer 会把下次唤醒时间设置为 tick_period,相当于恢复了周期时钟。如果没有别的进程需要运行,恢复周期时钟的做法显然是不合理的,我们需要的是在第一个定时时间到来之前停止若干 tick。 通过前面的内容可以了解到 tick 产生时会触发 TIMER_SOFTIRQ 软中断,所以内核在软中断的 irq_exit 函数中做了些手脚,解决了刚才所说的周期时钟恢复的问题,流程如下:
irq_exit -> tick_irq_exit -> tick_nohz_irq_exit -> __tick_nohz_idle_enter -> tick_nohz_stop_sched_tick
看到了熟悉的身影 tick_nohz_stop_sched_tick,通过它又可以停掉若干的 tick。
tick_irq_exit 函数如下:
static inline void tick_irq_exit(void) { #ifdef CONFIG_NO_HZ_COMMON int cpu = smp_processor_id(); /* Make sure that timer wheel updates are propagated */ if ((idle_cpu(cpu) && !need_resched()) || tick_nohz_full_cpu(cpu)) { if (!in_interrupt()) tick_nohz_irq_exit(); } #endif }
如果有别的进程需要运行, need_resched()
就会为 1,就不能进入 tick_nohz_irq_exit 函数,也就无法停掉若干 tick,下次唤醒的时间就还是一个 tick_period。
没有使能 tickless,tick 周期性产生,如下图所示:
配置了 tickless,tick 会被停掉若干时间,变得没有规律,如下图所示: