Shell 脚本编程
Shell 脚本把多条命令组合成可重复执行的自动化流程,是 AI 工程师管理训练任务、部署服务、处理数据的利器。本页涵盖 Bash 语法、重定向、退出码、实战脚本,以及数组、信号处理、参数解析和定时任务。
Linux 常用命令的速查见 Linux 基础与运维,本页侧重于把命令”编排”成脚本。
脚本执行流程
Section titled “脚本执行流程”一个 Shell 脚本从解释器声明到最终退出,典型流程如下:
#!/usr/bin/env bash# 严格模式:任何变量未定义、命令失败、管道失败都立即报错退出set -euo pipefail
# ===== 变量 =====MODEL_NAME="resnet50"EPOCHS=100DATASET_DIR="/data/imagenet"
# 变量引用必须加双引号,防止路径含空格时出错echo "训练模型: ${MODEL_NAME}, 轮数: ${EPOCHS}"
# ===== 条件判断 =====if [ -d "${DATASET_DIR}" ]; then echo "数据目录存在"elif [ -f "data.tar.gz" ]; then echo "存在压缩包,解压中..." tar -xzf data.tar.gzelse echo "错误:找不到数据" >&2 exit 1fi
# ===== 循环 =====# 遍历多个 GPU 分别启动任务for gpu in 0 1 2 3; do echo "在 GPU ${gpu} 上启动任务..." CUDA_VISIBLE_DEVICES=${gpu} python train.py --gpu ${gpu} &donewait # 等待所有后台任务完成
# ===== 函数 =====check_gpu_memory() { local required_gb=$1 # 提取第一块 GPU 的剩余显存(MB) local free_mb free_mb=$(nvidia-smi --query-gpu=memory.free --format=csv,noheader,nounits | head -1) if [ "${free_mb}" -lt $((required_gb * 1024)) ]; then echo "显存不足:需要 ${required_gb}GB,剩余 $((free_mb / 1024))GB" >&2 return 1 fi return 0}
# 调用函数check_gpu_memory 12 || exit 1set -euo pipefail 详解
Section titled “set -euo pipefail 详解”| 选项 | 作用 |
|---|---|
-e (errexit) | 任何命令返回非零退出码,脚本立即停止 |
-u (nounset) | 引用未定义变量时报错(而非展开为空字符串) |
-o pipefail | 管道中任一命令失败,整个管道算失败(否则只看最后一条) |
💡 生产级脚本的第一行永远是
set -euo pipefail。一个未捕获的错误可能导致数据损坏或训练跑飞。
text="hello_world_v2"
# 字符串长度echo ${#text} # 14
# 子串截取(从位置 0 开始取 5 个字符)echo ${text:0:5} # hello
# 删除前缀echo ${text#hello_} # world_v2
# 删除后缀(贪婪匹配用 %%)echo ${text%_*} # hello_worldecho ${text%%_*} # hello
# 替换(首个/全部)echo ${text/world/WORLD} # hello_WORLD_v2echo ${text//_/ -} # hello -world -v2
# 默认值(变量为空或未定义时用默认值)echo ${OUTPUT_DIR:-/tmp/output}重定向与 heredoc
Section titled “重定向与 heredoc”# 标准输出重定向(覆盖)python train.py > train_output.log
# 追加重定向(不覆盖已有内容)python train.py >> combined.log 2>&1 # 2>&1 将标准错误也合并到标准输出
# Heredoc:多行文本写入文件cat > config.yaml << 'EOF'model: name: resnet50 pretrained: truetraining: epochs: 100 batch_size: 256 learning_rate: 0.01EOF
# Heredoc 不加引号时可以做变量插值cat > run.sh << EOF#!/bin/bash# 自动生成:模型=${MODEL_NAME}CUDA_VISIBLE_DEVICES=0 python train.py --model ${MODEL_NAME}EOF# /dev/null 黑洞:丢弃输出python train.py > /dev/null 2>&1 # 静默运行
# 同时保存到文件和终端(tee)python train.py 2>&1 | tee train.log # 覆盖python train.py 2>&1 | tee -a train.log # 追加
# 进程替换:把命令输出当文件传给另一命令diff <(ls dir1) <(ls dir2) # 比较两个目录的文件列表
# here-string:把字符串作为标准输入grep "error" <<< "$LOG_TEXT"每条命令执行后返回一个 0-255 的退出码(exit code),0 表示成功,非 0 表示失败。在脚本和 CI 流水线中,退出码是判断成功与否的唯一依据。
python preprocess.pyif [ $? -ne 0 ]; then echo "预处理失败,终止流水线" >&2 exit 1fi💡 使用
set -e后,任何命令返回非零退出码都会让脚本自动退出,无需手动检查$?。set -u拦截未定义变量,set -o pipefail让管道中任一环节失败也算整体失败。三者组合set -euo pipefail是生产级脚本的标配。
常用退出码约定
Section titled “常用退出码约定”| 退出码 | 含义 |
|---|---|
0 | 成功 |
1 | 通用错误 |
2 | 命令行参数错误(shell 内建命令约定) |
126 | 命令不可执行(无权限) |
127 | 命令未找到 |
128+N | 被信号 N 杀死(如 130 = SIGINT/Ctrl+C) |
数组与关联数组
Section titled “数组与关联数组”# 声明数组gpus=(0 1 2 3)datasets=("imagenet" "coco" "voc")
# 访问元素echo ${gpus[0]} # 0echo ${gpus[@]} # 所有元素:0 1 2 3echo ${#gpus[@]} # 元素个数:4
# 追加元素gpus+=(4 5)
# 遍历for gpu in "${gpus[@]}"; do echo "使用 GPU ${gpu}"done
# 切片(取第 1-2 个元素)echo ${datasets[@]:0:2} # imagenet coco关联数组(associative array)
Section titled “关联数组(associative array)”关联数组相当于字典/哈希表,需要 Bash 4.0+:
declare -A model_paths
# 赋值model_paths["resnet50"]="/models/resnet50_v2.pt"model_paths["vit"]="/models/vit_base.pt"model_paths["yolo"]="/models/yolov8n.pt"
# 访问echo ${model_paths["resnet50"]}
# 遍历所有 keyfor model in "${!model_paths[@]}"; do echo "${model} -> ${model_paths[$model]}"done
# 元素个数echo ${#model_paths[@]}信号处理(trap)
Section titled “信号处理(trap)”trap 命令在脚本收到信号时执行指定的清理操作。训练脚本用它来确保收到 Ctrl+C / kill 时能保存 checkpoint。
#!/usr/bin/env bashset -euo pipefail
CLEANUP_DONE=false
cleanup() { if [ "$CLEANUP_DONE" = true ]; then return; fi CLEANUP_DONE=true echo "" echo "收到终止信号,正在清理..." # 保存 checkpoint、删除临时文件、通知监控系统... rm -f /tmp/train_lock_$$ # $$ 是当前进程 PID echo "清理完成,退出" exit 0}
# 捕获 SIGINT(Ctrl+C)、SIGTERM(kill 默认信号)、EXIT(脚本正常退出)trap cleanup SIGINT SIGTERM EXIT
echo "训练开始..."while true; do echo "训练中..." sleep 2done💡
trap '...' EXIT在脚本以任何方式退出(正常结束、被 kill、set -e 触发)时都会执行,是最可靠的清理机制。
参数解析(getopts)
Section titled “参数解析(getopts)”#!/usr/bin/env bashset -euo pipefail
# 默认参数MODEL="resnet50"EPOCHS=100LR=0.001GPU=0
usage() { cat << EOF用法: $0 [-m MODEL] [-e EPOCHS] [-l LR] [-g GPU] [-h] -m MODEL 模型名称(默认: resnet50) -e EPOCHS 训练轮数(默认: 100) -l LR 学习率(默认: 0.001) -g GPU GPU 编号(默认: 0) -h 显示帮助EOF exit 0}
# getopts 只支持短选项(-m),不支持长选项(--model)while getopts ":m:e:l:g:h" opt; do case ${opt} in m ) MODEL=$OPTARG ;; e ) EPOCHS=$OPTARG ;; l ) LR=$OPTARG ;; g ) GPU=$OPTARG ;; h ) usage ;; \? ) echo "无效选项: -$OPTARG" >&2; usage; exit 1 ;; : ) echo "选项 -$OPTARG 需要参数" >&2; exit 1 ;; esacdone
echo "模型=${MODEL}, 轮数=${EPOCHS}, 学习率=${LR}, GPU=${GPU}"# 使用: ./train.sh -m vit -e 50 -g 1长选项解析(手动模式)
Section titled “长选项解析(手动模式)”如果需要 --model vit 这种长选项,可以用手动解析:
while [[ $# -gt 0 ]]; do case $1 in --model) MODEL="$2"; shift 2 ;; --epochs) EPOCHS="$2"; shift 2 ;; --lr) LR="$2"; shift 2 ;; --gpu) GPU="$2"; shift 2 ;; -h|--help) usage ;; *) echo "未知参数: $1" >&2; exit 1 ;; esacdonecron 定时任务
Section titled “cron 定时任务”cron 是 Linux 内置的定时任务调度器,适合定期清理磁盘、检查 GPU 状态、备份数据等。
# 编辑当前用户的 crontabcrontab -e
# 查看当前用户的定时任务crontab -lcrontab 格式
Section titled “crontab 格式”# 分钟 小时 日 月 星期 命令# ───────────────────────────── */5 * * * * 命令 # 每 5 分钟 0 2 * * * 命令 # 每天凌晨 2:00 0 */6 * * * 命令 # 每 6 小时 30 1 * * 1 命令 # 每周一凌晨 1:30 0 0 1 * * 命令 # 每月 1 号 0:00实用定时任务示例
Section titled “实用定时任务示例”# === crontab 内容 ===
# 每天凌晨 3 点清理 7 天前的日志0 3 * * * find /data/logs -name "*.log" -mtime +7 -delete
# 每 10 分钟检查 GPU 状态并记录*/10 * * * * nvidia-smi --query-gpu=index,memory.used,utilization.gpu --format=csv,noheader >> /var/log/gpu_cron.log 2>&1
# 每周日凌晨 2 点备份模型目录0 2 * * 0 tar -czf /backup/models_$(date +\%Y\%m\%d).tar.gz /models >> /var/log/backup.log 2>&1⚠️ cron 环境变量极简(没有
PATH、没有conda activate),命令中要用绝对路径,或在脚本开头手动设置环境。%在 crontab 中需要转义为\%。
实战:GPU 监控脚本
Section titled “实战:GPU 监控脚本”#!/usr/bin/env bash# gpu_monitor.sh —— 持续监控 GPU 并记录日志,显存超阈值时告警set -euo pipefail
LOG_FILE="/var/log/gpu_monitor.log"THRESHOLD_GB=80 # 显存告警阈值(GB)INTERVAL=60 # 监控间隔(秒)
echo "GPU 监控已启动,间隔 ${INTERVAL}s,阈值 ${THRESHOLD_GB}GB" | tee -a "${LOG_FILE}"
while true; do timestamp=$(date '+%Y-%m-%d %H:%M:%S')
# 逐行读取每块 GPU 的信息 nvidia-smi --query-gpu=index,name,memory.used,memory.total,utilization.gpu \ --format=csv,noheader,nounits | while IFS=',' read -r idx name used total util; do # 去除多余空格 idx=$(echo "$idx" | xargs) used=$(echo "$used" | xargs) total=$(echo "$total" | xargs)
# 转换为 GB used_gb=$((used / 1024)) total_gb=$((total / 1024))
echo "[${timestamp}] GPU ${idx} (${name}): ${used_gb}GB / ${total_gb}GB, 利用率 ${util}%" \ | tee -a "${LOG_FILE}"
# 显存超阈值告警 if [ "${used_gb}" -gt "${THRESHOLD_GB}" ]; then echo "[${timestamp}] ⚠️ 告警:GPU ${idx} 显存使用 ${used_gb}GB 超过阈值 ${THRESHOLD_GB}GB" \ | tee -a "${LOG_FILE}" fi done
sleep "${INTERVAL}"done保存为 gpu_monitor.sh,用 chmod +x gpu_monitor.sh 赋予执行权限后即可后台运行:nohup ./gpu_monitor.sh &。
实战:批量训练调度脚本
Section titled “实战:批量训练调度脚本”#!/usr/bin/env bash# batch_train.sh —— 在多块 GPU 上并行调度多个模型训练set -euo pipefail
# 模型列表declare -a MODELS=("resnet50" "vit_base" "convnext" "efficientnet")NUM_GPUS=4
echo "开始批量训练:${#MODELS[@]} 个模型,${NUM_GPUS} 块 GPU"
# 每块 GPU 分配一个模型,GPU 满了就等待running=0for model in "${MODELS[@]}"; do gpu=$((running % NUM_GPUS))
echo "[$(date +%H:%M:%S)] 启动 ${model} 在 GPU ${gpu}" CUDA_VISIBLE_DEVICES=${gpu} python train.py --model "${model}" \ --output "/data/results/${model}" > "logs/${model}.log" 2>&1 & ((running++))
# GPU 全部占用时,等待至少一个任务完成 if (( running % NUM_GPUS == 0 )); then wait -n # 等待任意一个后台任务完成(Bash 4.3+) fidone
wait # 等待剩余任务echo "[$(date +%H:%M:%S)] 所有训练完成"| 术语 | 英文 | 解释 |
|---|---|---|
| 严格模式 | strict mode | set -euo pipefail,出错即停、未定义变量报错、管道失败传播 |
| 退出码 | exit code | 命令执行结果的 0-255 数值,0=成功 |
| 关联数组 | associative array | Bash 4.0+ 的键值对数组,类似 Python dict |
| 信号 | signal | 操作系统发给进程的异步通知(SIGINT、SIGTERM 等) |
| 陷阱 | trap | 捕获信号并在收到时执行清理命令 |
| 定时任务 | cron job | crontab 定义的周期性自动执行任务 |
- 站内关联
- Linux 基础与运维 —— Shell 脚本中使用的各类命令速查
- Git 版本控制 —— 配合 Git hooks 做自动化
- Docker 容器化 —— 把脚本和依赖打包到容器
- 推荐资源
- Bash 参考手册(GNU)
- ShellCheck —— Shell 脚本静态检查工具(强烈推荐)
- Pure Bash Bible —— 纯 Bash 实现常见操作的合集