前天我正在看代码的时候,一个同事跑过来问我,“/proc/cpuinfo中的CPU频率是怎么来的,我在跑performance测试的时候,通过/proc/cpuinfo 查看CPU频率只有1.2G,但是我的CPU最高频率可以到2.7G。”我让他去把cpufreq governor设置成performance之后,再去执行performance测试。虽然他在做出修改之后,performance测试没有问题了,不过他的第一个问题却让我心生疑问,就去查了一下相关代码。
在linux kernel中,可以通过以下三个接口获取CPU frequency信息
1) /proc/cpuinfo
2) /sys/devices/system/cpu/cpuX/cpufreq/scaling_cur_freq
3) /sys/devices/system/cpu/cpuX/cpufreq/cpuinfo_cur_freq
那么,这3个接口究竟反应的是什么样的频率信息呢?它们的值是否都相同呢?我们直接使用cat看看
在比较早期的kernel(3.16)版本中,设置当前频率为1.6G,通过以上3个接口查询的结果如下:
#cat /proc/cpuinfo |grep MHz
cpu MHz : 1600.000
cpu MHz : 1600.000
cpu MHz : 1600.000
cpu MHz : 1600.000
#cat /sys/devices/system/cpu/cpuX/cpufreq/scaling_cur_freq
1600000
1600000
1600000
1600000
#cat /sys/devices/system/cpu/cpuX/cpufreq/cpuinfo_cur_freq
1600000
1600000
1600000
1600000
从上面的结果可以看出,在3.16 kernel上,以上3个接口读出来的CPU frequency都是相同的,就是我们设定的1.6GHz。
在Ubuntu1804-自带的kernel4.15上,同样设置频率为1.6G,也通过以上3个接口查询,结果如下:
#cat /proc/cpuinfo |grep MHz
cpu MHz : 1596.330
cpu MHz : 1596.307
cpu MHz : 1596.313
cpu MHz : 1596.330
#cat /sys/devices/system/cpu/cpuX/cpufreq/scaling_cur_freq
1596340
1596348
1596368
1596333
#cat /sys/devices/system/cpu/cpuX/cpufreq/cpuinfo_cur_freq
1600000
1600000
1600000
1600000
以上测试结果表明:
1)只有cpuinfo_cur_freq的值跟我们的设定值一致,为1.6GHz;
2)scaling_cur_freq和/proc/cpuinfo 多次查询出来的结果都不太一样,而且跟设定值有一个小的误差;
以上测试说明了不同版本的kernel也会有不同,我们去kernel change log中找跟这些改动相关的patch,主要有:
1)跟scaling_cur_freq改动相关的patch:commit 4815d3c56d1e10449a44089a47544d9ba84fad0d
cpufreq: x86: Make scaling_cur_freq behave more as expected
该patch通过APEF和MPERF计算CPU在一段时间内的平均频率,能够摒除CPU进入到idle state的影响。
使用APERF-MPERF的方式,不管CPU 频率是由HW-controlled,还是firmware-controlled,抑或是由linux cpufreq subsystem控制,甚至是没有frequency控制的情况下,user通过scaling_cur_freq sysfs接口都能获得有一个有意义的频率信息。
2)跟/proc/cpuinfo改动相关的patch:commit 7d5905dc14a87805a59f3c5bf70173aac2bb18f8
x86 / CPU: Always show current CPU frequency in /proc/cpuinfo
在这个patch之前,/proc/cpuinfo中的频率信息,只是一个象征意义,根据平台实现不同,可能是cpufreq governor之前写下去的requested cpufreq,并不表示CPU实际的运行频率。这个patch也是使用APERF和MPERF来计算CPU频率,像scaling_cur_freq一样能够反映CPU的实际频率信息。
最后,我们来看一下,有了这些patch(kernel 4.15)之后,以上这3个接口具体是如何实现的。
我以acpi_cpufreq driver为例进行分析,我们先来看一下跟这三个接口相关的,acpi_cpufreq driver的一些关键的callback是如何实现的:
1)频率切换是通过target/target_index还是setpolicy来完成呢?
acpi_cpufreq只能在fixed predefined frequency point中间调整,使用target_index callback完成实际的频率切换,setpolicy callback为NULL;
2)acpi_cpufreq_driver.get = get_cur_freq_on_cpu,通过读取MSR 0x199获取当前的requested frequency
/proc/cpuinfo 对应的code在arch/x86/kernel/cpu/proc.c中
show_cpuinfo
{
if(cpu_has(c, X86_FEATURE_TSC))
{
unsigned int freq = aperfmperf_get_khz(cpu); //1)先用aperf-mperf snapshot的方式,统计一段时间内CPU的average frequency
if(!freq)
{
freq = cpufreq_quick_get(cpu); //2)若1)失败,使用cpufreq_driver->get获取当前频率
{
if(cpufreq_driver && cpufreq_driver->setpolicy && cpufreq_driver->get) //对于acpi_cpufreq而言,setpolicy callback为NULL
{
freq = cpufreq_driver->get(cpu);
return freq;
}
policy = cpufreq_cpu_get(cpu); //3)返回scaling governor上一次选中的target frequency;
if(policy)
return policy->cur;
}
}
if(!freq)
freq = cpu_khz; //4)如果1、2、3都失败,返回TSC的cpu_khz信息;
seq_printf(m, "cpu MHz\t\t: %u.%03u\n", freq / 1000, (freq % 1000));
}
}
/sys/devices/system/cpu/cpuX/cpufreq/cpuinfo_cur_freq和scaling_cur_freq都在drivers/cpufreq/cpufreq.c中实现
show_cpuinfo_cur_freq
{
cur_freq = __cpufreq_get(policy->cpu);
{
cpufreq_driver->get(cpu);
//即get_cur_freq_on_cpu
}
}
从MSR 0x199中获取该core的requestedFID-VID,再根据FID-VID去freq_table中查其相应的frequency
get_cur_freq_on_cpu(int cpu)
{
struct cpufreq_policy *policy;
struct acpi_cpufreq_data *data;
u32 val;
policy = cpufreq_cpu_get_raw(cpu);
data = policy->driver_data;
//暂存的OS request frequency;
cached_freq = policy->freq_table[to_perf_data(data)->state].frequency;
//读MSR 0x199
val = get_cur_val(cpumask_of(cpu), data);
{
u32 val = drv_read(data, mask);
{
struct acpi_processor_performance *perf = to_perf_data(data);
struct drv_cmd cmd =
{
.reg = &perf->control_register, //注意这里读的是control register,即MSR 0x199
.func.read = data->cpu_freq_read,
};
smp_call_function_any(mask, do_drv_read, &cmd,1);
}
return val;
}
//把FID-VID转换成frequency,去policy->freq_table中找MSR 199中FID-VID对应的pos,返回其frequency
freq = extract_freq(policy, val);
{
struct acpi_cpufreq_data *data = policy->driver_data;
switch(data->cpu_feature)
{
case SYSTEM_INTEL_MSR_CAPABLE:
case SYSTEM_AMD_MSR_CAPABLE:
return extract_msr(policy,val);
{
struct acpi_processor_performance *perf;
perf = to_perf_data(data);
msr = val;
cpufreq_for_each_entry(pos, policy->freq_table)
{
if(msr == perf->states[pos->driver_data].status)
return pos->frequency;
}
return policy->freq_table[0].frequency;
}
case SYSTEM_IO_CAPABLE:
return extract_io(policy,val);
default:
return 0;
}
}
}
show_scaling_cur_freq
{
freq = arch_freq_get_on_cpu(policy->cpu); //1)通过APERF-MPERF获取CPU频率
{
if(!boot_cpu_has(X86_FEATURE_APERFMPERF))
return 0;
if(aperfmperf_snapshot_cpu(cpu, ktime_get(),true))
return per_cpu(samples.khz, cpu);
msleep(APERFMPERF_REFRESH_DELAY_MS);
smp_call_function_single(cpu, aperfmperf_snapshot_khz, NULL,1);
return per_cpu(samples.khz, cpu);
}
if(!freq) //2)如果1)失败,通过cpufreq_driver->get callback获取CPU频率信息
{
if(cpufreq_driver && cpufreq_driver->setpolicy && cpufreq_driver->get) //2.1)对于acpi_cpufreq driver而言,setpolicy callback为空,从这里跳出去;
freq = cpufreq_driver->get(cpu);
}
if(!freq) //3)返回scaling governor上一次选择的target frequency作为CPU频率
freq = policy->cur;
}
来highlight一下结论:
MSR 0x199是per-core的,每个core都可以根据自己的load,计算一个target frequency,通过MSR 0x199写下去;但是我们知道,多个core的情况下,如果只有一个VRM,需要对多个core 的target frequency进行处理,取一个合适的频率作为实际的频率。如果有APERF-MPERF的话,那么可以通过/proc/cpuinfo 和scaling_cur_freq接口获得CPU实际频率,否则就只能看到各个core的requested frequency,而实际的频率可能跟我们读到的不一样。