3、CPU监控实战:使用/proc/stat读取CPU利用率、脚本实现CPU负载采集与告警
3.1 为什么盯着CPU?
做嵌入式开发的朋友都知道,CPU是系统的“心脏”。
它一旦跑满,整个系统就可能卡死、丢数据、甚至重启。我在项目中遇到过好几次,设备在野外运行了几个月,突然某天CPU飙到100%,结果日志全丢了。嗯,从那以后,CPU监控就成了我脚本里的“标配”。
说白了,CPU利用率就是告诉你——这颗芯片现在有多忙。忙到什么程度?是正常干活,还是在空转?这些数据,都藏在Linux的/proc/stat文件里。
3.2 /proc/stat 到底长什么样?
你可以在终端里直接敲:
cat /proc/stat
输出大概是这样:
cpu 12345 678 9012 345678 123 456 789 0 0 0
cpu0 6000 300 4500 172000 60 200 400 0 0 0
cpu1 6345 378 4512 173678 63 256 389 0 0 0
intr ......
ctxt ......
btime ......
processes ......
procs_running 1
procs_blocked 0
第一行是总的CPU数据。后面每一行对应一个核心。
这些数字代表什么?我习惯把它们分成三组:
| 字段 | 含义 | 说明 |
|---|---|---|
| user (第1列) | 用户态时间 | 跑应用程序的时间 |
| nice (第2列) | 低优先级用户态时间 | 被“nice”过的进程 |
| system (第3列) | 内核态时间 | 系统调用、驱动等 |
| idle (第4列) | 空闲时间 | CPU啥也没干 |
| iowait (第5列) | 等待I/O时间 | 等磁盘、网络等 |
| irq (第6列) | 硬中断时间 | 硬件中断处理 |
| softirq (第7列) | 软中断时间 | 软件中断处理 |
| steal (第8列) | 被虚拟机偷走的时间 | 嵌入式一般用不到 |
| guest (第9列) | 客户机时间 | 跑虚拟机时才有 |
| guest_nice (第10列) | 客户机低优先级时间 | 同上 |
3.3 怎么算CPU利用率?
CPU利用率不是直接读出来的。你得算。
公式其实很简单:
CPU利用率 = (总时间 - 空闲时间) / 总时间 × 100%
但这里有个坑——/proc/stat给的是累计值,不是瞬时值。所以你需要采样两次,算差值。
我刚开始写监控脚本时,就犯过这个错。直接拿第一次读到的数据算,结果CPU利用率一直是0%。后来才反应过来:你得等一秒,再读一次,然后算变化量。
具体步骤:
- 第一次读取,记录所有时间值
- sleep 1秒
- 第二次读取,再记录所有时间值
- 计算差值:
Δ总时间 = 新总时间 - 旧总时间 - 计算差值:
Δ空闲时间 = 新空闲时间 - 旧空闲时间 - 利用率 = (Δ总时间 - Δ空闲时间) / Δ总时间 × 100%
3.4 脚本实现:CPU负载采集
下面是我常用的一个Shell脚本。它采集CPU利用率,并支持告警。
#!/bin/bash
# cpu_monitor.sh - CPU利用率采集与告警脚本
# 用法: ./cpu_monitor.sh [阈值] [采样间隔秒数]
THRESHOLD=${1:-80} # 默认告警阈值80%
INTERVAL=${2:-1} # 默认采样间隔1秒
# 第一次读取
OLD=$(cat /proc/stat | grep '^cpu ' | head -1)
OLD_USER=$(echo $OLD | awk '{print $2}')
OLD_NICE=$(echo $OLD | awk '{print $3}')
OLD_SYSTEM=$(echo $OLD | awk '{print $4}')
OLD_IDLE=$(echo $OLD | awk '{print $5}')
OLD_IOWAIT=$(echo $OLD | awk '{print $6}')
OLD_IRQ=$(echo $OLD | awk '{print $7}')
OLD_SOFTIRQ=$(echo $OLD | awk '{print $8}')
OLD_TOTAL=$((OLD_USER + OLD_NICE + OLD_SYSTEM + OLD_IDLE + OLD_IOWAIT + OLD_IRQ + OLD_SOFTIRQ))
sleep $INTERVAL
# 第二次读取
NEW=$(cat /proc/stat | grep '^cpu ' | head -1)
NEW_USER=$(echo $NEW | awk '{print $2}')
NEW_NICE=$(echo $NEW | awk '{print $3}')
NEW_SYSTEM=$(echo $NEW | awk '{print $4}')
NEW_IDLE=$(echo $NEW | awk '{print $5}')
NEW_IOWAIT=$(echo $NEW | awk '{print $6}')
NEW_IRQ=$(echo $NEW | awk '{print $7}')
NEW_SOFTIRQ=$(echo $NEW | awk '{print $8}')
NEW_TOTAL=$((NEW_USER + NEW_NICE + NEW_SYSTEM + NEW_IDLE + NEW_IOWAIT + NEW_IRQ + NEW_SOFTIRQ))
# 计算差值
DELTA_TOTAL=$((NEW_TOTAL - OLD_TOTAL))
DELTA_IDLE=$((NEW_IDLE - OLD_IDLE))
# 计算利用率(保留整数)
if [ $DELTA_TOTAL -ne 0 ]; then
CPU_USAGE=$(( (DELTA_TOTAL - DELTA_IDLE) * 100 / DELTA_TOTAL ))
else
CPU_USAGE=0
fi
# 输出结果
echo "CPU利用率: ${CPU_USAGE}%"
# 告警判断
if [ $CPU_USAGE -ge $THRESHOLD ]; then
echo "⚠️ 告警: CPU利用率 ${CPU_USAGE}% 超过阈值 ${THRESHOLD}%"
# 这里可以加邮件、短信、写入日志等操作
# logger "CPU告警: ${CPU_USAGE}%"
fi
- 脚本只读第一行
cpu,那是所有核心的总和 - 用
awk提取每一列,清晰明了 - 告警阈值和间隔都可以通过参数调整
3.5 进阶:每个核心单独监控
有时候你只想看某个核心。比如双核芯片,一个核跑业务,一个核跑通信。这时候总利用率可能不高,但某个核心已经爆了。
我改一下脚本,让它支持指定核心:
#!/bin/bash
# cpu_core_monitor.sh - 指定核心监控
CORE=${1:-0} # 默认监控cpu0
THRESHOLD=${2:-80}
INTERVAL=${3:-1}
# 第一次读取
OLD=$(cat /proc/stat | grep "^cpu${CORE} " | head -1)
if [ -z "$OLD" ]; then
echo "错误: 核心 cpu${CORE} 不存在"
exit 1
fi
# 后面的计算逻辑和上面一样,只是把 'cpu ' 换成 "cpu${CORE} "
# ...(省略重复代码,实际使用时复制上面脚本并修改grep模式)
/proc/stat里的cpu行是总和,但每个核心的采样时刻可能略有偏差。所以别纠结,直接用总和行最准。
3.6 长期运行怎么办?
嵌入式设备经常要跑几个月。脚本不能只跑一次就退出了。我建议加个循环:
while true; do
# 执行上面的采集逻辑
# 输出到日志文件
echo "$(date) CPU: ${CPU_USAGE}%" >> /var/log/cpu_usage.log
sleep $INTERVAL
done
但要注意:日志文件会越来越大。我习惯加个日志轮转,或者只保留最近1000条。
另外,/proc/stat是虚拟文件系统,读它不会磨损Flash。放心用。
3.7 告警怎么发?
脚本里已经写了告警判断。但实际项目中,你总得做点什么。
我常用的几种方式:
- 写系统日志:用
logger命令,配合syslog或rsyslog - 写入文件:比如
/tmp/cpu_alarm,让其他监控进程去读 - 触发GPIO:嵌入式板子上接个LED,CPU高了就闪灯
- 发MQTT消息:如果设备联网,直接推送到云端
我个人习惯是:先写日志,再发告警。因为有时候网络断了,MQTT发不出去,但日志还在。
3.8 总结一下
CPU监控其实不复杂。核心就三步:
- 读
/proc/stat,拿到累计时间 - 等一秒,再读一次
- 算差值,得利用率
脚本写好了,可以放到crontab里,或者用systemd timer定时执行。嵌入式设备资源有限,别搞太复杂的框架,一个Shell脚本就够了。
嗯,下一章我们聊聊内存监控。内存比CPU更坑,因为/proc/meminfo里的“可用内存”和“空闲内存”不是一回事。到时候我详细说。
公众号:蓝海资料掘金营,微信deep3321