/proc/cpuinfo显示频率原理

  1. 背景
  2. 学习
    1. 计算频率原理

背景

众所周知,现在/proc/cpuinfo可以显示cpu的运行频率,而虚机中/proc/cpuinfo显示的cpu频率一直是一个固定的值

学习

计算频率原理

先不管虚拟化那一套,就先看看内核是怎么计算cpu频率的呢?

先了解一下相关背景:

APERF:是一个MSR寄存器,它总是以当前的CPU的实际工作频率按比例增加数值,每个cpu上都有一个。比如一个CPU主频2.6GHz,然后睿频到了3.4GHz,那APERF就按照3.4GHz的比例去增加数值。

MPERF:也是一个MSR寄存器,也是每个CPU都有一个。这个是按照最大CPU频率按比例增加,一般也就是CPU的主频,也就是标称频率,也就是p0状态下的频率。比如一颗cpu的主频是2.6GHz,那它就是去按照2.6GHz的比例去增加。

到这差不多就能猜到了,就是通过这俩msr去计算cpu运行频率的。当然,其实频率这块还涉及到别的msr,以及怎么设置调节频率这块,还和驱动有关系,有些麻烦,到时候再细看一下再单开一篇,现在只看这俩msr足够。

再看一下是怎么计算cpu频率的,以/proc/cpuinfo为例,现在的/proc/cpuinfo已经支持显示实时cpu频率了(有时候不会,后面会说到)

##cat /proc/cpuinfo时,会调到show_cpuinfo,直接来看显示cpu频率的部分
##其实就在这,如果开了tsc的话,就对freq格式化输出,那freq其实就是频率咯
##freq是arch_freq_get_on_cpu返回的,看一下这个函数
if (cpu_has(c, X86_FEATURE_TSC)) {
    unsigned int freq = arch_freq_get_on_cpu(cpu);
    seq_printf(m, "cpu MHz\t\t: %u.%03u\n", freq / 1000, (freq % 1000));
}

##看一下怎么返回cpu频率的
unsigned int arch_freq_get_on_cpu(int cpu)
{
    ##这个cpu_samples就是一个percpu的aperfmperf结构体
    ##里面主要有acnt mcnt aperf mperf
	struct aperfmperf *s = per_cpu_ptr(&cpu_samples, cpu);
	unsigned int seq, freq;
	unsigned long last;
	u64 acnt, mcnt;

    ##如果没有aperfmperf这个feature就跳到fallback了,这个后面还会提到
	if (!cpu_feature_enabled(X86_FEATURE_APERFMPERF))
		goto fallback;

    ##这里就是一些赋值acnt mcnt last_update,这些作用后面说
	do {
		seq = raw_read_seqcount_begin(&s->seq);
		last = s->last_update;
		acnt = s->acnt;
		mcnt = s->mcnt;
	} while (read_seqcount_retry(&s->seq, seq));

	/*
	 * Bail on invalid count and when the last update was too long ago,
	 * which covers idle and NOHZ full CPUs.
	 */
    ##如果mcnt为0或jiffies-last大于MAX_SAMPLE_AGE也去fallback
    ##到这就知道了,last_update也就是last其实就是上次更新时的jiffies
    ##而MAX_SAMPLE_AGE时HZ/50,一般内核默认时250HZ,也就是MAX_SAMPLE_AGE时20ms
    ##也就是如果这次采样和上次采样间隔超过20ms,就不继续计算了,去fallback了
	if (!mcnt || (jiffies - last) > MAX_SAMPLE_AGE)
		goto fallback;

    ##最后走到这是真正的计算频率,其实就是cpu_khz*acnt/mcnt
    ##cpu_khz其实一般就是cpu主频了,也是tsc的频率
    ##acnt mcnt是什么呢?先说答案,其实就是两次采样间隔中aperf mperf的差值,后面还会看到
    ##所以频率计算公式是什么?主频*(Δaperf)/(Δmperf),联系之前说的aperf mperf的作用就很好理解了
    ##也许有人会有疑问,为什么直接不是返回mperf就好了,还得乘主频再除aperf呢?aperf不就是按主频增长的嘛?
    ##原因在于aperf和mperf并不是按照频率增长,而是和频率成一个倍数关系的增长
    ##所以aperf mperf只能反应一个倍数关系,而不能反应真实的频率,所以最后还要乘tsc,因为tsc一直是按照主频增长的
	return div64_u64((cpu_khz * acnt), mcnt);

fallback:
    ##这里就是没有成功计算频率,但是最要返回一个值
    ##先尝试去cpufreq_quick_get获取,貌似要走驱动那块来着,有些忘了,写下一篇时再看
	freq = cpufreq_quick_get(cpu);
    ##如果quick也没获取到,那就只能返回cpu_khz了,一般也就是主频值了
	return freq ? freq : cpu_khz;
}


