1. 介绍
Linux系统初始化时会为每个cpu创建一个idle线程,当没有其他进程需要运行的时候,便运行idle线程。
对于不同的功耗及恢复时间的要求,可以根据芯片硬件支持的情况定义多种idle状态,这些状态按功耗从低到高(对应着恢复时间从少到多)排列,利用linux提供的cpuidle框架,用户选用不同的idle策略。这么做的目的就是尽可能在不影响性能的前提下,减少功耗。
在ARM64架构中,至少会提供一个wfi的idle状态,有些芯片可能还会提供core下电的idle状态。当CPU idle时,根据预测的idle时间、功耗受益大小、恢复的时间长短,选用一个idle状态,比如进入wfi,关掉CPU的arch timer以便降低功耗,当有中断触发时,CPU又会恢复回来。
2. 框架
cpuidle框图
1)scheduler模块:位于kernel\sched\idle.c中,负责实现idle状态的选择、idle的进入等等。
2)cpuidle core模块:cpuidle core抽象出cpuidle device、cpuidle driver、cpuidle governor三个实体。
- 以函数调用的形式,向上层sched模块提供接口。
- 以sysfs的形式,向用户空间提供接口。
- 向下层的cpuidle drivers模块,提供统一的driver注册和管理接口。
- 向下层的cpuidle governors模块,提供统一的governor注册和管理接口。
3)cpuidle governors模块:提供多种idle的策略,比如menu/ladder/teo/haltpoll。位于governors/目录下。
4)cpuidle drivers模块:负责idle机制的实现,即:如何进入idle状态,什么条件下会退出。
3. 数据结构
cpuidle core抽象出了三个数据结构:
- cpuidle device:用于描述CPU核的cpuidle设备。
- cpuidle driver:用于描述CPU核的cpuidle驱动。
- cpuidle governor:主要根据cpuidle的device和driver状态来选择策略。
cpuidle数据结构
4. 初始化及工作流程
4.1 cpuidle初始化流程
以cpuidle-pcsi.c为例,整个cpuidle注册流程如下图:
cpuidle注册流程
cpuidle初始化包括governor注册、驱动注册和设备注册三部分,其中驱动和设备注册流程由架构相关的初始化流程执行。
1) governor注册
cpuidle governor在cpuidle驱动和设备之前注册,内核使用一个链表维护系统中所有已注册的governor。且在每个新governor被注册时都会将其rating值与已注册governor的rating值比较,并将rating值最高的governor做为当前governor。
当前新版内核一共支持ladder、menu、teo和haltpoll四种governor,它们都通过调用cpuidle_register_governor函数将自身注册到系统中。
- Haltpoll governor:它是用于优化虚拟机性能的一种cpuidle governor。其原理为当vcpu进入idle时,通过guest端执行poll操作,以避免使其陷入host中。它的优点是减少了vm切换和通过ipi唤醒vcpu的成本,但它也造成在guest睡眠时,host无法复用该vcpu对应的物理cpu,从而降低系统吞吐量的问题。
- Ladder governor:该governor通过cpu前一次idle状态的驻留时间是否超过该state延迟时间一个特定的值(promotion_time_ns),以及下一个state的延迟时间是否超过系统延迟容忍度,来确定是否需要提升idle state。由于该governor每次只能提升一个state,因此其state提升方式就像梯子一样逐级往上,这也是它的名字由来。它往往用于periodic timer tick system。
- Menu governor:直接选择可能满足需求的最深休眠态,就好像你拿着菜单(menu)选菜一样。如果深度的idle state更好,那么就会直接进入到深度的idle state。
- Teo governor:采用的策略跟menu governor一样,都是预测接下来会有多长时间能待在idle状态,然后据此选择合适的idle mode。不过它跟menu governor考虑多方因素的策略是不同的。teo的理念是,多数系统上CPU唤醒最频繁的唤醒源都是timer events,而不是设备中断(device interrupts)。timer中断的数量要比其他中断高几个数量级。所以只要依据timer event就可以做好预测工作了。
2) cpuidle driver注册
cpuidle驱动注册流程比较简单,它主要包含以下三部分内容
- idle state相关参数设置、以及可能的broadcast timer
- 若设置了local-timer-stop属性,则为每个cpu设置相应的broadcast timer
- 若为该driver指定了governor,则切换current governor
3) cpuidle device注册
cpuidle设备注册主要包括初始化一些参数值,将该设备添加到全局设备链表中,然后为其初始化sysfs属性和使能该设备。
注册之后,cpuidle设备、cpuidle驱动及governor之间建立起了连接,最终系统经由cpuidle framework,通过接口来调用下层的接口,进而完成具体的硬件操作。
4.2 cpuidle触发流程
Idle task通过cpu_startup_entry为入口,调用到cpuidle_framework,流程如下图:
cpuidle触发流程
cpu启动完成时,会通过cpu_startup_entry函数将其自身切换到idle线程。除此之外,当某个cpu上没有可运行线程时,也会切换idle线程(上流程没画出,后面梳理进程调度的时候再细讲)。切换idle线程后,最终都会执行idle线程的主函数do_idle,并最终通过该函数将cpu设置为特定的idle state。
其中governor中的select、reflect函数是cpuidle的核心功能,决定了cpuidle状态的选择策略。
5. 策略核心函数分析
5.1 select函数
governor的select,以menu_select为例:
menu_select函数
1)预期idle的predict时间的计算:
menu governor会将下一个tick到来的时间(next_timer_us)作为一个基础的predicted_us,并在这个基础上调整。
首先,因为predicted_us并不总是与next_timer_us直接相等,在等待下一个tick的过程很有可能被其他事件所唤醒,所以需要引入校正因子(correction factor)校正predicted_us。此校正因子从对应的bucket索引中选取。
menu governor使用了一个12组校正因子来预测idle时间,校正因子的值基于过去predicted_us和next_timer_us的比率,并且采用动态平均算法。
另外对于不同的next_timers_us,校正因子的影响程度是不一样的。对于不同的iowait场景,系统对校正因子也有着不同的敏感程度 。
随后尝试通过最近的8个过去的停留时间来查找重复间隔,如果他们的标准差小于一定阈值,则将这8个时间的平均值作为predicted_us。
最后取以上两个流程中的最小值。
2)系统延迟容忍时间的计算
对于系统容忍度,menu governor使用性能乘数(performance multiplier)、预计停留时间(predicted)和系统延迟需求(latency requirement)来找出最大退出延迟。
系统延迟容忍时间作为第一个系统延迟容忍度。另外一个系统iowait容忍度计算如下:predicted_us / (1 +10 * iowaiters)。iowaiters指当前cpu上iowait的进程数。
最后取前面两个系统容忍度中最小值,作为最小的系统容忍度。
3)idle state的选取
最后根据前面计算出来的两个因素来选取具体的idle state,将计算出的predicted_us与所有idle状态的停留时间进行比较,选择特定idle state的条件是相应的停留时间应小于predicted_us。
另外,将状态的exit_latency与系统的延迟要求进行比较。基于两个等待时间因素,选择适当的idle state。
5.2 reflect函数
menu_reflect函数
在cpu退出idle状态后,menu governor会将将上一次进入idle状态的数据更新menu driver中的相关数据,作为下一次select的参数。
5.3 update函数
下一次进入idle state选择流程时,会先触发更新需求,即进入到menu_update()中。
menu_update函数
1)idle的存留时间计算
在更新信息时,会尝试算出进入idle状态到被唤醒经历了多长时间。
如果cpu被tick唤醒,而且上次记录的next_timer_us大于了一个tick的时间,那么governor就假定cpu已经空闲了很长时间,则measured_us = 9 * MAX_INTERESTING / 10(INTERESTING=50000)
如果cpu退出了轮询状态,会导致选择该状态的空闲持续时间不准确,故将next_timer_us作为measured_us。
除此之外,measured_us将使用驱动中记录的上次idle状态中停留时间。
算出来之后再减去退出延迟,然后与next_timer_us取最小值,便得出了最终的measured_us。
2)校正因子的计算
接下来是计算下一次选择校正因子(correction factor)的值,将上一次的校正因子先衰减一次,然后加上一个predicted_us和next_timer_us的比值new_factor += RESOLUTION * measured_us / data->next_timer_us;(RESOLUTION=1024)
最后就可以将idle存留时间及校正因子更新到governor的驱动中。