linux CPU占用异常升高问题的排查方法
引言:《哲学家和船夫的故事》
一个船夫在湍急的河水中驾驶小船,船上坐着一位哲学家。船平稳航行后,哲学家开口问船夫:“你懂得历史吗?”船夫摇了摇头,哲学家惋惜地说:“那你失去了一半的生命。”
没过多久,哲学家又问道:“你研究过数学吗?”船夫再次摇头,哲学家叹气道:“那你失去了一半以上的生命。”
正当两人继续交谈时,一阵狂风掀起巨浪,直接把小船掀翻,两人都被抛进了湍急的河水中。这时船夫大声喊道:“你会游泳吗?”在水里拼命挣扎的哲学家慌乱地回答:“不会!”船夫平静地说:“那你就失去了整个生命。”
--- 我最近正在尝试阅读一些哲学知识:技术和哲学都需要付出巨大的思考,思考越多,收获越多。当然,思考的同时,我们需要实践,避免只掌握空洞的理论,脱离了实际。
正文:
- 问题描述:arm-linux 端运行的业务进程,使用 top 指令去看CPU占用,即使在业务空闲状态下,CPU占用也维持在 50% 左右。
- 排查思路:
(1)内存泄漏导致内核频繁进行页面交换
(2)线程数异常增多
(3)资源累积导致锁竞争
(4)业务代码中存在死循环,如,while(1) {} - 排查过程
思路中的(1)和(3) 的排查手段暂未涉及,此处不做深入讨论。
线程数异常增多:
(1)在系统中,使用一个脚本进行线程数的监控,同时在业务进程中为每个线程赋予唯一名称,在出问题时可大致定位到出问题的线程,缩小范围
(2)参考脚本如下,通过粗略计算每个线程的CPU占用时间,反推CPU占用的情况
点击查看代码
#!/bin/sh
# ================= 配置 =================
TARGET_NAME="$1"
if [ -z "$TARGET_NAME" ]; then
echo "用法: $0 <进程名>"
echo "例如: $0 java"
exit 1
fi
OUTPUT="/root/thread_cpu_log.csv"
INTERVAL=2
MAX_LINES=5000
# 初始化
echo "Timestamp, TID, Thread_Name, CPU_Percent, State" > "$OUTPUT"
echo "开始监控进程: $TARGET_NAME"
echo "日志文件: $(pwd)/$OUTPUT"
# 获取 CLK_TCK
CLK_TCK=$(getconf CLK_TCK 2>/dev/null)
[ -z "$CLK_TCK" ] && CLK_TCK=100
RUN=0
PREV_FILE="/tmp/mon_prev_$$"
CURR_FILE="/tmp/mon_curr_$$"
> "$PREV_FILE"
trap 'rm -f $PREV_FILE $CURR_FILE; exit' INT TERM
while true; do
RUN=$((RUN + 1))
TS=$(date '+%Y-%m-%d %H:%M:%S')
# 1. 查找 PID (尝试多种匹配方式)
# 先用 -x 精确匹配,如果失败则去掉 -x 模糊匹配
PID=$(pgrep -x "$TARGET_NAME" | head -n 1)
if [ -z "$PID" ]; then
PID=$(pgrep "$TARGET_NAME" | head -n 1)
if [ -z "$PID" ]; then
if [ $((RUN % 5)) -eq 1 ]; then
echo "[$TS] 警告: 未找到进程 '$TARGET_NAME'。请检查进程名是否正确。"
echo " 提示: 使用 'ps aux | grep $TARGET_NAME' 查看准确名称"
fi
sleep $INTERVAL
continue
else
if [ $((RUN % 5)) -eq 1 ]; then
echo "[$TS] 提示: 使用模糊匹配找到 PID: $PID"
fi
fi
fi
TASK_DIR="/proc/$PID/task"
if [ ! -d "$TASK_DIR" ]; then
echo "[$TS] 错误: /proc/$PID/task 不存在。权限不足或进程已退出。"
sleep $INTERVAL
continue
fi
# 2. 采样当前状态
> "$CURR_FILE"
COUNT=0
for TPATH in "$TASK_DIR"/*/; do
TID=$(basename "$TPATH")
# 读取 stat
read UTIME STIME REST < "$TPATH/stat" 2>/dev/null
# 注意: /proc/pid/stat 的第14列是utime, 第15列是stime
# 但由于 comm 字段可能包含空格或括号,直接 awk 更安全
CPU_TICKS=$(awk '{print $14 + $15}' "$TPATH/stat" 2>/dev/null)
if [ -z "$CPU_TICKS" ]; then continue; fi
# 存入当前文件
echo "$TID $CPU_TICKS" >> "$CURR_FILE"
# 获取线程名和状态
TNAME=$(cat "$TPATH/comm" 2>/dev/null)
TSTATE=$(awk '{print $3}' "$TPATH/stat" 2>/dev/null)
# 3. 计算 CPU
PREV_TICKS=$(awk -v t="$TID" '$1==t {print $2}' "$PREV_FILE")
CPU_PCT="0.00"
if [ -n "$PREV_TICKS" ]; then
DELTA=$((CPU_TICKS - PREV_TICKS))
if [ $DELTA -gt 0 ]; then
CPU_PCT=$(awk "BEGIN{printf \"%.2f\", ($DELTA/$INTERVAL)/$CLK_TCK*100}")
fi
fi
# 4. 【强制】写入日志
echo "$TS, $TID, $TNAME, $CPU_PCT, $TSTATE" >> "$OUTPUT"
COUNT=$((COUNT + 1))
done
# 更新上一轮数据
mv "$CURR_FILE" "$PREV_FILE"
# 5. 反馈
if [ $((RUN % 3)) -eq 0 ]; then
LINES=$(wc -l < "$OUTPUT")
echo "[$TS] 正常: 采集到 $COUNT 个线程, 日志总行数: $LINES"
fi
# 6. 轮转
if [ $MAX_LINES -gt 0 ]; then
CUR_LINES=$(wc -l < "$OUTPUT")
if [ $CUR_LINES -ge $MAX_LINES ]; then
BAK="$OUTPUT.$(date +%s)"
mv "$OUTPUT" "$BAK"
echo "Timestamp, TID, Thread_Name, CPU_Percent, State" > "$OUTPUT"
echo "[$TS] 日志已轮转至 $BAK"
fi
fi
sleep $INTERVAL
done
业务代码中存在死循环:
(1)借助静态代码分析工具/手动走查代码
(2)使用分段隔离的方式,逐步屏蔽代码,位置业务进程运行,看哪一部分的业务代码删掉后,CPU的占用恢复正常 --- 笔者使用此方式最终定位问题。
(3)上述分段隔离方式,我称之为排除法,基础思想是二分法。可以先用脚本排查到出问题的线程范围,再使用此方式精确定位出问题的代码段。
延伸:
(1)线程赋名方法:prctl(PR_SET_NAME, "Net-IO-Handler", 0, 0, 0);
(2)linux 定时器,如果是使用如下方式,那么定时器回调线程的默认名称为进程名,也可能是创建定时器的上一级的线程名称 !!!所以如果想准确,也要在这个回调函数中赋名,方法同上!!!
点击查看代码
timer_t timerid;
struct sigevent sev;
struct itimerspec its;
UserData my_data = {101, "TestTimer"};
// 1. 配置 sigevent
memset(&sev, 0, sizeof(sev));
sev.sigev_notify = SIGEV_THREAD; // 使用线程通知
sev.sigev_notify_function = timer_callback; // 设置回调
sev.sigev_value.sival_ptr = &my_data; // 传递用户数据
sev.sigev_notify_attributes = NULL; // 使用默认线程属性
// 2. 创建定时器
// 使用 CLOCK_MONOTONIC 避免系统时间修改影响
if (timer_create(CLOCK_MONOTONIC, &sev, &timerid) == -1) {
perror("timer_create");
exit(EXIT_FAILURE);
}

浙公网安备 33010602011771号