3、CPU频率管理:cpufreq框架、governor、驱动接口

好,咱们今天聊聊CPU频率管理。说白了,就是让CPU在“跑得快”和“省点电”之间找个平衡。我刚开始做嵌入式Linux的时候,总觉得这玩意儿就是个黑盒——系统自己会调频率,我管它干嘛?直到有一次,产品在高温环境下频繁死机,我才意识到,频率管理没做好,后果很严重。

Linux内核里负责这事儿的,就是cpufreq框架。它是个中间层,上面连着用户空间的策略,下面连着具体的硬件驱动。今天我就带你把它拆开看看。

3.1 cpufreq框架:三层架构

cpufreq框架分三层,我习惯这么记:

  • 核心层(cpufreq core):提供统一的接口,管理策略、通知事件。
  • 策略层(governor):决定“什么时候该调频”。
  • 驱动层(cpufreq driver):真正去操作硬件寄存器,改变频率。

你想想看,这三层各司其职。核心层就像个调度中心,governor是决策者,驱动是执行者。我在项目中遇到过,有人直接在驱动里写死频率切换逻辑,结果governor完全失效——这就是没理解分层的好处。

3.2 Governor:五种策略,各有千秋

Linux内核默认提供了几种governor,我挑最常用的几个说说。

Governor 策略 适用场景
performance 始终跑最高频率 性能优先,不在乎功耗
powersave 始终跑最低频率 省电模式,比如待机
ondemand 根据CPU负载动态调频 大多数通用场景
conservative 类似ondemand,但调频更平缓 对功耗敏感,且不希望频繁跳频
userspace 由用户程序控制频率 定制化需求,比如跑特定算法

performancepowersave 是两个极端。我刚开始做产品时,图省事直接用了performance,结果电池续航惨不忍睹。后来换成ondemand,效果好了很多。

ondemand 是默认的governor,也是用得最多的。它的原理很简单:每隔一段时间(比如10ms)检查CPU负载,如果负载高就升频,负载低就降频。但这里有个坑——调频太频繁会导致额外的功耗和延迟。我曾经在一个项目中,ondemand的采样间隔设得太小,结果CPU一直在升频降频之间来回跳,反而更费电。

注意:ondemand的调频策略是“快升慢降”。也就是说,负载一上来,频率立刻拉高;负载降下去,频率慢慢回落。这样做是为了避免频繁切换,但如果你对响应时间有要求,可能需要调整参数。

userspace 这个governor比较特殊。它把频率控制权完全交给了用户空间。你可以写个脚本,或者用cpufreq-set命令手动设置频率。我记得有个做音频处理的同事,他需要CPU稳定在某个频率以保证音频流不卡顿,就是用userspace搞定的。

3.3 cpufreq驱动接口:怎么跟硬件打交道

governor决定了“什么时候调”,驱动决定了“怎么调”。驱动层需要实现一组回调函数,注册到cpufreq核心层。我挑几个关键的说:

  • init():初始化,比如获取支持的频率列表。
  • verify():验证用户设置的频率是否合法。
  • target():设置目标频率。这是最核心的函数。
  • get():读取当前频率。

驱动里最麻烦的就是 target() 函数。因为改变CPU频率不是简单写个寄存器就完事了,还得考虑电压、时钟源、PLL锁相环的稳定时间等等。我举个例子:

static int my_cpufreq_target(struct cpufreq_policy *policy,
                             unsigned int target_freq,
                             unsigned int relation)
{
    struct cpufreq_freqs freqs;
    unsigned int new_freq;

    // 1. 找到最接近的目标频率
    new_freq = cpufreq_frequency_table_target(policy, target_freq, relation);

    // 2. 通知核心层:频率要变了
    freqs.old = policy->cur;
    freqs.new = new_freq;
    cpufreq_freq_transition_begin(policy, &freqs);

    // 3. 实际设置硬件频率
    // 这里需要操作时钟控制器、PLL等寄存器
    // 注意:可能需要先调电压,再调频率
    my_set_cpu_freq(new_freq);

    // 4. 通知核心层:频率已变
    cpufreq_freq_transition_end(policy, &freqs);

    return 0;
}

嗯,这里要注意一点:调频的顺序很重要。如果是升频,通常先调高电压,再升频率;如果是降频,先降频率,再降电压。顺序搞反了,CPU可能会直接挂掉。我曾经在调试一个ARM平台时,就是因为升频时先改了频率没来得及调电压,导致系统瞬间死机——那感觉,真是欲哭无泪。

3.4 避坑指南:我踩过的几个坑

做cpufreq驱动,有几个地方特别容易出问题:

  • 频率表要合法:不是所有频率硬件都支持。驱动里必须提供一张频率表,核心层会用它来校验。
  • 调频期间要关中断:有些硬件在调频过程中不能响应中断,否则会出乱子。驱动里需要处理好临界区。
  • 多核CPU要小心:每个核可以有自己的频率,也可以共享一个频率。这取决于硬件设计。如果是共享的,调频时得通知所有核。

我曾经在一个四核平台上,因为没处理好共享频率的问题,导致一个核调频时,其他核还在跑着,结果系统直接panic。后来查了好久才发现,是驱动里少了一个同步锁。

3.5 小结

cpufreq框架其实不复杂,核心就是三层:核心层、governor、驱动。governor决定了策略,驱动负责执行。你只要理解了这层关系,写驱动或者调优策略,心里就有底了。

下一章,我会聊聊更底层的CPU idle管理——当CPU没事干的时候,怎么让它睡得更香。敬请期待。