打造 macOS 终极监控脚本:M 芯片温度解锁 + 硬盘 0E 预警 + 电池体检
在 macOS 上,我们通常使用 iStat Menus 或 Stats 等工具来监控系统。但作为开发者,我想要一个更轻量、更硬核、完全可控的方案:
- ❌ 拒绝 Electron:不想要臃肿的后台进程。
- ❌ 拒绝未知:我需要知道具体的底层数据(如 SSD 的 0E 致命错误)。
- ✅ 全自动:外接硬盘插拔后自动追踪,无需手动修改配置。
- ✅ M3 适配:解决 Apple Silicon M 芯片隐藏 CPU 温度传感器的问题。
经过一番折腾,我编写了一个 All-in-One 的 Shell 脚本,集成了 CPU 温度/压力、硬盘健康度(含 0E 检测)、外接硬盘自动追踪以及电池深度体检。
下面是实现方案。
🛠️ 前置准备
我们需要几个轻量级的命令行工具来获取底层数据。
1. 安装依赖
打开终端,使用 Homebrew 安装:
# 安装 smartmon tools (用于读取硬盘 SMART 信息)
brew install smartmontools
# 安装 terminal-notifier (用于发送原生系统通知)
brew install terminal-notifier
2. 配置 Sudo 免密
因为读取 smartctl 和 powermetrics 需要 root 权限,为了让脚本在后台静默运行,我们需要配置 sudo 白名单。
运行 sudo visudo,在文件最后添加以下内容(将 your_username 替换为你的用户名):
your_username ALL=(ALL) NOPASSWD: /opt/homebrew/bin/smartctl, /usr/bin/powermetrics
🔓 突破 M 芯片限制:自制 CPU 温度读取工具
在 M 芯片上,普通的 powermetrics 命令不再直接输出 CPU 核心温度。我们需要利用 C 语言调用 Apple 的私有 HID 接口来获取。
1. 创建源码 get_temp.c
新建一个文件 get_temp.c,填入以下代码:
#include <CoreFoundation/CoreFoundation.h>
#include <IOKit/IOKitLib.h>
// 声明 Apple 私有 HID 接口
typedef struct __IOHIDEventSystemClient * IOHIDEventSystemClientRef;
typedef struct __IOHIDServiceClient * IOHIDServiceClientRef;
typedef struct __IOHIDEvent * IOHIDEventRef;
#define kIOHIDEventTypeTemperature 15
extern IOHIDEventSystemClientRef IOHIDEventSystemClientCreate(CFAllocatorRef allocator);
extern int IOHIDEventSystemClientSetMatching(IOHIDEventSystemClientRef client, CFDictionaryRef match);
extern CFArrayRef IOHIDEventSystemClientCopyServices(IOHIDEventSystemClientRef client);
extern CFStringRef IOHIDServiceClientCopyProperty(IOHIDServiceClientRef service, CFStringRef key);
extern IOHIDEventRef IOHIDServiceClientCopyEvent(IOHIDServiceClientRef service, int64_t type, int32_t options, int64_t timestamp);
extern double IOHIDEventGetFloatValue(IOHIDEventRef event, int32_t field);
int main() {
IOHIDEventSystemClientRef system = IOHIDEventSystemClientCreate(kCFAllocatorDefault);
if (!system) return 1;
int page = 0xff00; int usage = 0x0005;
CFNumberRef pageNum = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &page);
CFNumberRef usageNum = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &usage);
const void *keys[2] = { CFSTR("PrimaryUsagePage"), CFSTR("PrimaryUsage") };
const void *values[2] = { pageNum, usageNum };
CFDictionaryRef matchDict = CFDictionaryCreate(kCFAllocatorDefault, keys, values, 2, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
IOHIDEventSystemClientSetMatching(system, matchDict);
CFArrayRef services = IOHIDEventSystemClientCopyServices(system);
if (!services) return 1;
CFIndex count = CFArrayGetCount(services);
double maxTemp = 0.0;
int found = 0;
for (CFIndex i = 0; i < count; i++) {
IOHIDServiceClientRef service = (IOHIDServiceClientRef)CFArrayGetValueAtIndex(services, i);
CFStringRef nameRef = IOHIDServiceClientCopyProperty(service, CFSTR("Product"));
if (!nameRef) continue;
char name[256];
CFStringGetCString(nameRef, name, 256, kCFStringEncodingUTF8);
CFRelease(nameRef);
if (strstr(name, "SOC MDI") || strstr(name, "Die") || strstr(name, "PMU tdev") || strstr(name, "Cluster")) {
IOHIDEventRef event = IOHIDServiceClientCopyEvent(service, kIOHIDEventTypeTemperature, 0, 0);
if (event) {
double temp = IOHIDEventGetFloatValue(event, (kIOHIDEventTypeTemperature << 16));
if (temp > 20.0 && temp < 120.0) {
if (temp > maxTemp) maxTemp = temp;
found = 1;
}
CFRelease(event);
}
}
}
if (found) printf("%.1f\n", maxTemp); else printf("0.0\n");
return 0;
}
2. 编译并安装
使用系统自带的 clang 编译,并移动到系统目录:
clang -framework CoreFoundation -framework IOKit -o get_temp get_temp.c
sudo mv get_temp /usr/local/bin/get_temp
现在,你在终端输入 get_temp 就能拿到精确的 CPU 温度了。
📜 终极监控脚本
这是脚本的最终形态。它不仅会在后台静默监控,如果手动运行,还会输出一份极其漂亮的 “系统体检报告”。
保存以下代码为 ~/script/temp_monitor.sh:
#!/bin/bash
# ================= 配置区域 =================
EXTERNAL_DRIVE_NAME="disk" # 你的移动硬盘卷名 (在 /Volumes 下的名字)
THRESHOLD_CPU=85 # CPU 报警阈值 (°C)
THRESHOLD_DISK=55 # 硬盘报警阈值 (°C)
THRESHOLD_BATT=40 # 电池报警阈值 (°C)
THRESHOLD_HEALTH=90 # 硬盘健康度报警阈值 (%)
# 环境变量
export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
# ================= 1. 获取基础数据 =================
# --- A. CPU ---
# 调用我们自制的 C 工具
CPU_TEMP=$(/usr/local/bin/get_temp)
if [[ -n "$CPU_TEMP" && "$CPU_TEMP" != "0.0" ]]; then
CPU_TEMP_INT=${CPU_TEMP%.*}
else
CPU_TEMP_INT=0; CPU_TEMP="N/A"
fi
# 获取系统热压力
CPU_PRESSURE=$(sudo /usr/bin/powermetrics -n 1 --samplers thermal 2>/dev/null | grep "Current pressure level" | awk '{print $4}')
if [[ -z "$CPU_PRESSURE" ]]; then CPU_PRESSURE="Unknown"; fi
# --- B. 电池 (深度数据) ---
BATT_INFO=$(ioreg -rn AppleSmartBattery)
# 温度
RAW_BATT_TEMP=$(echo "$BATT_INFO" | grep "\"Temperature\" =" | awk '{print $3}')
if [[ -n "$RAW_BATT_TEMP" ]]; then
if [[ "$RAW_BATT_TEMP" -gt 100 ]]; then BATT_TEMP=$(($RAW_BATT_TEMP / 100)); else BATT_TEMP=$RAW_BATT_TEMP; fi
else BATT_TEMP=0; fi
# 循环与容量
BATT_CYCLES=$(echo "$BATT_INFO" | grep "\"CycleCount\" =" | awk '{print $3}')
BATT_MAX_CAP=$(echo "$BATT_INFO" | grep "\"AppleRawMaxCapacity\" =" | awk '{print $3}')
BATT_DESIGN_CAP=$(echo "$BATT_INFO" | grep "\"DesignCapacity\" =" | awk '{print $3}')
if [[ -n "$BATT_MAX_CAP" && -n "$BATT_DESIGN_CAP" && "$BATT_DESIGN_CAP" -gt 0 ]]; then
BATT_HEALTH=$(( 100 * BATT_MAX_CAP / BATT_DESIGN_CAP ))
else BATT_HEALTH="?"; fi
# --- C. 内存 ---
RAM_LEVEL=$(sysctl -n kern.memorystatus_vm_pressure_level)
case "$RAM_LEVEL" in
1) RAM_TXT="正常" ;; 2) RAM_TXT="⚠️ 压力高" ;; *) RAM_TXT="🔥 严重" ;;
esac
# ================= 2. 报告生成函数 =================
generate_disk_report() {
local DISK_DEV=$1
local DISK_TYPE=$2
echo "正在读取 $DISK_DEV ($DISK_TYPE) ..."
SMART_DATA=$(sudo /opt/homebrew/bin/smartctl -a $DISK_DEV 2>/dev/null)
# 提取关键指标
MODEL=$(echo "$SMART_DATA" | grep "Model Number:" | awk -F: '{print $2}' | xargs)
if [[ -z "$MODEL" ]]; then MODEL="Unknown"; fi
TEMP=$(echo "$SMART_DATA" | grep -i "Temperature:" | awk '{print $2}')
if [[ -z "$TEMP" ]]; then TEMP="0"; fi
# 计算健康度
USED_PCT=$(echo "$SMART_DATA" | grep "Percentage Used" | awk '{print $3}' | tr -d '%')
if [[ -n "$USED_PCT" ]]; then HEALTH=$((100 - USED_PCT)); else HEALTH="N/A"; USED_PCT="?"; fi
# 统计数据
WRITTEN=$(echo "$SMART_DATA" | grep "Data Units Written" | awk -F'[][]' '{print $2}')
HOURS=$(echo "$SMART_DATA" | grep "Power On Hours" | awk '{print $4}' | tr -d ',')
if [[ -n "$HOURS" ]]; then DAYS=$((HOURS / 24)); TIME_TXT="${HOURS}小时(${DAYS}天)"; else TIME_TXT="N/A"; fi
UNSAFE=$(echo "$SMART_DATA" | grep "Unsafe Shutdowns" | awk '{print $3}' | tr -d ',')
if [[ -z "$UNSAFE" ]]; then UNSAFE="0"; fi
# 0E 致命错误检测
ERR_0E=$(echo "$SMART_DATA" | grep "Media and Data Integrity Errors" | awk '{print $NF}')
if [[ -z "$ERR_0E" ]]; then ERR_0E="N/A"; fi
# 打印可视化报告
echo "----------------------------------------"
if [[ "$DISK_TYPE" == "Internal" ]]; then echo " 🍎 本地硬盘 (macOS System)"; else echo " 💾 移动硬盘 ($DISK_DEV)"; fi
echo "----------------------------------------"
echo "📦 型号: $MODEL"
echo "🌡️ 温度: ${TEMP}°C"
echo "❤️ 寿命: ${HEALTH}% (已用 ${USED_PCT}%)"
echo "📝 写入: $WRITTEN"
echo "⏳ 时间: $TIME_TXT"
echo "🔌 掉电: $UNSAFE 次"
echo "✨ 0E错: $ERR_0E (数据完整性)"
echo ""
# 回传数据给主逻辑
eval "${DISK_TYPE}_TEMP=$TEMP"
eval "${DISK_TYPE}_0E=$ERR_0E"
eval "${DISK_TYPE}_HEALTH=$HEALTH"
}
# ================= 3. 执行输出 =================
echo ""
echo "========================================"
echo " 📟 系统全维体检报告 "
echo "========================================"
echo ""
# 核心算力
echo "🧠 核心算力"
echo "----------------------------------------"
echo "CPU 温度: ${CPU_TEMP}°C"
echo "CPU 压力: ${CPU_PRESSURE}"
echo "内存状态: ${RAM_TXT}"
echo ""
# 电池健康
echo "🔋 电池健康"
echo "----------------------------------------"
echo "🌡️ 温度: ${BATT_TEMP}°C"
echo "🔄 循环: ${BATT_CYCLES} 次"
echo "❤️ 寿命: ${BATT_HEALTH}%"
echo "🔋 容量: ${BATT_MAX_CAP} mAh (当前) / ${BATT_DESIGN_CAP} mAh (出厂)"
echo ""
# 硬盘检测
generate_disk_report "/dev/disk0" "Internal"
# 自动追踪移动硬盘 (APFS 兼容)
MOUNT_DEV=$(df "/Volumes/$EXTERNAL_DRIVE_NAME" 2>/dev/null | awk 'NR==2 {print $1}')
if [[ -n "$MOUNT_DEV" ]]; then
DISK_DEVICE=$(echo "$MOUNT_DEV" | sed 's/s[0-9]*$//')
generate_disk_report "$DISK_DEVICE" "External"
else
echo "----------------------------------------"
echo " 💾 移动硬盘 ($EXTERNAL_DRIVE_NAME)"
echo "----------------------------------------"
echo "⚠️ 状态: 未连接"
echo ""
External_TEMP=0; External_0E=-1; External_HEALTH="N/A"
fi
# ================= 4. 告警逻辑 (通知中心) =================
MSG=""
TITLE="🔥 系统严重告警"
# 0E 错误 (最高优先级)
if [[ "$Internal_0E" != "N/A" && "$Internal_0E" -gt 0 ]]; then MSG="${MSG}💥系统盘0E错误(${Internal_0E})"; fi
if [[ "$External_0E" != "N/A" && "$External_0E" -ge 0 && "$External_0E" -gt 0 ]]; then
if [[ -n "$MSG" ]]; then MSG="${MSG}, "; fi
MSG="${MSG}💥移动盘0E错误(${External_0E})"
fi
# 寿命预警
if [[ "$Internal_HEALTH" != "N/A" && "$Internal_HEALTH" -lt "$THRESHOLD_HEALTH" ]]; then
if [[ -n "$MSG" ]]; then MSG="${MSG}, "; fi
MSG="${MSG}系统盘寿命低(${Internal_HEALTH}%)"
fi
# 过热预警
if [[ "$External_TEMP" -ge "$THRESHOLD_DISK" && "$External_TEMP" -gt 0 ]]; then
if [[ -n "$MSG" ]]; then MSG="${MSG}, "; fi
MSG="${MSG}移动盘热:${External_TEMP}°C"
fi
if [[ "$CPU_TEMP_INT" -ge "$THRESHOLD_CPU" ]]; then
if [[ -n "$MSG" ]]; then MSG="${MSG}, "; fi
MSG="${MSG}CPU过热:${CPU_TEMP}°C"
fi
# 发送通知
if [[ -n "$MSG" ]]; then
terminal-notifier -message "$MSG" -title "$TITLE" -sound default -group "system_monitor"
fi
📊 运行效果
在终端手动运行 bash temp_monitor.sh,你会得到一份极其详细的报告:
========================================
📟 系统全维体检报告
========================================
🧠 核心算力
----------------------------------------
CPU 温度: 39.5°C
CPU 压力: Nominal
内存状态: 正常
🔋 电池健康
----------------------------------------
🌡️ 温度: 31°C
🔄 循环: 156 次
❤️ 寿命: 98%
🔋 容量: 4350 mAh (当前) / 4436 mAh (出厂)
----------------------------------------
🍎 本地硬盘 (macOS System)
----------------------------------------
📦 型号: APPLE SSD AP0512Z
🌡️ 温度: 35°C
❤️ 寿命: 100% (已用 0%)
📝 写入: 20.5 TB
⏳ 时间: 1500小时(62天)
🔌 掉电: 12 次
✨ 0E错: 0 (数据完整性)
----------------------------------------
💾 移动硬盘 (/dev/disk4)
----------------------------------------
📦 型号: WDC WDS960G2G0C
🌡️ 温度: 46°C
❤️ 寿命: 98% (已用 2%)
✨ 0E错: 0 (数据完整性)
⚙️ 自动化运行
如果希望它每分钟自动在后台检查(只在异常时弹窗),可以使用 crontab 或 launchd。
简单方法,在 crontab 中添加:
* * * * * /bin/bash /Users/你的用户名/script/temp_monitor.sh >/dev/null 2>&1
现在,你的 Mac 拥有了一套完全私有、透明且硬核的健康监控系统。只要通知中心安安静静,就说明你的 Mac 依旧健康!