在 macOS 上,我们通常使用 iStat Menus 或 Stats 等工具来监控系统。但作为开发者,我想要一个更轻量、更硬核、完全可控的方案:
- ❌ 拒绝 Electron:不想要臃肿的后台进程。
- ❌ 拒绝未知:我需要知道具体的底层数据(如 SSD 的 0E 致命错误)。
- ✅ 全自动:外接硬盘插拔后自动追踪,无需手动修改配置。
- ✅ M3 适配:解决 Apple Silicon M 芯片隐藏 CPU 温度传感器的问题。
经过一番折腾,我编写了一个 All-in-One 的 Shell 脚本,集成了 CPU 温度/压力、硬盘健康度(含 0E 检测)、外接硬盘自动追踪以及电池深度体检。
下面是实现方案。
🛠️ 前置准备#
我们需要几个轻量级的命令行工具来获取底层数据。
1. 安装依赖#
打开终端,使用 Homebrew 安装:
1
2
3
4
5
|
# 安装 smartmon tools (用于读取硬盘 SMART 信息)
brew install smartmontools
# 安装 terminal-notifier (用于发送原生系统通知)
brew install terminal-notifier
|
2. 配置 Sudo 免密#
因为读取 smartctl 和 powermetrics 需要 root 权限,为了让脚本在后台静默运行,我们需要配置 sudo 白名单。
运行 sudo visudo,在文件最后添加以下内容(将 your_username 替换为你的用户名):
1
|
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,填入以下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
#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 编译,并移动到系统目录:
1
2
|
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
|
#!/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,你会得到一份极其详细的报告:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
========================================
📟 系统全维体检报告
========================================
🧠 核心算力
----------------------------------------
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 中添加:
1
|
* * * * * /bin/bash /Users/你的用户名/script/temp_monitor.sh >/dev/null 2>&1
|
现在,你的 Mac 拥有了一套完全私有、透明且硬核的健康监控系统。只要通知中心安安静静,就说明你的 Mac 依旧健康!