Bash 脚本内存泄漏与资源管理故障排查 一、概述 Bash 脚本在运维自动化中广泛应用,但不当的资源管理会导致内存泄漏、文件描述符耗尽、进程僵尸等问题。本文详细介绍 Bash 脚本中常见的资源管理问题及其排查方法。
1.1 常见问题类型
问题类型
症状
严重程度
文件描述符泄漏
too many open files 错误
高
后台进程失控
系统负载飙升,进程数爆炸
高
变量内存累积
脚本内存占用持续增长
中
临时文件堆积
磁盘空间快速消耗
中
僵尸进程
defunct 进程累积
低
管道阻塞
脚本 hangs 不退出
中
1.2 排查工具 1 2 3 4 5 6 7 8 9 10 11 12 13 14 ps aux | grep script_name top -p $(pgrep -f script_name) lsof -p <pid> ls -la /proc/<pid>/fd/cat /proc/<pid>/status | grep -i vmpmap -x <pid> ulimit -a
二、文件描述符泄漏 2.1 问题场景 症状 :脚本运行一段时间后报错 bash: cannot create temp file for here-document: No space left on device 或 too many open files
根本原因 :打开的文件、管道、socket 未正确关闭
2.2 典型案例 1 2 3 4 5 6 7 8 9 #!/bin/bash while true ; do exec 3<> /tmp/log_$$.txt echo "log entry" >&3 done
2.3 正确做法 1 2 3 4 5 6 7 8 9 10 11 12 13 #!/bin/bash LOG_FILE="/tmp/script.log" exec 3>> "$LOG_FILE " while true ; do echo "log entry $(date) " >&3 sleep 1 done exec 3>&-
2.4 使用 trap 自动清理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #!/bin/bash exec 3>> /tmp/script.logexec 4<> /tmp/script.lockcleanup () { echo "Cleaning up..." >&2 exec 3>&- exec 4>&- rm -f /tmp/script.lock exit $1 } trap 'cleanup $?' EXIT INT TERMfor i in {1..100}; do echo "Processing $i " >&3 sleep 0.1 done
2.5 排查步骤 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 #!/bin/bash PID=$1 if [ -z "$PID " ]; then echo "Usage: $0 <pid>" exit 1 fi echo "=== 进程 $PID 文件描述符分析 ===" FD_COUNT=$(ls /proc/$PID /fd 2>/dev/null | wc -l) echo "打开的文件描述符数量:$FD_COUNT " MAX_FD=$(cat /proc/$PID /limits 2>/dev/null | grep "open files" | awk '{print $4}' ) echo "系统限制:$MAX_FD " echo "" echo "=== 文件描述符详情 ===" ls -la /proc/$PID /fd/echo "" echo "=== FD 类型统计 ===" ls -la /proc/$PID /fd/ | awk '{print $1}' | sort | uniq -c
三、后台进程失控 3.1 问题场景 症状 :系统进程数激增,ps aux | wc -l 显示异常高的进程数
根本原因 :后台进程未正确等待或清理
3.2 典型案例 1 2 3 4 5 6 7 8 9 10 11 #!/bin/bash for i in {1..1000}; do sleep 10 & done echo "Done"
3.3 正确做法 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 #!/bin/bash MAX_JOBS=10 JOB_COUNT=0 for i in {1..100}; do ( echo "Processing task $i " sleep 2 ) & JOB_COUNT=$((JOB_COUNT + 1 )) if [ $JOB_COUNT -ge $MAX_JOBS ]; then wait -n JOB_COUNT=$((JOB_COUNT - 1 )) fi done wait echo "All tasks completed"
3.4 使用作业控制 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #!/bin/bash for i in {1..5}; do sleep $((RANDOM % 10 )) & echo "Started job $! (PID)" done jobs -lwait %1 wait kill %2
3.5 排查失控进程 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 #!/bin/bash SCRIPT_NAME="${1:-my_script} " echo "=== 查找 $SCRIPT_NAME 相关进程 ===" MAIN_PID=$(pgrep -f "$SCRIPT_NAME " | head -1) if [ -z "$MAIN_PID " ]; then echo "未找到主进程" exit 1 fi echo "主进程 PID: $MAIN_PID " echo "" echo "=== 子进程树 ===" pstree -p $MAIN_PID CHILD_COUNT=$(pgrep -P $MAIN_PID | wc -l) echo "" echo "子进程数量:$CHILD_COUNT " ZOMBIE_COUNT=$(ps -o stat = --ppid $MAIN_PID | grep -c Z || echo 0) echo "僵尸进程数量:$ZOMBIE_COUNT " if [ "$ZOMBIE_COUNT " -gt 0 ]; then echo "" echo "=== 僵尸进程详情 ===" ps -o pid,stat ,cmd --ppid $MAIN_PID | grep Z fi
四、变量内存累积 4.1 问题场景 症状 :长时间运行的脚本内存占用持续增长
根本原因 :数组或变量不断追加内容但未清理
4.2 典型案例 1 2 3 4 5 6 7 8 9 #!/bin/bash LOG_BUFFER="" while true ; do LOG_BUFFER="${LOG_BUFFER} $(date) : log entry\n" sleep 0.1 done
4.3 正确做法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #!/bin/bash LOG_BUFFER="" MAX_LINES=1000 LINE_COUNT=0 while true ; do LOG_BUFFER="${LOG_BUFFER} $(date) : log entry\n" LINE_COUNT=$((LINE_COUNT + 1 )) if [ $LINE_COUNT -ge $MAX_LINES ]; then echo -e "$LOG_BUFFER " >> /tmp/script.log LOG_BUFFER="" LINE_COUNT=0 fi sleep 0.1 done
4.4 数组内存管理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #!/bin/bash declare -a RESULTSfor i in {1..100000}; do RESULTS+=("result_$i " ) done declare -a RESULTSMAX_SIZE=1000 for i in {1..100000}; do RESULTS+=("result_$i " ) if [ ${#RESULTS[@]} -ge $MAX_SIZE ]; then echo "Processing ${#RESULTS[@]} results..." unset RESULTS declare -a RESULTS fi done
4.5 内存监控脚本 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 #!/bin/bash PID=$1 INTERVAL=${2:-5} if [ -z "$PID " ]; then echo "Usage: $0 <pid> [interval]" exit 1 fi echo "监控进程 $PID 的内存使用 (每${INTERVAL} 秒)" echo "按 Ctrl+C 停止" echo "" while true ; do if [ ! -d "/proc/$PID " ]; then echo "进程 $PID 已退出" break fi VmRSS=$(grep VmRSS /proc/$PID /status 2>/dev/null | awk '{print $2, $3}' ) VmSize=$(grep VmSize /proc/$PID /status 2>/dev/null | awk '{print $2, $3}' ) TIMESTAMP=$(date '+%H:%M:%S' ) echo "[$TIMESTAMP ] VmRSS: $VmRSS | VmSize: $VmSize " sleep $INTERVAL done
五、临时文件管理 5.1 问题场景 症状 :/tmp 目录空间快速消耗
根本原因 :临时文件创建后未删除
5.2 典型案例 1 2 3 4 5 6 7 8 9 #!/bin/bash while true ; do TEMP_FILE="/tmp/data_$$_$RANDOM .tmp" echo "data" > "$TEMP_FILE " done
5.3 正确做法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #!/bin/bash TEMP_FILE=$(mktemp /tmp/script.XXXXXX) TEMP_DIR=$(mktemp -d /tmp/script_dir.XXXXXX) cleanup () { rm -f "$TEMP_FILE " rm -rf "$TEMP_DIR " echo "临时文件已清理" >&2 } trap cleanup EXIT INT TERMecho "data" > "$TEMP_FILE " mkdir -p "$TEMP_DIR /output"
5.4 临时文件最佳实践 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #!/bin/bash SAFE_TEMP=$(mktemp ) SAFE_DIR=$(mktemp -d) SAFE_FILE_IN_DIR=$(mktemp "$SAFE_DIR " /file.XXXXXX) chmod 600 "$SAFE_TEMP " trap 'rm -f "$SAFE_TEMP"; rm -rf "$SAFE_DIR"' EXIT
5.5 清理泄漏的临时文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #!/bin/bash TEMP_DIR="/tmp" MAX_AGE_HOURS=24 SCRIPT_PREFIX="my_script" echo "清理 $TEMP_DIR 中超过 ${MAX_AGE_HOURS} 小时的临时文件..." find "$TEMP_DIR " -name "${SCRIPT_PREFIX} *" -type f -mmin +$((MAX_AGE_HOURS * 60 )) -exec rm -v {} \; REMAINING=$(find "$TEMP_DIR " -name "${SCRIPT_PREFIX} *" -type f | wc -l) echo "剩余临时文件:$REMAINING "
六、僵尸进程处理 6.1 问题场景 症状 :ps aux 显示大量 <defunct> 进程
根本原因 :父进程未正确等待子进程退出
6.2 典型案例 1 2 3 4 5 6 7 8 9 10 11 12 #!/bin/bash for i in {1..100}; do ( echo "Task $i " exit 0 ) & done
6.3 正确做法 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 #!/bin/bash trap '' SIGCHLDfor i in {1..100}; do ( echo "Task $i " ) & done wait for i in {1..100}; do ( echo "Task $i " ) & if [ $((i % 10 )) -eq 0 ]; then wait -n fi done wait
6.4 僵尸进程检测 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #!/bin/bash echo "=== 系统僵尸进程统计 ===" ZOMBIE_TOTAL=$(ps aux | awk '$8 ~ /Z/ {count++} END {print count+0}' ) echo "僵尸进程总数:$ZOMBIE_TOTAL " echo "" echo "=== 按父进程分组 ===" ps -eo pid,ppid,stat ,cmd | awk '$3 ~ /Z/ {print $2}' | sort | uniq -c | sort -rn | head -10 SCRIPT_NAME="${1:-} " if [ -n "$SCRIPT_NAME " ]; then echo "" echo "=== $SCRIPT_NAME 的僵尸进程 ===" SCRIPT_PID=$(pgrep -f "$SCRIPT_NAME " | head -1) if [ -n "$SCRIPT_PID " ]; then ps -o pid,stat ,cmd --ppid $SCRIPT_PID | grep Z fi fi
七、管道阻塞问题 7.1 问题场景 症状 :脚本 hangs 不退出,即使看起来已完成
根本原因 :管道缓冲区满,生产者/消费者阻塞
7.2 典型案例 1 2 3 4 5 6 7 8 9 10 11 #!/bin/bash cat /var/log/syslog | while read line; do sleep 0.1 echo "$line " done | head -100
7.3 正确做法 1 2 3 4 5 6 7 8 9 10 11 #!/bin/bash cat /var/log/syslog | while read line; do echo "$line " done | head -100while read line; do echo "$line " done < <(cat /var/log/syslog | head -100)
7.4 使用 timeout 防止无限阻塞 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #!/bin/bash timeout 30s long_running_command{ timeout 60s producer_command } | { timeout 60s consumer_command } if ! timeout 30s command ; then echo "命令执行超时" >&2 fi
八、综合排查流程 8.1 快速诊断脚本 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 #!/bin/bash SCRIPT_PID="${1:-} " if [ -z "$SCRIPT_PID " ]; then echo "用法:$0 <脚本 PID>" exit 1 fi echo "============================================" echo "Bash 脚本资源诊断报告" echo "进程 PID: $SCRIPT_PID " echo "时间:$(date) " echo "============================================" echo "" echo "[1] 进程信息" ps -p $SCRIPT_PID -o pid,ppid,%cpu,%mem,vsz,rss,stat ,start,time ,cmd echo "" echo "[2] 文件描述符" FD_COUNT=$(ls /proc/$SCRIPT_PID /fd 2>/dev/null | wc -l) MAX_FD=$(cat /proc/$SCRIPT_PID /limits 2>/dev/null | grep "open files" | awk '{print $4}' ) echo "已打开:$FD_COUNT / 限制:$MAX_FD " if [ $FD_COUNT -gt 100 ]; then echo "⚠️ 警告:文件描述符数量异常" ls -la /proc/$SCRIPT_PID /fd/ | head -20 fi echo "" echo "[3] 子进程" CHILD_COUNT=$(pgrep -P $SCRIPT_PID 2>/dev/null | wc -l) echo "子进程数量:$CHILD_COUNT " if [ $CHILD_COUNT -gt 50 ]; then echo "⚠️ 警告:子进程数量异常" pstree -p $SCRIPT_PID | head -30 fi echo "" echo "[4] 僵尸进程检查" ZOMBIES=$(ps -o stat = --ppid $SCRIPT_PID 2>/dev/null | grep -c Z || echo 0) echo "僵尸进程数:$ZOMBIES " if [ "$ZOMBIES " -gt 0 ]; then echo "⚠️ 警告:发现僵尸进程" ps -o pid,stat ,cmd --ppid $SCRIPT_PID | grep Z fi echo "" echo "[5] 内存使用" cat /proc/$SCRIPT_PID /status 2>/dev/null | grep -E "^(VmSize|VmRSS|VmPeak):" echo "" echo "[6] 打开的文件(前 20 个)" lsof -p $SCRIPT_PID 2>/dev/null | head -20 echo "" echo "============================================" echo "诊断完成"
8.2 预防措施清单 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 ## Bash 脚本资源管理检查清单 ### 文件描述符 - [ ] 使用 `exec` 打开的 FD 是否有关闭操作- [ ] 是否使用 `trap` 注册清理函数- [ ] 避免在循环中重复打开同一文件### 后台进程 - [ ] 后台作业是否有 `wait` 对应- [ ] 是否限制最大并发数- [ ] 是否处理 SIGCHLD 信号### 变量管理 - [ ] 长运行脚本是否定期清理大变量- [ ] 数组是否有大小限制- [ ] 是否避免不必要的字符串拼接### 临时文件 - [ ] 是否使用 `mktemp` 创建- [ ] 是否有 `trap` 清理机制- [ ] 是否设置合理的文件权限### 进程管理 - [ ] 是否处理僵尸进程- [ ] 是否使用 `timeout` 防止阻塞- [ ] 是否有优雅退出机制
九、实战案例 9.1 案例:日志收集脚本内存泄漏 问题 :日志收集脚本运行 24 小时后内存占用从 10MB 增长到 2GB
排查 :
1 2 3 4 5 grep -n "BUFFER\|ARRAY" script.sh LOG_BUFFER="${LOG_BUFFER} ${new_log} "
修复 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 FLUSH_INTERVAL=1000 LOG_COUNT=0 add_log () { LOG_BUFFER="${LOG_BUFFER} $1 \n" LOG_COUNT=$((LOG_COUNT + 1 )) if [ $LOG_COUNT -ge $FLUSH_INTERVAL ]; then echo -e "$LOG_BUFFER " >> /var/log/collected.log LOG_BUFFER="" LOG_COUNT=0 fi }
9.2 案例:监控脚本文件描述符耗尽 问题 :监控脚本报错 too many open files
排查 :
1 2 3 4 5 6 7 lsof -p $(pgrep -f monitor.sh) | wc -l while true ; do exec 3<>/dev/tcp/host/port done
修复 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 exec 3<>/dev/tcp/host/port while true ; do echo "ping" >&3 read -t 5 response <&3 done check_host () { exec 3<>/dev/tcp/$1 /$2 echo "ping" >&3 read -t 5 response <&3 exec 3>&- }
十、总结 Bash 脚本资源管理的关键要点:
文件描述符 :打开必关闭,使用 trap 确保清理
后台进程 :控制并发,使用 wait 等待完成
变量内存 :长运行脚本定期清理大变量
临时文件 :使用 mktemp,注册清理回调
僵尸进程 :正确处理 SIGCHLD 或使用 wait -n
管道阻塞 :注意消费者提前退出的情况
超时保护 :使用 timeout 防止无限阻塞
通过遵循这些最佳实践,可以编写健壮、资源友好的 Bash 脚本。
参考资源