##到这基本就完全知道是怎么计算频率的了,但是还有个问题没有解释清楚
##acmt mcnt是谁更新的?什么时候更新的?其实就在下面这个函数
void arch_scale_freq_tick(void)
{
	struct aperfmperf *s = this_cpu_ptr(&cpu_samples);
	u64 acnt, mcnt, aperf, mperf;
    ##还是必须要支持X86_FEATURE_APERFMPERF才行
	if (!cpu_feature_enabled(X86_FEATURE_APERFMPERF))
		return;

    ##读MSR_IA32_APERF的值到aperf中
	rdmsrl(MSR_IA32_APERF, aperf);
    ##读MSR_IA32_MPERF到mperf中
	rdmsrl(MSR_IA32_MPERF, mperf);
    ##这就出来啦,下面四条连起来看
    ##acnt和mcnt就是当前采样的aperf和mperf的值和上次采样的值的差
    ##然后把cpu_samples中的aperf mperf重新赋值成当前采样的值
	acnt = aperf - s->aperf;
	mcnt = mperf - s->mperf;

	s->aperf = aperf;
	s->mperf = mperf;
    ##然后更新last_update为当前jiffies,更新acnt和mcnt
	raw_write_seqcount_begin(&s->seq);
	s->last_update = jiffies;
	s->acnt = acnt;
	s->mcnt = mcnt;
	raw_write_seqcount_end(&s->seq);

	scale_freq_tick(acnt, mcnt);
}

##那这个arch_scale_freq_tick函数什么时候会调用到呢?搜一下就看到了
scheduler_tick

所以这块应该就可以恍然大明白了,假如一个默认启动的内核时250HZ的,那每隔4ms就会发起一次tick,就会调到scheduler_tick,然后scheduler_tick里就会调到arch_scale_freq_tick去采样,然后更新acnt mcnt,当用户执行cat /proc/cpuinfo的时候就自然直接利用acnt和mcnt计算出当前频率了

但是,其实还有两个问题存在:1.有些cpu上并不显示实时频率,为什么?2.虚拟化情况下为什么只返回主频?

1.有些cpu上并不显示实时频率,为什么?

实测发现,有些cpu上cat /prco/cpuinfo的话并不会显示实时的cpu频率,而是一个固定的值

其实arch_freq_get_on_cpu函数中也说明了,如果设置了nohz或者nohz_full或者cpu一直在idle的话就可能因为采样间隔太大跳到fallback去了

其实还是没有负载搞得,只要你把cpu的负载稍微打上去一点,就能显示了

2.虚拟化情况下为什么只返回主频?

其实这个就更好理解了,可以看一下lscpu是不是没有aperfmperf这个feature,因为没有这个feature,也就是不支持X86_FEATURE_APERFMPERF,那也就不能去读aperf mperf去计算cpu频率了,所以只能返回固定的值了,也就是cpu主频

这其实又能引申出一个问题,虚拟化情况下不能访问apermperf的话,怎么估算cpu频率呢?其实应该可以使用perf之类的工具,比如使用perf state sleep 1会得到cycles和task-clock,cycles就是这一秒cpu运行了多少个周期,task-clock就是实际运行了多少时间。比如我得到了cycles是800000,task-clock是1ms。那么就可以计算800000/1*1000/10^9=0.8GHz,后面两个是换算单位,*1000是换算成秒,/10^9是换算成GHz


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 857879363@qq.com