1. 前言
本文以menu governor为例,进一步理解cpuidle framework中governor的概念,并学习governor的实现方法。
在当前的kernel中,有2个governor,分别为ladder和menu(蜗蜗试图理解和查找,为什么会叫这两个名字,暂时还没有答案)。ladder在periodic timer tick system中使用,menu在tickless system中使用。
注:有关periodic timer tick和tickless的知识,可参考本站时间子系统的系列文章。
2. 背后的思考
本节的内容,主要来源于drivers/cpuidle/governors/menu.c中的注释。
governor的主要职责,是根据系统的运行情况,选择一个合适idle state(在kernel的标准术语中,也称作C state)。具体的算法,需要基于下面两点考虑:
1)切换的代价
进入C state的目的,是节省功耗,但CPU在C state和normal state之间切换,是要付出功耗上面的代价的。这最终会体现在idle state的target_residency字段上。
idle driver在注册idle state时,要非常明确state切换的代价,基于该代价,CPU必须在idle state中停留超过一定的时间(target_residency)才是划算的。
因此governor在选择C state时,需要预测出CPU将要在C state中的停留时间,并和备选idle state的target_residency字段比较,选取满足“停留时间 > target_residency”的state。
2)系统的延迟容忍程度
备选的的C state中,功耗和退出延迟是一对不可调和的矛盾,电源管理的目标,是在保证延迟在系统可接受的范围内的情况下,尽可能的节省功耗。
idle driver在注册idle state时,会提供两个信息:CPU在某个state下的功耗(power_usage)和退出该state的延迟(exit_latency)。那么如果知道系统当前所能容忍的延迟(简称latency_req),就可以在所有exit_latency小于latency_req的state中,选取功耗最小的那个。
因此,governor算法就转换为获取系统当前的latency_req,而这正是pm qos的特长。
任务1,menu governor从如下几个方面去达成:
前面讲过,menu governor用于tickless system,简化处理,menu将“距离下一个tick来临的时间(由next timer event测量,简称next_timer_us)”作为基础的predicted_us。
当然,这个基础的predicted_us是不准确的,因为在这段时间内,随时都可能产生除next timer event之外的其它wakeup event。为了使预测更准确,有必要加入一个校正因子(correction factor),该校正因子基于过去的实际predicted_us和next_timer_us之间的比率,例如,如果wakeup event都是在预测的next timer event时间的一半时产生,则factor为0.5。另外,为了更精确,menu使用动态平均的factor。
另外,对不同范围的next_timer_us,correction factor的影响程度是不一样的。例如期望50ms和500ms的next timer event时,都是在10ms时产生了wakeup event,显然对500ms的影响比较大。如果计算平均值时将它们混在一起,就会对预测的准确性产生影响,所以计算correction factor的数据时,需要区分不同级别的next_timer_us。同时,系统是否存在io wait,对factor的敏感度也不同。基于这些考虑,menu使用了一组factor(12个),分别用于不同next_timer_us、不同io wait的场景下的的校正。
最后,在有些场合下,next_timer_us的预测是完全不正确的,如存在固定周期的中断时(音频等)。这时menu采用另一种不同的预测方式:统计过去8次停留时间的标准差(stand deviation),如果小于一定的门限值,则使用这8个停留时间的平均值,作为预测值。
任务2,延迟容忍度(latency_req)的估算,menu综合考虑了两种因素,如下:
1)由pm qos获得的,系统期望的,CPU和DMA的延迟需求。这是一个硬性指标。
2)基于这样一个经验法则:越忙的系统,对系统延迟的要求越高,结合任务1中预测到的停留时间(predicted_us),以及当前系统的CPU平均负荷和iowaiters的个数(get_iowait_load函数获得),算出另一个延迟容忍度,计算公式(这是一个经验公式)为:
predicted_us / (1 + 2 * loadavg +10 * iowaiters)
这个公式反映的是退出延迟和预期停留时间之间的比例,loadavg和iowaiters越大,对退出延迟的要求就越高奥。
最后,latency_req的值取上面两个估值的最小值。
Menu Governor:一种有效的 CPU 空闲状态选择算法
Menu Governor 是一种 CPU 空闲状态选择算法,旨在平衡能效和性能。它设计用于在具有不同工作负载和性能要求的系统上有效工作。
Menu Governor 基于以下三个主要因素运行:
- **能量平衡点:**此因素考虑进入和退出空闲状态相关的能量成本。Governor 估计抵消此能量成本所需的闲置持续时间,称为“目标驻留时间”。它使用校正因子根据历史数据调整其预测。
/*
* Which bucket should we use for the correction factor index, given
* the expected duration and the number of I/O waiters?
*/
static inline int which_bucket(u64 duration_ns, unsigned int nr_iowaiters)
{
int bucket = 0;
/*
* We keep two groups of stats; one with no
* IO pending, one without.
* This allows us to calculate
* E(duration)|iowait
*/
if (nr_iowaiters)
bucket = BUCKETS/2;
if (duration_ns < 10ULL * NSEC_PER_USEC)
return bucket;
if (duration_ns < 100ULL * NSEC_PER_USEC)
return bucket + 1;
if (duration_ns < 1000ULL * NSEC_PER_USEC)
return bucket + 2;
if (duration_ns < 10000ULL * NSEC_PER_USEC)
return bucket + 3;
if (duration_ns < 100000ULL * NSEC_PER_USEC)
return bucket + 4;
return bucket + 5;
}
- **性能影响:**Menu Governor 考虑进入空闲状态的潜在性能影响。它采用一个性能乘数,该乘数包含每 CPU 负载平均值和等待 I/O 的进程数等因素。较高的性能乘数会提高选择更深 C 状态的门槛,从而减轻对性能的影响。
/*
* Return a multiplier for the exit latency that is intended
* to take performance requirements into account.
* The more performance critical we estimate the system
* to be, the higher this multiplier, and thus the higher
* the barrier to go to an expensive C state.
*/
static inline int performance_multiplier(unsigned int nr_iowaiters)
{
/* for IO wait tasks (per cpu!) we add 10x each */
return 1 + 10 * nr_iowaiters;
}
- **延迟容忍度:**Governor 考虑用户配置的延迟要求和每个空闲状态的退出延迟。它选择满足延迟约束且同时最大程度降低能耗的空闲状态。
/*
* Try detecting repeating patterns by keeping track of the last 8
* intervals, and checking if the standard deviation of that set
* of points is below a threshold. If it is... then use the
* average of these 8 points as the estimated value.
*/
static unsigned int get_typical_interval(struct menu_device *data)
{
int i, divisor;
unsigned int min, max, thresh, avg;
uint64_t sum, variance;
Menu Governor 为不同的场景维护一组校正因子。这些校正因子根据测量的闲置持续时间和预测持续时间进行更新。Governor 还利用可重复间隔检测器来识别闲置持续时间中的模式并改进其预测。
Menu Governor 的主要优点包括:
-
**能效:**它通过选择在考虑性能要求的同时提供最佳节能效果的空闲状态来有效降低能耗。
-
**性能感知:**Governor 考虑空闲状态的性能影响并相应地调整其选择,确保性能不受损害。
-
**适应性:**Menu Governor 持续更新其校正因子并利用历史数据来改进其预测,使其能够适应不断变化的工作负载和系统条件。
Menu Governor 已被证明是 CPU 空闲状态选择的有效选择,可在节能和性能之间取得平衡。它的适应性和处理不同工作负载的能力使其成为各种系统的合适选择。
3. 代码分析
理解menu governor背后的思考之后,再去看代码,就比较简单了。
3.1 初始化
首先,在init代码中,调用cpuidle_register_governor,注册menu_governor,如下:
static struct cpuidle_governor menu_governor = {
.name = "menu",
.rating = 20,
.enable = menu_enable_device,
.select = menu_select,
.reflect = menu_reflect,
};
/**
* init_menu - initializes the governor
*/
static int __init init_menu(void)
{
return cpuidle_register_governor(&menu_governor);
}
postcore_initcall(init_menu);
menu_governor 结构体定义了名为 "menu" 的 CPU 空闲状态调度器(governor)。这个结构体是 struct cpuidle_governor 类型的,表示一个 CPU 空闲状态调度器的属性。
name: 调度器的名字,这里为 "menu"。
rating: 调度器的评分,这个值用于在多个调度器之间进行选择,这里设定为 20。
enable: 指向一个函数的指针,用于启用调度器,这里为 menu_enable_device。
select: 指向一个函数的指针,用于选择下一个空闲状态,这里为 menu_select。
reflect: 指向一个函数的指针,用于反映(reflect)某个状态的改变,这里为 menu_reflect。
init_menu 函数是初始化 "menu" 调度器的函数。
cpuidle_register_governor(&menu_governor) 用于注册 "menu" 调度器。将 menu_governor 结构体传递给 cpuidle_register_governor 函数,将 "menu" 调度器添加到系统的调度器列表中。
postcore_initcall(init_menu) 是一个内核初始化阶段的回调函数,将 init_menu 函数注册为 postcore_initcall 回调,确保在内核的初始化过程中调用该函数。这是在内核初始化过程的末尾调用的函数,用于执行与调度器初始化相关的任务。
总体而言,这段代码注册了一个名为 “menu” 的 CPU 空闲状态调度器,设置了相关的属性和回调函数,并在系统初始化的末尾注册了初始化函数以确保 “menu” 调度器被正确初始化。
3.2 enable API
enable API负责governor运行前的准备动作,由menu_enable_device实现:
/**
* menu_enable_device - 扫描 CPU 的状态并进行设置
* @drv: cpuidle 驱动程序
* @dev: CPU 设备
*
* 该函数用于激活并设置 CPU 的 Menu governor。在这里,它主要完成了以下任务:
* 1. 将 CPU 对应的 menu_device 结构体初始化为零。
* 2. 在校正因子为零的情况下(例如,初次初始化或 CPU 热插拔),将校正因子初始化为统一因子。
*
* 参数:
* - drv: cpuidle 驱动程序,表示关联的 cpuidle 驱动
* - dev: CPU 设备,表示关联的 cpuidle 设备
*
* 返回值:
* 返回 0 表示成功激活并设置 Menu governor。
*/
static int menu_enable_device(struct cpuidle_driver *drv,
struct cpuidle_device *dev)
{
struct menu_device *data = &per_cpu(menu_devices, dev->cpu);
int i;
// 将 menu_device 结构体的内存清零,即初始化为零
memset(data, 0, sizeof(struct menu_device));
/*
* 如果校正因子为零(例如,初次初始化或 CPU 热插拔),实际上我们希望
* 从一个统一的因子开始。
*/
for(i = 0; i < BUCKETS; i++)
data->correction_factor[i] = RESOLUTION * DECAY;
return 0;
}
该函数是 Menu governor 中的一个重要部分,负责激活和设置 CPU 的 Menu governor。在函数中,主要的数据结构是 menu_device,它包含了与 CPU 相关的状态信息。以下是对函数的详细分析:
struct menu_device *data = &per_cpu(menu_devices, dev->cpu);
使用 per_cpu 宏获取与当前 CPU 相关的 menu_device 结构体的引用。
memset(data, 0, sizeof(struct menu_device));
通过 memset 将 menu_device 结构体的内存清零,即初始化为零。这确保了结构体的所有字段都被正确初始化。
校正因子的初始化:
通过 for 循环,将 correction_factor 数组中的每个元素初始化为 RESOLUTION * DECAY。这是在校正因子为零的情况下进行的初始化操作。
返回值:
返回 0,表示成功激活并设置 Menu governor。
总体而言,该函数的作用是在 CPU 启用 Menu governor 时进行初始化,确保相关的数据结构和参数被正确设置,为 Menu governor 的后续运行做好准备。
/**
* struct menu_device - Menu governor 数据结构
* @needs_update: 表示是否需要更新状态的标志,非零表示需要更新
* @tick_wakeup: 表示是否是由时钟中断唤醒的标志,非零表示是
* @next_timer_ns: 下一个计时器事件的时间,以纳秒为单位
* @bucket: 表示存储桶的索引,用于存储持续时间的信息
* @correction_factor: 校正因子数组,用于纠正预测误差
* @intervals: 存储过去若干次计时器间隔的数组,用于检测重复模式
* @interval_ptr: intervals 数组的指针,指示下一个要更新的位置
*
struct menu_device {
int needs_update;
int tick_wakeup;
u64 next_timer_ns;
unsigned int bucket;
unsigned int correction_factor[BUCKETS];
unsigned int intervals[INTERVALS];
int interval_ptr;
};
这是 Menu governor 中使用的数据结构 menu_device。它包含了用于管理 CPU 空闲状态的多个字段,这些字段在算法中扮演着重要的角色
3.2 select接口
governor的核心API,根据系统的运行情况,选择一个合适的C state。由menu_select接口实现,逻辑如下:
/**
* menu_select - 选择下一个要进入的空闲状态
* @drv: 包含状态数据的 cpuidle 驱动程序
* @dev: CPU 设备
* @stop_tick: 是否停止时钟中断的指示
*
* 该函数用于选择下一个要进入的空闲状态,根据 Menu governor 的算法和给定的约束条件进行选择。
*
* 参数:
* - @drv: cpuidle 驱动程序,包含了状态数据和配置信息。
* - @dev: CPU 设备,表示当前的 CPU。
* - @stop_tick: 一个布尔指针,指示是否停止时钟中断。
*
* 返回值:
* - 返回选择的空闲状态的索引。
*/
static int menu_select(struct cpuidle_driver *drv, struct cpuidle_device *dev,
bool *stop_tick)
{
struct menu_device *data = this_cpu_ptr(&menu_devices);
s64 latency_req = cpuidle_governor_latency_req(dev->cpu);
u64 predicted_ns;
u64 interactivity_req;
unsigned int nr_iowaiters;
ktime_t delta, delta_tick;
int i, idx;
// 如果需要更新状态,则调用 menu_update 函数更新
if (data->needs_update) {
menu_update(drv, dev);
data->needs_update = 0;
}
// 获取当前 CPU 上 I/O 等待的数量
nr_iowaiters = nr_iowait_cpu(dev->cpu);
// 获取典型空闲间隔的纳秒表示
predicted_ns = get_typical_interval(data) * NSEC_PER_USEC;
// 如果典型间隔大于阈值 RESIDENCY_THRESHOLD_NS
if (predicted_ns > RESIDENCY_THRESHOLD_NS) {
unsigned int timer_us;
// 获取距离最近计时器的时间
delta = tick_nohz_get_sleep_length(&delta_tick);
if (unlikely(delta < 0)) {
delta = 0;
delta_tick = 0;
}
data->next_timer_ns = delta;
data->bucket = which_bucket(data->next_timer_ns, nr_iowaiters);
// 计算纠正后的最小预期空闲间隔
timer_us = div_u64((RESOLUTION * DECAY * NSEC_PER_USEC) / 2 +
data->next_timer_ns *
data->correction_factor[data->bucket],
RESOLUTION * DECAY * NSEC_PER_USEC);
// 选择最小的预期空闲间隔
predicted_ns = min((u64)timer_us * NSEC_PER_USEC, predicted_ns);
} else {
// 处理没有计时器事件的情况
data->next_timer_ns = KTIME_MAX;
delta_tick = TICK_NSEC / 2;
data->bucket = which_bucket(KTIME_MAX, nr_iowaiters);
}
// 处理一些特殊情况,直接返回状态 0
if (unlikely(drv->state_count <= 1 || latency_req == 0) ||
((data->next_timer_ns < drv->states[1].target_residency_ns ||
latency_req < drv->states[1].exit_latency_ns) &&
!dev->states_usage[0].disable)) {
*stop_tick = !(drv->states[0].flags & CPUIDLE_FLAG_POLLING);
return 0;
}
// 处理时钟中断已停止的情况
if (tick_nohz_tick_stopped()) {
if (predicted_ns < TICK_NSEC)
predicted_ns = data->next_timer_ns;
} else {
// 使用性能乘数和 latency_req 计算最大退出延迟
interactivity_req = div64_u64(predicted_ns,
performance_multiplier(nr_iowaiters));
if (latency_req > interactivity_req)
latency_req = interactivity_req;
}
// 选择最低功耗的空闲状态,满足约束条件
idx = -1;
for (i = 0; i < drv->state_count; i++) {
struct cpuidle_state *s = &drv->states[i];
if (dev->states_usage[i].disable)
continue;
if (idx == -1)
idx = i; /* 第一个启用的状态 */
// 如果目标空闲时间大于预测时间,处理不同的情况
if (s->target_residency_ns > predicted_ns) {
if ((drv->states[idx].flags & CPUIDLE_FLAG_POLLING) &&
s->exit_latency_ns <= latency_req &&
s->target_residency_ns <= data->next_timer_ns) {
predicted_ns = s->target_residency_ns;
idx = i;
break;
}
if (predicted_ns < TICK_NSEC)
break;
if (!tick_nohz_tick_stopped()) {
predicted_ns = drv->states[idx].target_residency_ns;
break;
}
if (drv->states[idx].target_residency_ns < TICK_NSEC &&
s->target_residency_ns <= delta_tick)
idx = i;
return idx;
}
if (s->exit_latency_ns > latency_req)
break;
idx = i;
}
if (idx == -1)
idx = 0; /* 没有启用的状态,必须使用 0 */
// 处理不停止时钟中断的情况
if (((drv->states[idx].flags & CPUIDLE_FLAG_POLLING) ||
predicted_ns < TICK_NSEC) && !tick_nohz_tick_stopped()) {
*stop_tick = false;
// 如果选择的状态是轮询状态或者预期的空闲时间较短,不停止时钟中断
if (idx > 0 && drv->states[idx].target_residency_ns > delta_tick) {
for (i = idx - 1; i >= 0; i--) {
if (dev->states_usage[i].disable)
continue;
idx = i;
if (drv->states[i].target_residency_ns <= delta_tick)
break;
}
}
}
return idx;
}
3.3 reflect接口
/**
* menu_reflect - 记录需要更新数据结构的信息
* @dev: CPU 设备
* @index: 实际进入状态的索引
*
* 注意:这里的速度很重要,因为此操作将增加总体退出延迟。
*/
static void menu_reflect(struct cpuidle_device *dev, int index)
{
struct menu_device *data = this_cpu_ptr(&menu_devices);
// 记录最后进入的状态的索引
dev->last_state_idx = index;
// 标记需要更新数据结构
data->needs_update = 1;
// 检查是否有时钟中断唤醒了 CPU
data->tick_wakeup = tick_nohz_idle_got_tick();
}
该函数用于记录需要更新数据结构的信息,以及记录最后进入的空闲状态的索引。这个信息是为了保持数据结构的一致性,以及在某些情况下可能出现的时钟中断唤醒的情况。