news 2026/10/7 7:25:32

devfreq框架深度剖析:内核动态调频的原理与实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
devfreq框架深度剖析:内核动态调频的原理与实战

做功耗管理和内核驱动,devfreq 这层框架迟早要碰。它跟 cpufreq 很像,但更偏门、更容易让人绕晕。尤其在 DDR、GPU、NPU 这些非 CPU 设备的动态频率调节上,devfreq 几乎是绕不开的通用方案。我最早是在一个带 AI 加速器的平台上做内存带宽管控,被供应商的“魔改总线和自定义 governor”折磨了一整轮,后来才意识到,根子上就得先把 devfreq framework 吃透。这篇文章把 devfreq 的来龙去脉、核心对象、注册流程和调试经验整理出来,给正要入门或者已经被它坑过的内核工程师一个可以照着抄的路线。

1. devfreq 解决的是哪一类功耗痛点

1.1 cpufreq 与 devfreq 的本质差异

很多刚接触 devfreq 的同学第一反应是:这不就是 cpufreq 换了个名吗?还真不是一回事。cpufreq 调节的是 CPU 核心的电压频率,它面对的硬件是 CPU 域里的 PLL、时钟树和电源域。devfreq 调节的则是 CPU 之外的设备频率,典型对象包括 DDR 控制器、总线互联、GPU、NPU、ISP、多媒体编解码器。

这两者背后的模型差异非常大。CPU 的工作负载基本靠系统调度器体现,cpufreq 的 governor 可以根据运行队列长度、PELT 负载等判断要不要升频;而一个 DDR 控制器的“繁忙程度”没有那么直接的指标可以看,你得靠硬件计数器统计端口访问量、总线占用时间、buffer 中 pending transaction 的数目等。devfreq 设计的目的就是把“设备自身负载统计”和“频率调节策略”这两件事解耦。上层 governor 只关心一个抽象的 busy_time/total_time,具体怎么统计是设备驱动自己实现 get_dev_status 回调的事。

所以理解 devfreq 的关键,不是把它当成一个“通用频率调节模块”,而是当成一套“可插拔的 DVFS 策略容器”。它把不同设备的负载采集细节挡在框架外面,让 governor 用同一套策略逻辑去适配完全不同的硬件。

1.2 典型应用场景与边界

devfreq 最常见的使用场景有三个。第一个是 DDR/内存总线调频,手机 SoC 里 memory throughput 直接决定了后台应用切换时的流畅度和整机功耗,这一档调频做得烂,会出现“亮屏不动也发热”的诡异现象。第二个是 GPU 调频,游戏场景下 GPU 负载变化非常快,轮询周期设大了画面卡顿,设小了无谓功耗上升。第三个是总线/NPU 这类有固定工作负载窗口的加速器,它们的频率切换往往要配合 QoS 请求,简单轮询反而容易出错。

边界场景也要想清楚。像 Display 这类有严格时序要求的 IP,就不太适合用 devfreq 的轮询模型去做频率调节,因为一帧的刷新窗口非常短,等到统计出负载再切换频率,已经影响帧率了。这类场景更适合在硬件层做 QoS 反馈或者提前预算。另外,对外设频率变化有强实时性要求的场合,比如 PCIe 链路协商,也不该通过 devfreq 这种软件轮询去碰。

我有次在一个 video encode 的 IP 上直接套 devfreq 的 simple_ondemand,结果每秒频率都在跳,编码器输出时间戳都乱了。后来改成 fixed performance governor 并把 target 带宽设置为固定值,问题才消停。所以先搞清楚设备能不能接受动态频率,比怎么调 governor 更重要。

1.3 没有框架时的旧做法对比

在没有 devfreq 的时代,调一个外设频率通常是在设备驱动里自己写一套:

  • 起一个内核延时工作队列,每隔几十毫秒采样某个寄存器。
  • 自己实现几种调频策略,比如“超过某个阈值就提频”“低于某个阈值就降频”。
  • 自己管理 OPP 表里的电压频率对应关系。
  • 自己在 sysfs 里造各种 debug 节点,方便调试。

