本文会基于Android Pixel4(android linux kernel 4.14 ARMv8(AArch64)),重点分析schedutil这个cpufreq的governor,同时简单回顾CPU电源管理的常见手段以及cpufreq的框架,以便更加完整和清晰的分析schedutil的核心逻辑和来龙去脉。即本文会围绕以下问题展开:

一、CPU电源管理简介

如果我们的能源是无限制的,那可能也不需要做现在这样复杂的电源管理控制,尤其是在嵌入式设备如手机上,在追求极致性能的同时,还要追求续航时间,二者是一对相互约束的矛盾体,需要软硬件紧密配合以满足用户越发苛刻的性能和功耗的需求。

CPU是设备的控制核心,它的电源管理是整个SOC电源管理非常重要的一环。常见的CPU电源管理设计,主要也是围绕静态功耗和动态功耗的设计和优化展开:

1)静态功耗:ASIC集成电路的最基本单元是晶体管,我们的SOC在待机时仍然会有漏电流产生,从而引起静态功耗的消耗,得益于晶圆厂SOC制程(工艺)不断改进,如相比7nm FinFET,台积电5nm EUV工艺能效提升15%,功耗减少30%。所以从这个角度来讲,CPU要降低静态功耗,需要ASIC工程师把面积做小,同时需要晶圆厂把工艺制程不断改进。

2)动态功耗:这块比较复杂,软硬件配合的有cpufreq、cpuidle、锁机制底层实现等,ASIC方面有clock gating、不同power domain等,我们简单介绍下。

cpu内核怎么和引脚连接(cpufreqschedutil原理剖析)(1)

二、cpufreq总体框架简介

cpu内核怎么和引脚连接(cpufreqschedutil原理剖析)(2)

介绍完CPU电源管理的常用手段,我们简单回顾下cpufreq的总体架构和各模块的作用,以及体现的软件思想。更细节的大家可以参考蜗窝科技的分析文章。

1. 主要模块介绍2. 软件思想介绍

cpufreq体现的一些软件设计思想,在Linux kernel中其他模块中也很常见,值得我们学习和借鉴:

1) 内核提供机制,用户空间实现策略。比如后文我们会介绍的User Space这个governor就是由用户程序决策,直接控制频率切换策略。

2) 内核提供机制,特定的策略由governor去实现,用户空间修改governor的参数,例如后文我们介绍的schedutil如果scheduler选择用WALT去跟踪task和CPU负载就有一些参数会在用户空间调节。

3. 调频方法和影响CPU频率的因素

cpu内核怎么和引脚连接(cpufreqschedutil原理剖析)(3)

从内核文档,我们可以知道:

无论是采取哪种方法,一般CPU的工作频率都会和CPU工作承担的负载正相关。但也会有有一些其他因素影响CPU的频率,主要是thermal和用户态策略,thermal主要功能是一个热保护,过高的CPU温度除了引起如烫手等因素外,累积效应还有可能引起如晶体管击穿导致SOC不能正常工作,甚至会导致引起一些其他严重的后果(类似于若干年前的三星手机的电池爆炸的)。

thermal一般会通过内核态接口cpufreq_update_policy来实现频率控制。而用户态应用程序会通过sysfs去控制。最终都是影响policy的{max, min}。

4. 常见governor三、 schedutil的总体框架1. 设计思想

该governor由Rafael J. Wysocki在2016年提出,最终合入到linux kernel 4.7中。它体现主要的思想和相比其他governor的改进点如下:

一句话总结就是:通过它,让scheduler和调频建立起更加紧密的联系,同时提升了性能和功耗表现(调频上升和下降的曲线都更加陡峭,频率更快的上升或者下降到目标频率)。

无论是PELT还是WALT,schedutil都可以与之协同工作,Pixel4内核使用了PELT,其他很多手机产商一般会沿用SOC产商的策略,采用WALT。究竟哪个能发挥更好的调频效果,现在也没有定论,看各自的调优手段了,不在本次讨论的范畴。

2. 软件框图

cpu内核怎么和引脚连接(cpufreqschedutil原理剖析)(4)

其实起初内核社区有两个基于scheduler的调频方案:

scheduler,一方面使用WALT或PELT去跟踪TASK负载进而产生CPU负载用来作为目标频率计算的输入,一方面在负载变化时调用cpufreq_update_util调用hook触发schedutil工作,进而进行频率决策和后续的频率调整。

SchedTune,则是通过cgroup进程组的方式,影响CPU负载,进而影响CPU调频的频率选择,是一种性能提升技术。它还可以通过prefer_idle标志位,告诉scheduler用户空间进程组希望在调度时更倾向于功耗还是性能(值为1表示更倾向于性能),在linux kernel mainline 5.4中这块功能将被UClamp(Utilization Clamping)所代替,有兴趣的同学可以自行研究。

tunables,提供了sysfs接口在用户空间灵活配置,根据细分场景进行调优:

schedutil最终决策需要进行调频后,有2个路径:

最后,从软件框图我们可以看到,无论是CFS、RT、DL task本质上都是通过cpu utilization去影响频率的计算和选择。对DL task,认为是负载时未知的(目前还不能追踪utilization),会将CPU频率设置到policy->max,对CFS/RT Task则会根据都会根据utilization(runqueue的avg.util_avg成员,即struct sched_avg的util_avg成员)去计算频率。其实一开始RT/DL task的调度策略是一样的,都是简单粗暴地设置到最高频,后来通过sched/rt: add utilization trackingsched: cpufreq: use PELT rt_rq as estimate of required RT CPU capacity,这两个patch对RT task做了改进。

至此,schedutil和相关联模块的简单分析就结束,接下来我们继续分析它的实现细节。

四、schedutil的核心逻辑

本文的代码分析会侧重分析核心逻辑,有些细节不做展开,防止胡子眉毛一把抓。我们主要关注这个governor是怎么初始化和启动的,何时触发schedutil工作,以及schedutil的决策逻辑最终怎么去做频率切换的。

1. schedutil的初始化和启动

governor的初始化和启动,是在cpufreq_set_policy函数中来做的。一般发生在governor切换的时候。Governor的停止和去初始化,是逆过程,不再展开。

(1)sugov_init函数

一般初始化函数都是要准备一些资源,我们schedutil也不例外。

这个函数每个policy都会执行到,可能会同时执行,产生竞争条件,最终会在cpufreq_init_governor函数中被调用。简化代码如下:

cpu内核怎么和引脚连接(cpufreqschedutil原理剖析)(5)

cpu内核怎么和引脚连接(cpufreqschedutil原理剖析)(6)

1)调用框架提供的cpufreq_enable_fast_switch接口尝试使能快速切换功能

2)给struct sugov_policy类型的指针分配内存并初始化

3)如果不支持快速切换,则调用sugov_kthread_create走slow_path创建相关进程和workqueue相关的work,包括sugov_work和sugov_irqwork。

4)tunables的初始化,包括创建sysfs接口,供用户态空间进行调优。

(2)sugov_start函数

该函数最主要的作用就是要把最终调频决策和执行调频动作的核函数注册到scheduler的hook中。该函数会在框架的cpufreq_start_governor函数中先被调用,紧接着还要调用sugov_limits函数进行限频的检查。简化码如下:

cpu内核怎么和引脚连接(cpufreqschedutil原理剖析)(7)

cpu内核怎么和引脚连接(cpufreqschedutil原理剖析)(8)

cpu内核怎么和引脚连接(cpufreqschedutil原理剖析)(9)

1)sstruct sugov_policy *sg_policy变量的初始化。

2)所有共用policy都要循环执行struct sugov_cpu *sg_cpu变量的初始化

3)调用cpufreq_add_update_util_hook接口,向scheduler注册回调函数,在cpufreq_update_util中被调用。当多个CPU共用一个policy时,则将sugov_update_shared函数注册给scheduler;反之,则将sugov_update_single函数注册给scheduler。

(3)sugov_limits函数

该函数完成频率限制的检查,简化代码如下:

cpu内核怎么和引脚连接(cpufreqschedutil原理剖析)(10)

1)是否支持频率快速切换(支持在中断上下文中调用)的区别在于调用时间信息的函数和频率限制后最终调用的调频接口的区别:

2)调用sugov_track_cycles函数更新sgpolicy的curr_cycles(累加值)和last_cyc_update_time(最后一次时间戳)成员。

3)根据当前policy->{max, min}的限制和当前频率信息,看是否需要将当前频率调整到policy->{max, min}当中的一个。

2. schedutil调频的触发时机

CFS负载变化或者RT/DL任务状态更新时就可以启动调频,这几个scheduler类会调用cpufreq_update_util函数(也就是调用注册进来的hook函数)触发schedutil工作。每个CPU最终会回调到sugov_update_shared或者sugov_update_single函数当中的一个。

因为是从scheduler里直接调用下来的,最终执行频率切换时无论慢速路径触发kthread运行,还是快速路径简单写寄存器都不会占用过多的调度开销,这也是schedutil能被社区接纳的一个很重要的原因。

触发的具体时机如下:

1)当一个task被唤醒的时候(对应try_to_wake_up函数被调用),如果使用WALT且满足PL(Predict Load);

2)在系统tick到来(对应scheduler_tick函数被调用),如果使用WALT且满足ED(Early Detection)时;

3)如果使用WALT且WALT窗口滑动时(对应walt_irq_work函数被调用);

4)DL任务状态更新时(对应update_curr_dl函数被调用);

5)RT任务更新时(对应cpufreq_update_util函数被调用);

6)当CFS更新RQ的负载时(对应cfs_rq_util_change函数被调用)

7)当一个设置了in_iowait的CFS(TODO)任务进入Runqueue并且是Schedtune类型的进程组设置了prefer_idle标识时。

3. schedutil调频的决策和频率切换

schedutil调频被触发工作以后,剩下的工作就交由sugov_update_shared或者sugov_update_single这两个核心函数去完成了。

(1)sugov_update_single函数

此函数主要针对该cluster里只有一个CPU的情形。简化代码如下:

