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列) 客户机低优先级时间 同上
我的小技巧: 嵌入式设备一般只看前7列。第8列以后,大部分场景下都是0。你想想看,一个单片机跑Linux,哪来的虚拟机?

3.3 怎么算CPU利用率?

CPU利用率不是直接读出来的。你得算。

公式其实很简单:

CPU利用率 = (总时间 - 空闲时间) / 总时间 × 100%

但这里有个坑——/proc/stat给的是累计值,不是瞬时值。所以你需要采样两次,算差值。

我刚开始写监控脚本时,就犯过这个错。直接拿第一次读到的数据算,结果CPU利用率一直是0%。后来才反应过来:你得等一秒,再读一次,然后算变化量。

具体步骤:

  1. 第一次读取,记录所有时间值
  2. sleep 1秒
  3. 第二次读取,再记录所有时间值
  4. 计算差值:Δ总时间 = 新总时间 - 旧总时间
  5. 计算差值:Δ空闲时间 = 新空闲时间 - 旧空闲时间
  6. 利用率 = (Δ总时间 - Δ空闲时间) / Δ总时间 × 100%
注意: 采样间隔太短(比如0.1秒)会导致利用率波动很大。我建议至少1秒。如果是做长期监控,5秒一次更稳。

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模式)
避坑指南: 我曾经在树莓派上跑这个脚本,发现cpu0和cpu1的利用率加起来不等于总利用率。后来查了资料才知道——/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命令,配合syslogrsyslog
  • 写入文件:比如/tmp/cpu_alarm,让其他监控进程去读
  • 触发GPIO:嵌入式板子上接个LED,CPU高了就闪灯
  • 发MQTT消息:如果设备联网,直接推送到云端

我个人习惯是:先写日志,再发告警。因为有时候网络断了,MQTT发不出去,但日志还在。

注意: 告警频率别太高。如果CPU一直高,你每1秒发一次告警,那系统会被你搞崩。我一般加个“静默期”,比如5分钟内只告警一次。

3.8 总结一下

CPU监控其实不复杂。核心就三步:

  1. /proc/stat,拿到累计时间
  2. 等一秒,再读一次
  3. 算差值,得利用率

脚本写好了,可以放到crontab里,或者用systemd timer定时执行。嵌入式设备资源有限,别搞太复杂的框架,一个Shell脚本就够了。

嗯,下一章我们聊聊内存监控。内存比CPU更坑,因为/proc/meminfo里的“可用内存”和“空闲内存”不是一回事。到时候我详细说。


公众号:蓝海资料掘金营,微信deep3321