这套做法的毛病很典型:每个驱动都造一遍轮子,阈值写死在驱动里,策略没法在外面切换;其次,每次调频打出的 log 风格都不一样,排查问题全靠 grep dmesg。Devfreq 框架把这些公共逻辑抽出来,让设备驱动只做“设置频率”和“提供负载统计”这两件事,其余轮询、策略选择、频率上下限、统计周期、sysfs 可视化全部由框架统一处理。

实际接入过旧代码的同事应该深有体会,那种自己管理的调频代码经常伴随着“频率高下不去”“负载统计不准”这种问题,而且因为和业务驱动耦合太深,调起来非常难受。devfreq 相当于给 DVFS 打了一个标准件,虽然你不一定喜欢它的轮询模型,但它至少给你一个可以讨论和复现的容器。

2. devfreq framework 核心架构剖析

2.1 三个关键对象:device、profile、governor

devfreq 框架的核心抽象是三个东西:struct devfreq、struct devfreq_dev_profile、struct devfreq_governor。

  • struct devfreq 是框架对外暴露的设备实例,也是 sysfs 里注册节点的载体。
  • struct devfreq_dev_profile 是设备驱动向框架提交的“硬件描述文件”,里面定义了轮询周期、调频回调、状态采集回调和当前频率读取回调。
  • struct devfreq_governor 是调频策略的载体,负责根据 profile 提供的负载状态,计算目标频率后交给 devfreq 去执行。

设备驱动在 probe 阶段做三件事:填充 devfreq_dev_profile,设置好 OPP 表,最后调用 devfreq_add_device() 把设备交给框架。governor 则由框架通过传入的 governor_name 去查找,找到的话,设备实例上电后就开始周期调度。

static const struct devfreq_dev_profile my_dev_profile = { .polling_ms = 50, .target = my_dev_target_set, .get_dev_status = my_dev_get_status, .get_cur_freq = my_dev_get_cur_freq, };

关键点是 profile 一旦注册就不建议随便改,尤其是回调函数不能指向临时变量。我见过有驱动把 profile 放在栈上或者 module_init 的局部变量里,结果 devfreq 线程一跑就崩,这类问题内存损坏工具很难查,很容易被误判成“供应商驱动 bug”。

2.2 框架内部数据流向

devfreq 的数据流向可以简化成三个环节:

  1. 周期定时器唤醒 monitor 函数,触发一次负载统计。
  2. monitor 调用 profile 的 get_dev_status 回调,拿到当前 busy_time、total_time 和 current_frequency。
  3. 当前正在使用的 governor 根据 status 计算目标频率,然后走 devfreq_set_target() 去执行。

执行阶段会做一次 OPP 表查找,把目标频率对齐到硬件支持的最近档位。这里有两种查找方向:向上取整和向下取整。通常升频时用 OPP 表里的下一档或 ceil,降频时用 floor。框架里通过 dev_pm_opp_find_freq_floor() 和 dev_pm_opp_find_freq_ceil() 这两个 API 来匹配。

opp = dev_pm_opp_find_freq_floor(dev, &target_freq); if (!IS_ERR(opp)) { dev_pm_opp_get_freq(opp); dev_pm_opp_put(opp); }

调频之前框架会检查这次目标频率是否落在 min_freq 和 max_freq 的区间里。区间通过 sysfs 暴露给用户态,也可以在驱动里通过 devfreq_set_min_freq() 系列接口设置。最终 target 回调执行成功后,框架会把本次频率更新记录到 trans_stat 节点,方便后续查看调节轨迹。

2.3 源码位置与驱动注册入口

devfreq 框架主体在 drivers/devfreq/ 目录下。devfreq.c 是核心,governor.c 是 governor 注册与查找,governor_simpleondemand.c 和 governor_performance.c 分别对应不同的策略实现。如果平台用了 complex memory bus,往往还会在 drivers/devfreq/ 下再放对应平台的 devfreq-event 驱动。

