一、 开头:死亡现场
最近入手了一台 Mac Mini 6(Apple M6 芯片),计划用来评测一些小机型大模型的情况。因此,需要一个能直观、方便观察系统性能的工具。综合对比下,开源项目 eul 非常合适------它足够小巧玲珑(App 仅 5MB 左右),且原生常驻在菜单栏。
但在用 brew 安装后,结果让人傻眼:CPU 温度和 GPU 相关信息全部显示 N/A。对于要跑大模型测试的场景来说,这些信息极其关键。
图 1.1 CPU 温度和 GPU 相关信息全部显示 N/A
于是找到了 eul 的源码。GitHub 上,master 分支的最新 commit 是 b5c3bd0,提交信息写着 Create Localizable.strings (#238)。日期栏赫然显示:5 years ago。
图 1.2 eul项目源码更新情况
右上角 10k Star 似乎在展示着过去的辉煌,就这样静静地躺在 GitHub 上。Issues 区里,有人在两年前问:"M1 还有救吗?"没有人回答。
在 M6 上它还有救么?我的 macOS 应用开发经验不多,但在如今的 AI 时代,或许也可一试。
二、 问题:调研现场
拿到 5 年前的源码后,发现 eul 的温度读取实现源自 Intel 时代的 SMCKit 。SMCKit 的 README 明确写着它「for Intel based Macs」,其内置的传感器清单全部是 Intel 机型的 key(TC0P/TG0P/TM0S 等),显然对于 Apple Silicon 毫无办法。
使用 Debug 构建,断言直接触发,App 启动即崩溃:
eul/SMC.swift:390: Assertion failed: SMCParamStruct size is != 80
显然,原有的一些数据结构已经不能适用于如今 M6 的运行环境了。
查找一番资料后,发现一个更伤感的事情:Apple 从未公开 AppleSMC 的 API 和 key 命名规范。难道要凭我这有限的 macOS 研发经验去盲猜?
是时候用下这个时代的工具了:Workbuddy
三、 复活吧,eul:先写探针,再动手术
由于没有官方文档支持,Workbuddy 首先写了一个 Swift 探针(smc_probe.swift),直接与底层驱动对话。探针做两件事:
- 读取
#KEY得到本机 key 总数,逐个枚举,筛出所有T*前缀的温度 key,并解码当前值。 - 遍历
IOAccelerator服务,打印PerformanceStatistics的全部统计项。
最戏剧性的一幕发生了:用来排查故障的探针,自己先崩了。
探针在读取数据时,底层的 IOKit 调用直接抛出了错误:IOKit call error: 0x-1ffffd39。为了查明原因,让探针打印出它发给驱动的数据结构尺寸。结果发现,尺寸变成了 88 字节。
这恰好解释了前文 eul 启动崩溃时的那个断言报错:SMCParamStruct size is != 80,显然之前面对 88 字节的驱动数据包,程序底层直接拒绝了沟通。
这意味着,用来排查问题的探针,自己先完美复现了 eul 的故障。这直接让根因定位从"盲人摸象"变成了"精准实测"。
探针跑通后,拿到了 M6 的一手数据:
- SMC 总 key 数量:2066
- Tx 温度类 key 数量:135
- 温度类型:135 / 135 均为
flt类型,没有sp78
Workbuddy 顺藤摸瓜,制定了"四刀"修复方案。
第一刀:缝合 ABI 裂缝(88 字节 vs 80 字节)
结构体 SMCParamStruct 出自 Apple 早期的开源项目,驱动侧固定按 80 字节解析。但结构体中的一个字段 IOByteCount,在 arm64 用户态下变成了 64 位(8 字节)。这多出来的 4 字节加上对齐填充,让结构体变成了 88 字节。 Debug 构建直接崩溃,Release 构建调用静默失败。
- 修复: 不依赖平台定义,Workbuddy 建议直接把
dataSize的类型固定为UInt32。这样既保证了二进制布局稳定,又去掉了三处多余的类型转换。
第二刀:自适应数据类型(sp78 vs flt)
原代码把读取逻辑写死为 Intel 时代的 sp78(2 字节定点)。而 M6 全是 flt(4 字节 IEEE 754 浮点)。按 sp78 去读,只能读出垃圾值。
- 修复: 由"假设类型"改为"查询类型"。先查
keyInformation拿真实类型,再按实际类型选解码器。Workbuddy 顺手补齐了原有fromSP78丢失小数位的 Bug,并新增了fromFLT解码器,一举覆盖 T2 和 Apple Silicon 芯片。
第三刀:放弃静态清单,拥抱动态发现
原代码只遍历硬编码的 28 个 Intel key。在 M6 上,这些 key 一个都不存在,导致传感器列表为空。
- 修复: 新增
allTemperatureSensors()函数,直接枚举 2066 个 key,按"T 前缀 + 温度类型"筛选出 135 个候选,再剔除未初始化的热区占位符(如Tz11恒为 0.0 °C),最后保留 126 个有效传感器。 - 纠错插曲: 聚合温度时,第一版代码是按"显示名"匹配前缀的。但显示名早已被改写(如改为"CPU P-Core"),导致匹配不上。Workbuddy 提醒道:标识符(key code)与显示名必须严格分离 。改为按
Tp0、Tg0等代码前缀匹配后,温度终于出来了。
第四刀:GPU 识别脱虚向实
原代码要求 GPU 必须有 PCI 标识符(device-id / IOPCIMatch)。但在 Apple Silicon 上,集成 GPU 根本没有这玩意儿,导致 GPU 列表被直接丢弃。
- 修复: 让
device-id从"必需"变成"可选",缺失时用型号名(如Apple M6)兜底。GPU 的温度也从"必须有 registry 字段"改为"registry 优先,SMCTg0x兜底"。
四、 跑起来了,但这不是终点
经过编译和构建参数的微调(调整 SDKROOT 等),App 终于成功跑起来了。
图 4.1 CPU 温度和 GPU 相关信息全部正常显示
在这台 M6 上,它成功读取到了:
- CPU 温度:32 °C
- GPU 温度:31 °C
- GPU 使用率:10 %
但这不仅仅是几个数字的胜利。修复前后的代码对比,本质上是把 eul 的底层采集架构,从"面向 Intel 机型的死胡同"拉回了"面向底层协议"的正轨。
修复后的整体架构演进
图 4.2 整体架构演进
从上图可以看出,系统的数据流向没有变,依然是自下而上(硬件 -> 接口层 -> 采集层 -> 状态层 -> 展示层)。真正的变化,发生在那几根最底层的箭头上。
以前,底层接口层(SMCKit / IOHelper)与硬件之间是"硬连线"的,键名、类型、标识符全写死;现在,它们变成了一条带有"翻译器"和"过滤器"的通用管道。
温度读取链路:从"死胡同"到"自适应"
图 4.3 温度读取链路
左侧是原来的路径:按 28 个 Intel 硬编码键名去查 -> 查不到 -> 列表为空 -> 显示 N/A。 右侧是修复后的路径:枚举全量 Key -> 筛选 flt/sp78 类型 -> 合理性过滤(剔除 Tz11 这类恒为 0 的无效热区)-> 按 Key Code 前缀聚类取最大值 -> 输出真实温度。
架构级变化: 彻底解耦了"显示名"与"标识符"。原来的代码按 "CPU_0_DIE" 去查传感器,一旦硬件改了名,代码就崩。现在程序只认底层的 Tp0、TPD 等代码前缀,显示层用 friendlyName 随时可以包装成漂亮的"CPU P-Core"。这就是"面向协议"带来的韧性。
GPU 读取链路:脱虚向实后
图 4.4 GPU 读取链路 GPU 链路的修复异曲同工。原代码在 system_profiler 和 IOAccelerator 里死找 PCI 标识符(device-id / IOPCIMatch),找不到就丢弃整条记录。
修复后,标识符变成了"双轨制":有 PCI 就走 PCI,没有 PCI(如 M6 集成 GPU)就用型号名(如 Apple M6)兜底匹配。同时,GPU 温度不再强求从 registry 里拿,而是回退到 SMC 的 Tg0x 传感器集群。
修改规模与兼容性回归
这次"手术"看似动了底层,其实切口极小,并未伤筋动骨:
| 文件 | 改动量 | 核心使命 |
|---|---|---|
SMC.swift |
+81 / -10 | 修复 ABI 裂缝、自适应解码、动态发现传感器 |
SmcControl.swift |
+51 / -12 | 传感器过滤、按前缀聚合最大温度 |
GPU.swift |
+23 / -8 | 移除 PCI 强依赖,兼容型号名兜底 |
GpuStore.swift |
+11 / -3 | 重构 GPU 统计匹配逻辑 |
project.pbxproj |
+1 / -1 | 修复 SwiftPM 的 SDKROOT 脚本报错 |
更重要的是兼容性 。通过回归验证,Intel 机型的逻辑不仅没被破坏,反而因为修复了 sp78 的小数位精度而变得更好。命名传感器(如 CPU_0_DIE)在 Intel 机器上依然是第一优先级,回退分支根本不会触发。这才是一个合格的开源重构:不砸别人的饭碗,只接自己的断腿。
五、 复盘:当 AI 越过"门槛",开发者还剩什么?
回顾这次修复,Workbuddy 扮演的角色远不止一个"代码补全工具"。在这个过程中,它深刻展示了 AI 时代解决底层系统问题的新范式。
1. AI 填平了晦涩的"黑盒鸿沟"
如果是传统开发模式,面对"未公开 API"和"报错 0x-1ffffd39",缺乏 OS 开发经验的工程师大概率会卡在查阅无数个过时的 StackOverflow 帖子中。但 Workbuddy 能迅速理解崩溃日志的底层逻辑,通过生成探针代码"反过来"测出结构体大小,从而推导出 88 字节的 ABI 裂缝。AI 把黑盒变成了灰盒,把试错从"盲人摸象"变成了"按图索骥"。
2. 从"我怎么写"变成了"我怎么设计"
在这个项目中,核心的困难不在于写出 Swift 的语法,而在于"传感器怎么聚合更合理"、"标识符与显示名分离"这样的架构决策。Workbuddy 负责生成 80% 的样板代码、适配和修复草稿;而人负责验证、测试、重写那 20% 的关键逻辑。AI 负责往前冲,人负责拉缰绳。 这种协作模式,极大拉高了个人能力的上限。
3. AI 无法替你承担"最后一步"
AI 可以写出 fromFLT 解码器,但它没法替你处理 Xcode 的签名证书;它可以帮你把 pciMatch 改为可选,但无法替你承担代码合并到主干后的开源维护责任。"写代码"的门槛在变低,但"为结果负责"的门槛永远在。
六、 结尾:停在 5 年前的代码,也有权利跑在新 M6 上
10k Star,380 Fork,35 Contributors,48 Releases。这些数字曾经代表一个活跃的项目,后来变成了一座数字纪念碑。
但 AI 没有让旧代码变新,它只是帮我们跨过了时间。真正复活这个项目的,不是某个模型,而是"还有人愿意为它再编译一次"。
开源不是免费劳动力。AI 能降低维护门槛,但不能替代责任、测试和社区。
下一个 5 年,也许它还会再次停更。但至少现在,它跑在了这台 Mac Mini 6 上,读出了 32 °C 的 CPU 温度,伴随继续折腾大模型评测。
喜欢本文的朋友 欢迎关注、点赞、收藏 一键三连,大家的支持是写文章最大的动力