在嵌入式设备中对于高功耗都避之若浼。IOT 物联网,手机等更是看中设备的电池使用时间。省电节约功耗基本从考虑降低频率(手机CPU 的大小核),关闭暂未使用模块,睡眠等方向考虑。 在kernel 中就有tickless timer,通过在OS IDLE 时减少scheduling-clock ticks,节省功耗。下面主要分析kernel-4.9.198 Idle dynticks system(tickless idle)。
1. Base
kernel Timer system 中常见有如下方法管理:
- 周期时钟 (schedule-clock interrupts, CONFIG_HZ_PERIODIC=y)
- idle 时忽略timer tick (tickless idle, CONFIG_NO_HZ_IDLE=y or CONFIG_NO_HZ=y, kernel default choose)
- idle 时或者只有一个task run 时不需要调度,忽略CPU 的调度时钟滴答(CONFIG_NO_HZ_FULL=y)
在kernel/time/Kconfig 可以看见其配置信息。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22config NO_HZ_IDLE
bool "Idle dynticks system (tickless idle)"
select NO_HZ_COMMON
help
This option enables a tickless idle system: timer interrupts
will only trigger on an as-needed basis when the system is idle.
This is usually interesting for energy saving.
Most of the time you want to say Y here.
config NO_HZ_FULL
bool "Full dynticks system (tickless)"
# We need at least one periodic CPU for timekeeping
select NO_HZ_COMMON
select RCU_NOCB_CPU
select VIRT_CPU_ACCOUNTING_GEN
select IRQ_WORK
help
Adaptively try to shutdown the tick whenever possible, even when
the CPU is running tasks. Typically this requires running a single
task on the CPU. Chances for running tickless are maximized when
the task mostly runs in userspace and has few kernel activity.
Tick,即周期性产生的 timer 中断事件,可用于系统时间管理、进程信息统计、低精度 timer 处理等等。低分辨率定时器
低分辨率定时器是基于 HZ 来实现的,精度为 1/HZ。对定时器精度要求不高的内核模块还在大量使用低分辨率定时器,例如 CPU DVFS,CPU Hotplug 等。内核通过 time_list 结构体来描述低分辨率定时器。
高精度定时器
高精度定时器可以提供纳秒级别的定时精度,以满足对时间精度要求严格的内核模块,例如音频模块,内核通过 hrtimer 结构体来描述高精度定时器。在系统启动的开始阶段,高精度定时器只能工作在低精度周期模式,在条件满足之后的某个阶段就会切换到高精度单触发模式。在系统切换为高精度timer 后,通过one_shot 模拟周期timer,为系统提供周期tick。
tickless
动态时钟,并不是真正没有tick, 只是在idle 时停掉tick 一段时间。
2. tickless 何时启动?
通过搜索tickless 关键函数tick_nohz_stop_sched_tick()
的调用, tickless 启动主要有两个时机点:
- cpu idle 时
- irq exit 时,尝试继续tickless
1 | /* sched tick emulation and no idle tick control/stats*/ |
2.1. idle 时启动tickless
kernel 总所周知有一个pid=1 的init 进程, 其实还有一个pid=0 的idle 进程。当CPU 进入此低优先级的idle 进程后,我们可以认定此时CPU 是处于idle 状态。 因此,在该进程中可以尝试停掉一段时间的tick。callstack 如下:
cpu_idle_loop ->
tick_nohz_idle_enter ->
__tick_nohz_idle_enter ->
tick_nohz_stop_sched_tick
从cpu_idle_loop()
可以看出进入idle 进程 尝试开启tickless, 之后让CPU 进入低功耗cpuidle_idle_call()
, 之后若有调度事件 则退出tickless, tick_nohz_idle_exit()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23/* kernel-4.9.198, remove some code */
static void cpuidle_idle_call(void)
{
/* default idle */
__asm__ volatile ("wfi');
}
static void cpu_idle_loop(void)
{
int cpu = smp_processor_id();
while (1) {
tick_nohz_idle_enter();
while (!need_resched()) {
local_irq_disable();
arch_cpu_idle_enter();
cpuidle_idle_call();
arch_cpu_idle_exit();
}
tick_nohz_idle_exit();
}
}
在__tick_nohz_idle_enter()
中设定idle_entrytime, idle_active 状态, 并且判断是否可以stop idle tick, 其依据为:
- cpu online
- no need re-schedule
- no local softirq pending
- disable nohz full, or do timer cpu is not current cpu
1 | static ktime_t tick_nohz_start_idle(struct tick_sched *ts) |
tickless 核心关键函数落在tick_nohz_stop_sched_tick()
,其基本流程是是否能进入tickless。若可以,则计算出合适的ticks 并调用h
rtimer_start()或
tick_program_event()`对timer 重新编程。在后面小节tickless 停掉tick 数确定 会较详细分析该函数。
2.2. irq_exit 时尝试启动tickless
当有one shot timer中断到来时,我们在 tick_nohz_handler() intr servive 中设定下一次tick唤醒,这样在没有其他中断,进程执行时,也要被唤醒,这样我们的timer 又变成了周期timer。最好的应该是kernel 进行检查是否需要继续进行tickless 操作,因此在irq_exit 中有如下操作:
irq_exit ->
tick_irq_exit ->
tick_nohz_irq_exit->
tick_nohz_stop_sched_tick
补充:
irq 在arm 处理流程大致为:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15void handle_IRQ(unsigned int irq, struct pt_regs *regs)
{
__handle_domain_irq(NULL, irq, false, regs);
}
int __handle_domain_irq(struct irq_domain *domain, unsigned int hwirq,
bool lookup, struct pt_regs *regs)
{
irq_enter();
...
/* call irq intr func */
generic_handle_irq(irq);
irq_exit();
}
irq_enter(), irq_exit() 就会去做我们的软中断,generic_handle_irq() 实作HW 中断。
3. tickless 停掉tick 数确定
tickless 能停掉的ticks 怎样确认了?
在只有idle 进程运行时,cpu 需要处理的还有不定的HW 中断等,但是发生时间不能预测。但是第一个即将到期的中断时间是可以知道的,在这个时间到期之前都可以停掉 tick,由此得到需要停掉的tick 数。
另外,停掉 tick 的时间不能超过 clock_event_device 的 max_delta_ns,不然可能会造成 clocksource 的溢出。
如果停掉的ticks 是在下一个tick_period 则不用stop tick。
1 | static ktime_t tick_nohz_stop_sched_tick(struct tick_sched *ts, |
4. tickless 何时停止?
在cpu_idle_loop 循环中判断有新的进程被唤醒时退出tickless 模式,可参见前面cpu_idle_loop()
函数,恢复到tick_period。
tick_nohz_idle_exit->
tick_nohz_restart
在函数中,会判断高低分辨 timer,若是high resolution timer 则设定one shot 下一次中断来的expires, 反之低分辨率timer 则使用tick_program_event()
重新设定clockevent 。1
2
3
4
5
6
7
8
9
10
11
12
13static void tick_nohz_restart(struct tick_sched *ts, ktime_t now)
{
hrtimer_cancel(&ts->sched_timer);
hrtimer_set_expires(&ts->sched_timer, ts->last_tick);
/* Forward the time to expire in the future */
hrtimer_forward(&ts->sched_timer, now, tick_period);
if (ts->nohz_mode == NOHZ_MODE_HIGHRES)
hrtimer_start_expires(&ts->sched_timer, HRTIMER_MODE_ABS_PINNED);
else
tick_program_event(hrtimer_get_expires(&ts->sched_timer), 1);
}
5. tickless 对中断的影响
由于动态时钟出现,jiffies 是滞后的,其一般是在恢复周期时钟时更新。如果进入中断时需要访问jiffies,那么数据是不准确的。因此,进入中断irq_enter, tick_check_idle() 被调用,以此来更新jiffies值。
1 | static void tick_nohz_stop_idle(struct tick_sched *ts, ktime_t now) |
Reference
Linux时间子系统之八:动态时钟框架(CONFIG_NO_HZ、tickless)