注册入口我记得很清楚:

struct devfreq *devfreq_add_device(struct device *dev, struct devfreq_dev_profile *profile, const char *governor_name, void *data);

这个函数会在 sysfs 中创建 /sys/class/devfreq/xxx 节点,启动 monitor 内核定时器,并把设备标记为由 framework 管理。对应地,注销用 devfreq_remove_device()。

需要注意 devfreq_add_device() 传入的 dev 不是 profile 里设置频率时的那个直接设备,它更偏向于框架的管理节点。在常见实现里,dev 指向外设平台设备对应的 struct device,比如一个 platform_device 的 &pdev->dev。而 profile 的 target 回调里,你要通过 dev_get_drvdata(dev) 拿到驱动私有结构,再从私有结构里取 clk 和寄存器基地址。

还有一点,如果模块依赖的 governor 还没加载,devfreq_add_device() 会失败返回 -EPROBE_DEFER。这个行为对驱动 probe 影响非常大,实际调试时经常碰到“设备驱动明明写好了,却一直不 probe”,结果就是 governor 模块没有被系统加载。后面我会专门讲这个坑。

3. 关键细节解析与 sysfs 接口

3.1 governor 的注册与加载顺序

governor 在内核里通过 devfreq_governor_init() 注册,这是一个宏,本质是在 module_init 阶段调用 devfreq_governor_register()。

static struct devfreq_governor my_governor = { .name = "my_governor", .get_target_freq = my_governor_get_target_freq, .event_handler = my_governor_event_handler, }; devfreq_governor_init(my_governor);

每个 governor 至少要实现 get_target_freq(),框架的 monitor 会在每次唤醒后调用这个方法,拿到一个新的目标频率。governor 内部用不用 OPP 表、用什么样的负载阈值模型,完全自己控制。

governor 加载顺序容易出问题。比如你给设备驱动 module_init 里加了 devfreq_add_device(),但 governor 模块是用 modprobe 手动加载的,两个模块谁先谁后不可控,设备驱动 probe 就会遇到 -EPROBE_DEFER。解决方式有几个:

  • 把 governor 编入内核,而不是编成模块。
  • 在设备驱动的 module_init 里先调用 try_then_request_governor(),主动请求 governor 模块。
  • 在系统 init 脚本里严格排序加载。

我最后的做法是:devfreq governor 如果能进 defconfig 就直接编进内核。这类策略代码本身很小,没必要为了省这点空间给自己埋雷。

3.2 OPP 表与 target 回调

OPP 表是 devfreq 调频的地基。它描述设备有哪些可用频率,以及每个频率对应的电压。内核里很多时候把 OPP 表放在设备树里,格式大概像这样:

opp-table { compatible = "operating-points-v2"; opp-200000000 { opp-hz = /bits/ 64 <200000000>; opp-microvolt = <900000>; }; opp-400000000 { opp-hz = /bits/ 64 <400000000>; opp-microvolt = <950000>; }; };

设备驱动在 probe 阶段通过 dev_pm_opp_of_add_table() 解析这张表,之后就可以用 dev_pm_opp_find_freq_floor() 等接口在表里做频率匹配。

target 回调里面做的事大体是:先解析 dev_pm_opp_find_freq_floor() 得到的标准频率,再调用 clk_set_rate() 把时钟树切到对应频率。需要注意的是,某些平台的时钟树上还有 voltage domain 调整,需要额外调用 regulator_set_voltage(),这个动作往往会带来较大延时,执行时要考虑是否会阻塞 monitor 线程。

我实际遇到过的坑是:OPP 表里写了两个相同频率、不同电压的节点,看起来像是一个“名义频率”一个“offset 频率”。dev_pm_opp_find_freq_floor() 在这种场景下行为不好预判,直接导致 clk_set_rate() 设置完成了,但电压没有跟着走。最好的做法是确保 OPP 表频率节点的唯一性。

3.3 devfreq-event 与实时负载采集

