这台 ztl A588 主机(RK3588)此前由厂商预置的 cpu-governor-performance.service 接管 CPU 调频,策略长期停在 performance。性能是给足的,但频率几乎没有回落空间;而机主既不清楚长时间高负载下散热能撑到什么程度,也不想用焊死频率的方式换稳定。最终方案是两件事叠加:把调频交回内核的 schedutil 动态策略,再额外加一层 80°C 温度守护,在温度逼近上限时统一压低所有 CPU 策略的频率上限。
整套改动在 2026-10-08 完成部署与实测,内核未做任何修改,不碰设备树、固件、GPU/NPU 设置和现有硬件加速链路,也没有新增端口。
环境与前提
Python 统一通过 /opt/cpu-freq-guard/venv/bin/python 运行,虚拟环境用 python3 -m venv --without-pip 创建,重装时重新生成,不归档虚拟环境。
设计目标与实施步骤
频率范围与保护动作
守护同时管理三个 cpufreq policy,覆盖大小核,并按热区最高读数做统一决策。
需要强调一句容易误解的地方:恢复的是最高频率上限,不是把 CPU 强制锁定到最高频率。日常负载下内核仍然会自由升降频,守护只负责压住天花板。1.2 GHz 保护值、温度和轮询参数都可配置,如果填写了不存在的频率档位,程序会向下选取现有档位;它不会把频率提升到 2.4 GHz。
迟滞与连续确认为什么必要
如果只用一个阈值来回切,温度在 80°C 上下抖动时会导致限频反复开关。这里的做法是触发用 80°C、恢复用 75°C,中间留 5°C 间隙,并要求 75°C 以下连续 3 次采样都成立才释放限制。启动瞬间也走同一条保守逻辑:开机温度若已经高于 75°C,守护不会因为「服务刚重启」就把保护放开。
传感器读不到就限频
温度读数缺失、数值异常,甚至热区集合发生变化(比如内核枚举的热区数量变了)时,守护一律按保守策略限频到 1.2 GHz。逻辑很简单:不知道温度时,按最坏情况处理比按最好情况处理安全。
安装与文件布局
源码目录自带安装入口,默认只预演不写盘:
cd /root/AI/cpu-freq-guard
./restore.sh # 默认预演,不写盘
./restore.sh --apply # 实际安装;已有 /etc 文件先做同目录日期备份守护由 systemd 直接运行,单元里约束了权限与可见的 sysfs 范围(ProtectSystem=strict 下只放开 cpufreq 目录的写权限、热区目录只读);照抄时这段也可以直接用:
[Unit]
Description=CPU schedutil governor and thermal guard
Documentation=file:/root/AI/docs/cpu-freq-guard.md
After=sysinit.target
Conflicts=cpu-governor-performance.service
[Service]
Type=simple
ExecStart=/opt/cpu-freq-guard/venv/bin/python /usr/local/sbin/cpu-freq-guard
Restart=on-failure
RestartSec=3s
TimeoutStopSec=10s
StateDirectory=cpu-freq-guard
StateDirectoryMode=0700
RuntimeDirectory=cpu-freq-guard
RuntimeDirectoryMode=0700
RuntimeDirectoryPreserve=yes
UMask=0077
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
PrivateDevices=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictAddressFamilies=AF_UNIX
ReadWritePaths=/sys/devices/system/cpu/cpufreq
ReadOnlyPaths=/sys/class/thermal
Environment=PYTHONDONTWRITEBYTECODE=1
[Install]
WantedBy=multi-user.target对应安装位置是 /etc/systemd/system/cpu-freq-guard.service,而配置文件的初始内容就是下面这几行,字段含义与下文的「修改阈值」一节一致:
{
"governor": "schedutil",
"hot_millic": 80000,
"cool_millic": 75000,
"poll_seconds": 2,
"cool_samples": 3,
"cap_khz": 1200000
}查看运行状态
systemctl status cpu-freq-guard.service --no-pager
journalctl -u cpu-freq-guard.service -n 30 --no-pager
cat /run/cpu-freq-guard/status.json
for p in /sys/devices/system/cpu/cpufreq/policy*; do
echo "$p"
cat "$p/scaling_governor" "$p/scaling_min_freq" "$p/scaling_max_freq" "$p/scaling_cur_freq"
done状态文件里的 normal 表示正常动态调频,hot 表示温度保护中,sensor-fault 表示传感器异常保护。时间戳 updated_unix 用来判断数据是否过期——状态文件在服务停止后可能仍然保留,所以判断状态时要同时确认服务是 active。日志只在状态切换时记录,不会每两秒刷屏。
修改阈值
cp -a /etc/default/cpu-freq-guard.json "/etc/default/cpu-freq-guard.json.bak.$(date +%Y%m%d-%H%M%S)"
# 编辑 JSON 后:
systemctl restart cpu-freq-guard.service可配置字段为 governor(本方案仅接受 schedutil)、hot_millic=80000、cool_millic=75000、poll_seconds=2、cool_samples=3、cap_khz=1200000。配置必须满足恢复阈值低于触发阈值,程序不支持未知字段。修改后要同步源码配置、归档与校验和。
暂停与回滚
systemctl stop cpu-freq-guard.service
# 仅暂停监测,保留当前 governor 和频率限制,停止时不会自动恢复满频。
systemctl start cpu-freq-guard.service
# 完整回滚(先检查预演):
/usr/local/sbin/cpu-freq-guard-rollback
/usr/local/sbin/cpu-freq-guard-rollback --apply回滚要求读数正常且所有热区低于 75°C,然后停止新服务、恢复第一次部署前每个策略的 governor/min/max、禁用新服务,并重新启用厂商 performance 开机服务;源码、程序和基线都不会被删除。以后如果要再回到动态策略:
systemctl disable cpu-governor-performance.service
systemctl enable --now cpu-freq-guard.service回滚与重装还原是两回事:rollback 是把性能策略恢复成原来的样子;归档 restore 是安装这套新的动态调频方案。
实测验证
20 项单元测试通过:覆盖 80°C 边界、最高热区判定、75°C 迟滞与计数重置、故障恢复、无传感器、基线不覆盖、拓扑改变拒绝运行、现有低上限保护、内核更严格限制、动态 QoS 和配置校验等。测试使用临时目录模拟 sysfs,不碰硬件。
实机升降频:空闲采样时三个 policy 均到 408 MHz;大核短时四线程负载约 0.6 秒采样已到 2256 MHz,峰值 51.769°C;卸载约 2 秒后大核回到 408 MHz。小核四线程负载到 1800 MHz,卸载后回到 408 MHz。日常后台活动仍会带来短时升频。
温度用模拟读数注入,频率限制写真实 sysfs:80000 触发后各策略真实
max=1200000;76000 继续限频;75000 连续三次后恢复 1800000/2256000/2256000;模拟读数故障后再次限频。服务重启与完整回滚:实机恢复部署前原始 governor/min/max,与持久基线一致,随后重新启用 schedutil;服务重启测试通过。完整回滚脚本也经过实机执行,之后恢复新方案。
一处返工:初版过于严格地立即读回有效 min,导致测试期间出现重启日志;最终版改为适应内核异步更新及聚合 QoS,稳定性重新验证,未保留 ondemand 过渡逻辑。历史错误日志未清除。
注意事项、局限与未验证项
此守护不是温度恒定器。CPU 限频不能保证芯片始终低于 80°C,尤其在 GPU/NPU 持续满载时。内核原有热保护继续工作,长时间重负载仍需足够散热。
守护持续管理 governor/min/max,不要让其他调频服务同时写这些值。内核其他 QoS 请求仍能临时抬高有效最低频率——看到小核的 min 在 408000、900000、1800000 等值之间变化,不代表锁频失败;
scaling_min_freq是聚合后的有效值,内核更新本身也有异步延迟。因此程序对 governor 做有限时间读回校验、对最高值验证不超过请求上限,而不要求有效 min 永远等于 408000。另外,performance并不普遍意味着修改最低频率,此前把本机 min 变化归因于 governor 的说明并不准确。未进行整机重启、真实 80°C 长时烤机、功耗测量。已检查开机链接、单元校验和运行状态,但不应把这些说成完成了重启实测,也不应据此承诺某个节电比例。
硬件拓扑或内核 OPP 变化后须人工核对,程序会拒绝使用不匹配的基线快照;不要删除原始基线。
归档只覆盖本功能,不复制全系统、不替换其他守护、不包含凭据。旧原始参数仅存源码的部署前记录,不还原到新机器的运行状态目录,避免用旧硬件/旧策略快照误回滚。归档放在 eMMC 上,重装会清空它,必须另行拷出设备。
集成测试会临时停止守护、模拟温度分支后恢复正式服务,不修改正式 80°C 配置;启动前要求温度低于 65°C,负载温度达到 70°C 会提前结束,仅在确认可短时测试时执行。
重装后需手工确认硬件支持 schedutil、温度读数正常,再检查日志和频率变化,并更新项目规则及归档总索引。
附:完整脚本
上面步骤里用到的完整脚本不再整段贴在正文,已作为附件上传到对象存储,点击文件名即可下载:
cpu_freq_guard.py —— 231 行,调频与温度守护主程序
对象存储策略会自动把文件名改写成 UUID(例如
95D96A8A-FB38-4CCB-AB42-F6E906E70760.py),这是存储策略的行为,不影响文件内容;下载后按正文里的路径改名放好即可。