返回星港

RK3588 动态调频改造:schedutil 加 80°C 温度守护的部署与实测

OBSERVER又菜又爱玩的小朱 LAUNCH JUMPS0 次跃迁
星域 RK3588 信标 # RK3588
RK3588 动态调频改造:schedutil 加 80°C 温度守护的部署与实测 · 封面

这台 ztl A588 主机(RK3588)此前由厂商预置的 cpu-governor-performance.service 接管 CPU 调频,策略长期停在 performance。性能是给足的,但频率几乎没有回落空间;而机主既不清楚长时间高负载下散热能撑到什么程度,也不想用焊死频率的方式换稳定。最终方案是两件事叠加:把调频交回内核的 schedutil 动态策略,再额外加一层 80°C 温度守护,在温度逼近上限时统一压低所有 CPU 策略的频率上限。

整套改动在 2026-10-08 完成部署与实测,内核未做任何修改,不碰设备树、固件、GPU/NPU 设置和现有硬件加速链路,也没有新增端口。

环境与前提

项目

实际情况

设备

ztl A588 / RK3588

系统

Ubuntu 24.04,厂商内核 Linux 5.10.198

托管服务

cpu-freq-guard.service 已开机启用

厂商服务

cpu-governor-performance.service 的开机链接已禁用,原始厂商单元未修改

依赖

systemd、Python 3.12 与 python3-venv、Bash/coreutils;只用标准库,不安装 pip 包

Python 统一通过 /opt/cpu-freq-guard/venv/bin/python 运行,虚拟环境用 python3 -m venv --without-pip 创建,重装时重新生成,不归档虚拟环境。

设计目标与实施步骤

频率范围与保护动作

守护同时管理三个 cpufreq policy,覆盖大小核,并按热区最高读数做统一决策。

项目

正式设置

governor

schedutil,三个 policy 一起管理

小核 cpu0–3

请求范围 408 MHz–1.8 GHz

大核 cpu4–5 / cpu6–7

请求范围 408 MHz–2.256 GHz

监测范围

所有 thermal_zone 的最高读数,当前为 7 个 SoC 热区

触发

任一区域 ≥80°C,限制全部 CPU 策略的最高频率至 1.2 GHz

恢复

全部热区 ≤75°C,连续 3 次确认后恢复部署前上限

轮询

2 秒,触发响应通常为 0–2 秒,另有内核更新延迟

传感器异常

读数缺失、异常或区域集合变化时,保守限频至 1.2 GHz

重启时的迟滞

启动温度 >75°C 时先限频,不因服务重启提前释放保护

需要强调一句容易误解的地方:恢复的是最高频率上限,不是把 CPU 强制锁定到最高频率。日常负载下内核仍然会自由升降频,守护只负责压住天花板。1.2 GHz 保护值、温度和轮询参数都可配置,如果填写了不存在的频率档位,程序会向下选取现有档位;它不会把频率提升到 2.4 GHz。

迟滞与连续确认为什么必要

如果只用一个阈值来回切,温度在 80°C 上下抖动时会导致限频反复开关。这里的做法是触发用 80°C、恢复用 75°C,中间留 5°C 间隙,并要求 75°C 以下连续 3 次采样都成立才释放限制。启动瞬间也走同一条保守逻辑:开机温度若已经高于 75°C,守护不会因为「服务刚重启」就把保护放开。

传感器读不到就限频

温度读数缺失、数值异常,甚至热区集合发生变化(比如内核枚举的热区数量变了)时,守护一律按保守策略限频到 1.2 GHz。逻辑很简单:不知道温度时,按最坏情况处理比按最好情况处理安全。

安装与文件布局

文件 / 目录

用途

/usr/local/sbin/cpu-freq-guard

守护程序

/etc/default/cpu-freq-guard.json

配置文件,温度单位 m°C、频率单位 kHz

/etc/systemd/system/cpu-freq-guard.service

systemd 单元

/usr/local/sbin/cpu-freq-guard-rollback

默认预演的回滚入口

/var/lib/cpu-freq-guard/original.json

首次更改前的持久基线,重启不会覆盖

/run/cpu-freq-guard/status.json

运行状态,位于 tmpfs,避免周期写 eMMC

/root/AI/cpu-freq-guard/

源码、测试、git 历史和首次部署记录

/root/AI/backup/cpu-schedutil-thermal-80c/

重装还原归档

源码目录自带安装入口,默认只预演不写盘:

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 是安装这套新的动态调频方案。

实测验证

  1. 20 项单元测试通过:覆盖 80°C 边界、最高热区判定、75°C 迟滞与计数重置、故障恢复、无传感器、基线不覆盖、拓扑改变拒绝运行、现有低上限保护、内核更严格限制、动态 QoS 和配置校验等。测试使用临时目录模拟 sysfs,不碰硬件。

  2. 实机升降频:空闲采样时三个 policy 均到 408 MHz;大核短时四线程负载约 0.6 秒采样已到 2256 MHz,峰值 51.769°C;卸载约 2 秒后大核回到 408 MHz。小核四线程负载到 1800 MHz,卸载后回到 408 MHz。日常后台活动仍会带来短时升频。

  3. 温度用模拟读数注入,频率限制写真实 sysfs:80000 触发后各策略真实 max=1200000;76000 继续限频;75000 连续三次后恢复 1800000/2256000/2256000;模拟读数故障后再次限频。

  4. 服务重启与完整回滚:实机恢复部署前原始 governor/min/max,与持久基线一致,随后重新启用 schedutil;服务重启测试通过。完整回滚脚本也经过实机执行,之后恢复新方案。

  5. 一处返工:初版过于严格地立即读回有效 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、温度读数正常,再检查日志和频率变化,并更新项目规则及归档总索引。

附:完整脚本

上面步骤里用到的完整脚本不再整段贴在正文,已作为附件上传到对象存储,点击文件名即可下载:

对象存储策略会自动把文件名改写成 UUID(例如 95D96A8A-FB38-4CCB-AB42-F6E906E70760.py),这是存储策略的行为,不影响文件内容;下载后按正文里的路径改名放好即可。