负载统计是 devfreq 能否正常工作的前置条件。很多 SoC 上设备自身的计数器很容易拿到,比如 bus monitor 提供的带宽占用量、PLL 反馈频率等。但有些平台的 DDR 控制器只提供带宽占用率,不提供传统意义上的 busy_time/total_time,这时候就要用 devfreq-event 设备。

devfreq_event_dev 是框架提供的另一类抽象,专门用于包装“硬件事件计数器”这类资源。它通过 devfreq_event_get_edev_by_phandle() 在设备树里获取对端 event 设备,然后调用 get_event 类接口读取硬件计数值,再把原始值换算成加载率。

实际写代码时,我通常会在 get_dev_status() 里同时读两个 event 计数器:一个统计活动周期,一个统计总周期,然后算 busy_time 和 total_time。有的平台 event 设备的计数器会自动 wrap,换算时注意位数溢出,避免出现“偶尔统计出 120% 负载”的灵异现象。

示例:

static int my_dev_get_status(struct device *dev, struct devfreq_dev_status *stat) { struct my_dev *mdev = dev_get_drvdata(dev); unsigned long busy, total; devfreq_event_get_event(mdev->load_edev, &busy, &total); stat->busy_time = busy; stat->total_time = total; stat->current_frequency = clk_get_rate(mdev->clk); return 0; }

如果 get_dev_status 频繁被调用,event 设备的读取本身也会带来功耗和总线带宽开销。所以 polling_ms 要结合硬件事务频率来定,不是越短越好。DDR 这种高频设备常见设 50ms,GPU 这种游戏负载剧烈的设备可能会设到 20ms,我见过有人调成 5ms,结果系统平均功耗直接涨了 5%,频率曲线看着细碎,实际效果并没好多少。

3.4 常用 sysfs 节点速查表

devfreq 框架挂载在 class 设备下,接入设备后,系统里就会出现/sys/class/devfreq/<device-name>/目录。写驱动时常用这几个节点来验证:

节点作用常见操作
governor查看/切换当前 governorecho simple_ondemand > governor
available_governors列出所有可用策略cat 查看
cur_freq当前实际频率cat 查看
target_freq框架认为应该达到的频率cat 查看
available_frequencies从 OPP 表导出可用的频率档位cat 查看
min_freq / max_freq频率上下限echo 写入
polling_interval轮询周期,单位 msecho 20 写入
trans_stat频率切换统计cat 查看,echo 0 清零

我排查频率“卡死”时,第一步就是同时看 cur_freq 和 target_freq。如果 target_freq 一直在变而 cur_freq 不变,说明 target 回调或 clk_set_rate 环节有问题;如果两个都不变,再看 governor 是不是 performance,这个 governor 会把频率直接锁到最高档,不随着负载走。

还有一个细节:available_frequencies 在有些平台上显示的是“启动时就固定的一串 OPP”,即使 OPP 表后来被动过,这个节点内容也不会刷新。所以别只依赖 sysfs 里的列表去判断硬件最大能力,必要时到设备树里实际确认一下。

4. 实操示例:把一个外设接入 devfreq

4.1 定义 profile 和私有数据结构

假设我现在要为某个媒体 IP 写 devfreq 驱动。第一步先把私有数据准备齐,至少要有 clk 指针、event 设备指针、以及一个串行化的 mutex,避免调频过程中和中断处理函数冲突。

struct my_dev { struct device *dev; struct clk *clk; struct devfreq *devfreq; struct devfreq_event_dev *load_edev; struct mutex lock; unsigned long busy_time_us; unsigned long total_time_us; };

结构体里我习惯把 busy_time 和 total_time 取出来缓存,而不是每次都现读。不是担心 event 设备读不出来,而是有些平台的 event 寄存器读取时序比较脆,调用频率太高会触发总线 STALL,反而影响真实性能。

合池设计:现在这些数据结构,到底要不要带 lock,取决于你调频回调是否会跟某个硬实时的中断路径抢频率。如果不会,其实一个 seqcount_t 都够用;但如果没把握,宁可先用 mutex,稳定性优先,锁开销后面再优化。

