前文参考
[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
2
3
4
5
6
7
8
9
10
11static int32_t next_timeout(void)
{
struct _timeout *to = first(); //从timeout list中取出最近一个要到期的timeout,timeout list的管理可以参考[2]
int32_t ticks_elapsed = elapsed();
//如果没有timeout要处理就按照最大时间设置产生Timer中断
int32_t ret = to == NULL ? MAX_WAIT
: CLAMP(to->dticks - ticks_elapsed, 0, MAX_WAIT);
return ret;
}
Idle时
当idle线程运行时,说明现在系统无其它线程运行可以进入idle,在Tickless电源管理配置打开后,会通过下面流程设置下一次中断时间,并在设置好中断Timer后让CPU进入idle状态,详细可以参考价[3]pm_save_idle
->pm_system_suspend
->z_set_timeout_expiry
->sys_clock_set_timeout
1
2
3
4
5
6
7
8
9
10
11
12static void pm_save_idle(void)
{
#ifdef CONFIG_PM
//获取下一次中断产生的时间,z_get_next_timeout_expiry就是直接调用的next_timeout
int32_t ticks = z_get_next_timeout_expiry();
_kernel.idle = ticks;
if (pm_system_suspend(ticks) == PM_STATE_ACTIVE) {
k_cpu_idle();
}
#endif
}
下面只列出pm_system_suspend
中与Tickless相关的代码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18enum pm_state pm_system_suspend(int32_t ticks)
{
...
if (ticks != K_TICKS_FOREVER) {
__ASSERT(z_power_state.min_residency_us >=
z_power_state.exit_latency_us,
"min_residency_us < exit_latency_us");
//这里的ticks就是下一次中断的时间,因为从idle状态唤醒需要时间,所以设置到timer的时间要将唤醒的延迟时间扣除
//z_set_timeout_expiry调用的就是sys_clock_set_timeout,设置下一次中断时间
z_set_timeout_expiry(ticks -
k_us_to_ticks_ceil32(z_power_state.exit_latency_us), true);
}
...
}
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中取的是2781
2#define COUNTER_MAX 0x00ffffff
#define MAX_TICKS ((COUNTER_MAX / CYC_PER_TICK) - 1)
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18static uint32_t elapsed(void)
{
uint32_t val1 = SysTick->VAL; /* A */
uint32_t ctrl = SysTick->CTRL; /* B */
uint32_t val2 = SysTick->VAL; /* C */
//判断Timer在elapsed中到期,如果发生到期,需要将上一次的计时last_load加入到overflow_cyc中
if ((ctrl & SysTick_CTRL_COUNTFLAG_Msk)
|| (val1 < val2)) {
overflow_cyc += last_load;
/* We know there was a wrap, but we might not have
* seen it in CTRL, so clear it. */
(void)SysTick->CTRL;
}
return (last_load - val2) + overflow_cyc;
}
执行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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59sys_clock_isr()
{
if (TICKLESS) {
dticks = (cycle_count - announced_cycles) / CYC_PER_TICK;
announced_cycles += dticks * CYC_PER_TICK;
//此处中断传入278tick
sys_clock_announce(dticks);
}
}
void sys_clock_announce(int32_t ticks)
{
k_spinlock_key_t key = k_spin_lock(&timeout_lock);
announce_remaining = ticks;
//第一次announce_remaining为278
//第二次announce_remaining为278
//第三次announce_remaining为278
//第四次announce_remaining为166
//第一次时first()->dticks为1000, 不会进入循环
//第二次时first()->dticks为722, 不会进入循环
//第三次时first()->dticks为444, 不会进入循环
//第四次时first()->dticks为166, 进入循环
while (first() != NULL && first()->dticks <= announce_remaining) {
struct _timeout *t = first();
int dt = t->dticks;
//达到Tickless要求
curr_tick += dt;
announce_remaining -= dt;
t->dticks = 0;
remove_timeout(t);
k_spin_unlock(&timeout_lock, key);
t->fn(t);
key = k_spin_lock(&timeout_lock);
}
//第一次时first()->dticks 1000扣除278,剩余为722
//第二次时first()->dticks 722扣除278,剩余为444
//第三次时first()->dticks 444扣除278,剩余为166
if (first() != NULL) {
first()->dticks -= announce_remaining;
}
//更新系统累计执行的tick数
curr_tick += announce_remaining;
announce_remaining = 0;
//第一次next_timeout()拿到的是722,被设置下去,大于278, 因此任然按278tick定时
//第二次next_timeout()拿到的是444,被设置下去,大于278, 因此任然按278tick定时
//第三次next_timeout()拿到的是166,小于于278, 按166tick定时
sys_clock_set_timeout(next_timeout(), false);
k_spin_unlock(&timeout_lock, key);
}
对于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
2
3
4
5
6
7
8
9
10
11
12
13
14
15static int32_t next_timeout(void)
{
struct _timeout *to = first();
int32_t ticks_elapsed = elapsed();
int32_t ret = to == NULL ? MAX_WAIT
: CLAMP(to->dticks - ticks_elapsed, 0, MAX_WAIT);
#ifdef CONFIG_TIMESLICING
//当有时间片存在时,且剩余的时间片小于下一次tickless的tick数,则以较小的slice_ticks作为timer定时设置
if (_current_cpu->slice_ticks && _current_cpu->slice_ticks < ret) {
ret = _current_cpu->slice_ticks;
}
#endif
return ret;
}
非时间片到引发调度时需要为下一个线程重设时间片, 设置时间片时会加上从上一次中断到现在经过的tick数1
2
3
4
5
6
7void z_reset_time_slice(void)
{
if (slice_time != 0) {
_current_cpu->slice_ticks = slice_time + sys_clock_elapsed();
z_set_timeout_expiry(slice_time, false);
}
}
通过下图分析原因:
假设一个时间片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中直接加上了这一部分来达到相同的效果。