cpu内核怎么和引脚连接(cpufreqschedutil原理剖析)(11)

cpu内核怎么和引脚连接(cpufreqschedutil原理剖析)(12)

cpu内核怎么和引脚连接(cpufreqschedutil原理剖析)(13)

1)调用sugov_set_iowait_boost设置iowait_boost相关变量

2)调用sugov_should_update_freq判断是否真的需要频率切换,不需要更新则直接返回,需要切换则主要是如下情况:

3)如果是DL任务,则直接将目标频率设置成硬件支持的最大值。

4)如果是其他调度类的任务,则继续处理5-11步的处理。

5)调用sugov_get_util获取CPU统计的负载,会同时考虑WALT/PELT的处理方式,同时会Schedtune类型的进程组设置的boost_value考虑进来。

6)对sgpolicy的hispeed_util做归一化处理。归一化到算力最高的CPU上,同时考虑tunables的hispeed_freq的设置(hispeed_freq默认为0)。

7)计算平均算力,不使用WALT特性则不做处理。

8)如果scheduler设置SCHED_CPUFREQ_IOWAIT标志,主要依据第一步的iowait_boost变量提升负载,进而提升频率,起到加速执行的效果。

9)如果是WALT特性,则tunables的hispeed_load、pl变量设置以及sg_policy的平均算力等,进一步提升负载,进而提升频率加速task的执行。

10)调用get_next_freq函数,将负载数据转换成平台支持的目标频率,具体计算过程我们后续单独展开。

11)调用sugov_update_commit函数,执行频率切换,我们后续单独展开。

(2)sugov_update_shared函数

针对该cluster里有多个CPU的情形,因为多个CPU复用该函数,需要要考虑并发的问题,与sugov_update_single函数在于:

4. 目标频率的计算-get_next_freq函数

该函数在sugov_update_single函数中被调用,适用于cluster里只有一个CPU的情形,或者说一个cpu独享一个policy的时候。简化代码如下所示:

cpu内核怎么和引脚连接(cpufreqschedutil原理剖析)(14)

1)对ARM体系结构而言,有目标频率的计算公式如下:

freqnext = 1.25 * freqmax * util / capacitymax

util / capacitymax ,也就是当前CPU使用率与该CPU的最大算力的比值等于0.8为临界点,当该系数小于 0.8会降频;超过0.8则会升频。

为什么会取1.25这个系数或者说将0.8作为临界点呢,其实这里0.8的意思是“当负载达到最大算力的80%就要选择升频了”,这样就有一定的提前量(猜测负载还会继续升高),如果这个值设置过小就会显得“太激进”,设置过大就显得“太保守”。当然这个值的设定也不是完全靠经验决定的,而是在这个feature的patch提出后经过linux kernel pm maillist成员的讨论、实测以及借鉴schedfreq的类似设定来决定的,更细节信息请参考patch讨论。

2)如果使用计算出来的目标频率和已缓存的频率相等,直接使用sg_policy->next_freq的值,省去了driver的映射查找的过程时间消耗。

5. 目标频率的计算- sugov_next_freq_shared函数

该函数在sugov_update_shared函数中被调用,适用于cluster里有多个CPU的情形,或者说多个cpu共享一个policy的时候。简化代码如下:

cpu内核怎么和引脚连接(cpufreqschedutil原理剖析)(15)

cpu内核怎么和引脚连接(cpufreqschedutil原理剖析)(16)

cpu内核怎么和引脚连接(cpufreqschedutil原理剖析)(17)

1)遍历所有共享policy的CPU(一个cluster里面的CPU),取CPU负载最重的那个CPU作为该cluster的所有CPU的目标频率。当然,也会对CPU的负载进行iowait_boost和WALT特有的boost(如果开启WALT的话)。

2)如果最后一次的CPU负载更新和频率更新的时间间隔足够长的话,很可能这个CPU处在idle态,该CPU略过(同时将iowait_boost相关状态清楚)直接考虑下一个CPU了。

3)取所有CPU中,使用率与CPU算力的比值最大的那个组合。

4)这部分代码的作用和sugov_update_single函数代码片段相同,不再赘述。

6. 频率切换的执行-sugov_update_commit函数

cpu内核怎么和引脚连接(cpufreqschedutil原理剖析)(18)

cpu内核怎么和引脚连接(cpufreqschedutil原理剖析)(19)

1)目标频率与当前频率相等,直接返回

2)schedutil为频率上下调分别设置了两个时间间隔,这个通过sysfs供用户空间配置,必须要大于这两个时间间隔才去执行频率切换动作。

3)如果支持快速切换功能,直接调用cpufreq_driver_fast_switch接口让driver切换频率(比如可以通过简单的读寄存器)

4)如果不支持快速切换功能,则需要去irq_work上去排队。通过依次调用sugov_irq_worksugov_work这两个函数去执行调频请求,最后会调用__cpufreq_driver_target接口去完成调频切换的动作。

还有一些细节的代码,我们没有展开,不过不影响我们对核心逻辑的分析。

至此全文分析完毕

,