4.2 实现 target 和 get_dev_status 回调

target 回调是整个调频的执行口,里面要把频率档次对齐到硬件支持的值。

static int my_dev_target_set(struct device *dev, unsigned long *freq, u32 flags) { struct my_dev *mdev = dev_get_drvdata(dev); struct dev_pm_opp *opp; unsigned long target; int ret; target = *freq; opp = dev_pm_opp_find_freq_floor(dev, &target); if (IS_ERR(opp)) return PTR_ERR(opp); dev_pm_opp_put(opp); ret = clk_set_rate(mdev->clk, target); if (ret) return ret; *freq = target; return 0; }

get_dev_status 则要把统计到的忙闲数据填进 framework 要求的结构体。这里有个取舍:直接从硬件寄存器读,还是先用某种方式算出比率?我建议在驱动里做一个小的平滑处理,至少把上一轮数值过滤一下,不然某些瞬时波动会把 governor 压到频繁升频降频。

static int my_dev_get_status(struct device *dev, struct devfreq_dev_status *stat) { struct my_dev *mdev = dev_get_drvdata(dev); unsigned long busy = 0, total = 0; devfreq_event_get_event(mdev->load_edev, &busy, &total); mdev->busy_time_us = (mdev->busy_time_us + busy) >> 1; mdev->total_time_us = (mdev->total_time_us + total) >> 1; stat->busy_time = mdev->busy_time_us; stat->total_time = mdev->total_time_us; stat->current_frequency = clk_get_rate(mdev->clk); return 0; }

这个平滑式子当然不是万金油,如果业务场景对突发负载很敏感,就不适合取平均值,得改用带上升沿加速的滤波算法。实际调频效果好不好,往往就体现在这些细节上。

4.3 注册 devfreq 设备并挂载 governor

接下来把这些回调组进 devfreq_dev_profile,调用 devfreq_add_device() 完成注册。

static const struct devfreq_dev_profile my_dev_profile = { .polling_ms = 50, .target = my_dev_target_set, .get_dev_status = my_dev_get_status, .get_cur_freq = my_dev_get_cur_freq, }; static int my_dev_probe(struct platform_device *pdev) { struct my_dev *mdev; mdev = devm_kzalloc(&pdev->dev, sizeof(*mdev), GFP_KERNEL); platform_set_drvdata(pdev, mdev); mdev->clk = devm_clk_get(&pdev->dev, "media_clk"); mutex_init(&mdev->lock); mdev->devfreq = devfreq_add_device(&pdev->dev, &my_dev_profile, "simple_ondemand", NULL); if (IS_ERR(mdev->devfreq)) return PTR_ERR(mdev->devfreq); return 0; }

注册完成后,cat /sys/class/devfreq/ 下应该能看到设备,接下来就能在用户态测试频率切换效果了。这里要注意:devfreq_add_device() 内部会请求 governor,如果返回 -EPROBE_DEFER,probe 函数要正确返还这个错误码,让 deferred probe 机制后续重新触发。

4.4 用 sysfs 实测效果

我一般会先做一个“人工负载”测试,比如让某个多媒体线程持续跑起来,然后观察 target_freq 和 trans_stat 的变化:

watch -n 0.5 cat /sys/class/devfreq/1000000.media/cur_freq cat /sys/class/devfreq/1000000.media/trans_stat echo 0 > /sys/class/devfreq/1000000.media/trans_stat

线程跑起来后,频率应该从低档逐步升到中高档。等线程停掉,负载降下来,频率再在下一个轮询周期回落。如果频率只升不降,说明 governor 没调用 get_dev_status,或者统计值有问题;如果只在两档间反复横跳,那就该检查 busy_time 的平滑系数和 polling_ms 是否设得太小。

5. 调试经验与常见问题排查

5.1 频率不变化,先查这几项

