前文参考
[1] Zephyr Tick Clock简介
[2] Zephyr内核Timeout模块简介
[3] Zephyr电源管理-Tickless Idle
先看wiki上对Tickless kernel的描述”A tickless kernel is an operating system kernel in which timer interrupts do not occur at regular intervals, but are only delivered as required”
操作系统内核Timer中断不是周期性的产生,只在需要的时候产生。当内核不是Tickless的情况下,每个Tick都会产生一次中断,内核在中断内进行Timeout和调度的相关处理。但实际上Timeout并不会频繁到每个Tick都需要处理,每个Tick都中断一次都做的是无用功,中断时产生的上下文切换就是无必要的开销。由于内核会管理所有的Timeout,因此内核自己知道下一次Timeout是什么时候,通过设置timer过期时间可以让Timer在下一次Timeout的时候再产生中断,这就叫需要的时候产生,避免了不必要的Tick中断,也就是我们说的Tickless。
Zephyr内核支援Tickless, 并基于Tickless提供以下两个功能
- 运行时:减少不必要的Tick中断
- idle时:SOC进入idle,需要运行时从idle中唤醒
Tickless的实现依赖于硬件Timer,本文以Cortex-M的systick timer为基础进行分析。
配置
系统要支持Tickless,必须要有硬件timer支持sys clock,通过下面配置项进行配置:CONFIG_SYS_CLOCK_EXISTS=y
配置启用系统时钟,kernel/Kconfig中是默认选中该项的CONFIG_CORTEX_M_SYSTICK=y
选择系统时钟Timer为systick,不同的平台会用不同的Timer,可以参考文件drivers/timer/Kconfig,里面包含select TICKLESS_CAPABLE
的timer就是支持Tickless的。CONFIG_TICKLESS_KERNEL=y
配置启用Tickless,实际上只要配置的Timer支持TICKLESS_CAPABLE,kernel/Kconfig检查到有TICKLESS_CAPABLE就会将CONFIG_TICKLESS_KERNEL配置起来
因此只要配置了CONFIG_CORTEX_M_SYSTICK=y
Zephyr就会以Tickless工作。CONFIG_SYS_CLOCK_HW_CYCLES_PER_SEC=600000000
配置硬件时钟,这里为600M, CPU的工作频率,也是systick的时钟频率CONFIG_SYS_CLOCK_TICKS_PER_SEC=10000
配置内核每秒有多少个Tick,一般情况下我们无需配置,在kernel/Kconfig对其有默认设置,非Tickless为100,Tickless情况下为10000,非Tickless的单个tick时间更短,原因后面会做说明。
在Tickless启用的情况下,600M的clock,一个tick有60000个硬件clock,对于精简指令系统来说一个tick可以执行60000条指令。
Tickless
Tickless就是在需要的时候产生中断,要实现这个目标需要以下两个支撑:
- Timer允许设置指定的时间产生中断
- 内核能够计算出何时需要产生中断
在Zephyr中是以tick为单位计算时间,在Tickless下硬件Timer的过期时间会跨多个Tick,因此更短的Tick可以提高精度,由于是多个Tick才会产生一次中断也不用担心Tick太短导致Timer中断变多。
计算何时需要产生中断
运行时
Zephyr在运行时,只有在处理内核对象注册Timeout的情况下才需要产生中断,从[2]中我们知道所有的timeout都被timeout list管理,因此通过timeout list就可以得到最近一次需要产生中断的时间。[2]中说明了这一流程:
内核对象通过z_add_timeout
将timeout加入到timeout list, sys_clock_announce90
中通过next_timeout()
获取到最近一次需要中断的时间再通过sys_clock_set_timeout90
设置下一次中断时间
1 | static int32_t next_timeout(void) |
Idle时
当idle线程运行时,说明现在系统无其它线程运行可以进入idle,在Tickless电源管理配置打开后,会通过下面流程设置下一次中断时间,并在设置好中断Timer后让CPU进入idle状态,详细可以参考价[3]pm_save_idle
->pm_system_suspend
->z_set_timeout_expiry
->sys_clock_set_timeout
1 | static void pm_save_idle(void) |
下面只列出pm_system_suspend
中与Tickless相关的代码
1 | enum pm_state pm_system_suspend(int32_t ticks) |
Systick设置中断时间
在非Tickless的情况下systick每个tick产生一次中断,在Ticless下Zephyr对systick进行了设计,让其可以支持在指定Tick数后产生中断,这就是前文提到的sys_clock_set_timeout
。
由于systick只有24bit,可计数clock为0xFFFFFF,一个tick有60000个硬件clock,因此systick一轮最多可以定时0xFFFFFF/60000 = 279 tick,在cortex_m_systick.c中取的是278
1 | #define COUNTER_MAX 0x00ffffff |
Systick到期时会自动加载LOAD寄存器进行下一轮计时产生中断sys_clock_isr
sys_clock_set_timeout
sys_clock_set_timeout有下面特性:
- idle情况,ticks为K_TICKS_FOREVER时,将停止systick timer
- 非idle情况下,当ticks大于MAX_TICKS时systick按MAX_TICKS定时
1 | void sys_clock_set_timeout(int32_t ticks, bool idle) |
在没有调用sys_clock_set_timeout
的情况下,每次中断时更新cycle_count
和announced_cycles
,每次中断后announced_cycles
和cycle_count
是相等的,在调用sys_clock_set_timeout
后cycle_count
被重新设置,以上流程如下图:
- 进入函数时,记录下当前systick值为val1
- 计算从最近一次中断到现在执行了多少个clock
- 根据设置的tick数计算下一次中断的时间
- 下一次中断的时间要做tick对齐得到delay
- 获取当前systick值val2, val2-val1就是函数执行时间
- 将delay设置到systick的load中开始计时
- 将cycle_count更新
关于elapsed
elapsed用于获取最近一次设置LOAD寄存器到当前执行了的clock数量
1 | static uint32_t elapsed(void) |
执行elapsed有两种情况:
- 在isr中执行,此时是timer到期,因此必定有COUNTFLAG标记
- 在thread中执行,在执行elapsed前都会锁中断,执行过程中及时timer到期也不会进入isr,timer到期有几种情况:
– 在A前到期:必定有COUNTFLAG标记
– 在A和B之间到期:必定有COUNTFLAG标记,且val1<val2
– 在B和C之间到期:没有COUNTFLAG标记,但val1val2,到期将在下一次执行时判断
硬件Timer定时长短的影响
通过sys_clock_set_timeout来设置期望的ticks后产生中断,从前文知道systick可以定时的最大长度为MAX_TICKS也就是MAX_TICKS=278tick。但如果需要的tickless的ticks数超过MAX_TICKS会怎么处理呢?
我们假设线程tickless要求的ticks为1000, 该情况下systick会以278tick最大定时长度定时产生中断,会每278tick产生一次tick中断,直到1000个tick执行完毕。
1 | sys_clock_isr() |
对于idle也类似,CPU每278个tick会从idle状态下唤醒,idle thread又重新设置systick,再进入idle。
由此可以看出当tick timer的定时长度比较小时,在Tickless过程中任然会产生中断有额外开销,而对于idle来说再期望的idle期间设备也会被多次唤醒。
为了解决该问题可以选择计数位数更多的硬件Timer或者降低硬件Timer的时钟,例如目前zephyr正在对rt系列的soc进行这方面的修改,将tick Timer换为32bit的GPT, 时钟将为32786Hz,最长的定时时间可以达到36小时。
时间片和Tickless
我们知道在没有Tickless的情况下,每个Tick都会产生中断,时间片会在每个中断中进行更新。但Tickless下只会在预定的tick到来后才会产生中断,此时可能已经超过了当前线程允许的时间片大小,因此在计算下一次tickless的tick时需要考虑当前时间片:
1 | static int32_t next_timeout(void) |
非时间片到引发调度时需要为下一个线程重设时间片, 设置时间片时会加上从上一次中断到现在经过的tick数
1 | void z_reset_time_slice(void) |
通过下图分析原因:
假设一个时间片4个tick
- A线程执行3个tick后,在a时刻切换到线程B运行,使用z_reset_time_slice重设thread B的时间片
- 由于重设时间片,原本再b要发生的时间片tickless中断不再发生
- 在c处发生其它tickless中断,此时线程B只执行了3个tick还不到一个时间片
- 在c处,送到sys_clock_announce的是dticks=6(包含了A执行了的3个时间片),
- 如果B第一次的_current_cpu->slice_ticks被设置为slice_time=4,那么此时z_time_slice会判断到dticks>_current_cpu->slice_ticks完成一个时间片,但实际线程B还没有执行完一个时间片
基于以上分析我们可知道:在时间片执行过程中切换线程后,下一次tick中断计算的tick数会包含上一个线程已经执行了的时间片,计算切换后线程运行的时间片应该扣出这一部分。在代码实现上为了简洁,就在current_cpu->slice_ticks中直接加上了这一部分来达到相同的效果。