频率不动的排查顺序很重要,直接乱翻代码容易陷入泥潭。我会按照下面的顺序来:

  1. 看/sys/class/devfreq/<dev>/governor,确认当前 governor 是不是 performance。performance 会把频率固定在高档,单纯看 cur_freq 不变是正常行为。
  2. 看 cur_freq 和 target_freq,如果 target_freq 一直变而 cur_freq 不变,问题在 target 回调或 clk_set_rate。
  3. 看 trans_stat,这个节点记录了每一档的切换统计。如果切换次数始终为零,说明 governor 没有触发调频动作。
  4. 检查 get_dev_status 有没有被调用。可以用 tracepoint,或者直接在驱动里加一个 dev_info 打印,注意要把它放在 rate_limited 的路径里。

一个细节:target 回调里我喜欢加一行dev_dbg(),调频时打印旧值和新值。但如果这条打印太多,被 printk 的限流机制遮掉,反而找不到关键信息。更好的做法是在 struct devfreq 挂到 sysfs 之后,用 ftrace 的 function_graph 跟踪 devfreq_set_target() 和 clk_set_rate() 的调用链。

5.2 governor 无法使用的常见原因

available_governors里没有任何 governor 的情况并不少见。通常原因就是 governor 模块没有加载,或者编译时没有选上对应的 Kconfig。内核里要打开 CONFIG_DEVFREQ_GOV_SIMPLE_ONDEMAND 等,否则即使 devfreq 框架存在也无策略可用。

另一种情况是 governor 绪域启动异常,比如 governor 的 event_handler 在 DEVFREQ_GOV_START 事件中没有正确初始化内部状态。表现为 sysfs 写 governor 时直接报错,或者设备 probe 成功但 monitor 线程不启动。

我实践中最保险的定位方式是把所有 devfreq governor 都编进内核,并打开 CONFIG_DEVFREQ_EVENT。若平台不支持 event 设备,至少框架的缺省路径还能跑通。

5.3 devfreq 与 cpufreq、cpuidle 的联动

实际项目里,这几个电源管理模块是联动的,不能孤立看待。比如某个平台的总线频率和 DDR 频率是耦合的,CPU 频率提高后总线负载也会变大,busy_time 会跟着涨,然后 devfreq 会继续拉高 DDR 频率。如果你看到 CPU 和 DDR 频率联动得非常“激进”,往往不是 devfreq 本身问题,而是负载统计样本中含了 CPU 引起的瞬时访问。

面对这类联动问题,我的习惯是先在 devfreq 侧加 max_freq 限制,把峰值频率锁住,再做 cpufreq 和 devfreq 的交叉开关实验。比如先把 devfreq 的 max_freq 固定在 400MHz,再跑负载,看功耗变化;固定到 800MHz 再跑,对比性能拐点。这样就把“哪一档是收益拐点”摸出来后,再根据场景去设备树里设 default level,而不是靠 governor 无限动态调整。

同样的,cpuidle 也会影响 devfreq。当 CPU 进入 deep idle,总线时钟会被拉低,devfreq 的 get_dev_status 可能采到一段很长的 idle 区间,导致负载统计被拉平。所以很多平台会把 devfreq 的 monitor 在系统 suspend/resume 时暂停,避免 suspend 期间采集到奇怪的活跃数据。如果 devfreq 驱动没有处理 DEVFREQ_GOV_SUSPEND / DEVFREQ_GOV_RESUME 这两个事件,就要特别小心睡眠唤醒后的频率状态。

5.4 trans_stat 和 tracepoint 定位问题

trans_stat 是我排查频率切换问题时用得非常多的一个节点。它的格式类似一个矩阵,每一行表示一条横跨的频率转换路径,后面是该路径的切换次数。如果发现某个频率档几乎没被跳到,而 program 又明显觉得卡,那可能就是 OPP 表缺少某个中间档位,导致从低档直接升到高档,功耗不降反而高。

cat /sys/class/devfreq/1000000.media/trans_stat From : To : 200000000 400000000 800000000 200000000: 0 2 0 400000000: 1 0 1 800000000: 0 1 0

从这个输出能看出,设备没有经过 200MHz 到 800MHz 的切档,说明 governor 的调频步进设计里,升档逻辑跳太快。这个现象常见于使用 max/avg 负载估算的 governor,中间档在阈值设计里变成了死区。

如果 trans_stat 也看不出异常,就用 ftrace 跟踪 devfreq_set_target() 的调用参数。在内核配置中打开 FUNCTION_TRACER,然后:

echo devfreq_set_target > /sys/kernel/debug/tracing/set_ftrace_filter echo 1 > /sys/kernel/debug/tracing/events/power/power_frequency/enable echo 1 > /sys/kernel/debug/tracing/tracing_on

这样能看到调频请求的目标频率变化轨迹,快速判断是 governor 决策异常,还是 target 回调里 clk_set_rate 执行失败。

5.5 还有一个关于性能损耗的额外提醒

devfreq monitor 线程如果设计不仔细,会影响实时性。最典型的案例是某平台上 devfreq 的轮询周期和硬件实时时钟抢占同一个硬件单元,每次轮询都触发一次 interrupt storm。CPU 使用率会莫名上涨,功耗反而更高。

如果遇到这种情况,不要把 polling_ms 无限调大,而是要查一下 event 设备获取负载结果时,是不是每次都要 read 好几个寄存器,且这些寄存器访问又要在总线上等待 response。如果是,那就得考虑把统计交给硬件自动累积,软件只读一个最终结果,而不是在轮询路径里反复读取原始计数。也可以把 poll 任务放到普通 workqueue 而不是 high-priority workqueue,避免和实时任务争抢。

最后再补充一个我在实践中特别在意的小技巧

devfreq 的调频输出跟 CPU 频率一样,也存在“乱跳”和“稳不住”的问题。单纯改 polling_ms 并不总能解决,有一个体感很好的补法:在 target 回调里增加一个最小切换间隔,比如至少 100ms 才能再切一次。这样对 DDR 这类切换成本高的设备很有用,代价是响应速度变慢,但实际功耗通常更优。我一般会在私有结构体里记录上次切换的 jiffies,达不到间隔就先把目标频率记录下来,等下一轮再执行。这样合并频繁请求,能显著降低总线时钟切换的开销。

另外,如果你在一个复杂的多设备平台上做整机功耗,建议把 devfreq 当前 governor、polling_ms、max_freq 都做成可配置项放到调试内核参数里。很多真正的问题不会在单模块测试时暴露,只有整机功耗测试时才看得出是调频策略、负载统计还是硬件反馈环的问题。做好实验工具,效率和返修率都会不一样。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 7:24:58

在线教程丨高性能与易部署兼得,DeepSeek-V4-Flash模型参数284B,简单任务可媲美1.6T Pro版模型:用TaoToken统一Key跑通vLLM本地部署与效果验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/7 7:24:40

Windows内核池泄漏排查:libdlt与strdup符号拦截陷阱

1. 从一次线上告警说起&#xff1a;GODService 到底出了什么问题GODService 是我们内部一个常驻型的后台服务&#xff0c;跑在 Windows 平台上&#xff0c;负责设备状态采集、指令下发和日志回传这一整套链路。它本身不算大&#xff0c;代码量也就几万行&#xff0c;但胜在稳定…

作者头像 李华
网站建设 2026/10/7 7:24:17

Agent Skills 实战指南:从安装、开发到组合编排的完整避坑手册

1. 从“skills”这个热词说起&#xff1a;它到底是什么&#xff0c;为什么突然火了最近一段时间&#xff0c;不管是在技术社区、开发者群聊&#xff0c;还是在各种项目讨论里&#xff0c;“skills”这个词出现的频率高得离谱。很多人第一次看到“skills”这个词&#xff0c;脑子…

作者头像 李华
网站建设 2026/10/7 7:23:10

Claude Code 多用户部署:用 CC Switch 把 API key 改到 TaoToken

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华