### 来看一个常见的场景 假设你正在开发一个数据备份脚本。这个脚本需要执行以下操作: 1. 创建临时工作目录 2. 将数据复制到临时目录 3. 压缩打包 4. 清理临时文件 ```text #!/bin/bash WORK_DIR="/tmp/backup_$(date +%Y%m%d)" echo "开始备份..." mkdir -p "$WORK_DIR" echo "创建临时目录: $WORK_DIR" echo "复制文件中..." cp -r /path/to/data "$WORK_DIR/" sleep 5 # 模拟耗时操作 echo "压缩打包..." tar -czf backup.tar.gz "$WORK_DIR" sleep 3 # 模拟耗时操作 echo "清理临时文件..." rm -rf "$WORK_DIR" echo "备份完成!" ``` ### 如果我中断了脚本怎么办! 当我们运行这个脚本时,如果在执行过程中按下 Ctrl+C 中断操作,会发生什么? 临时目录 `$WORK_DIR` 将被遗留在系统中,因为清理步骤没有被执行。长期积累下来,这些未清理的临时文件会占用大量磁盘空间。 ### 使用 trap 命令改善程序 这时,`trap` 命令就派上用场了。`trap` 可以捕获特定的信号并执行相应的处理函数。SIGINT(通常由 Ctrl+C 触发)就是最常见的信号之一。 首先,我们定义一个中断处理函数: ```text on_interrupt() { echo -e "\n程序被中断!" echo "清理临时文件..." rm -rf "$WORK_DIR" exit 1 } ``` 然后,在脚本开头使用 `trap` 设置信号处理: ```text trap on_interrupt SIGINT ``` 完整的改进版脚本如下: ```text #!/bin/bash WORK_DIR="/tmp/backup_$(date +%Y%m%d)" # 定义中断处理函数 on_interrupt() { echo -e "\n程序被中断!" echo "清理临时文件..." rm -rf "$WORK_DIR" exit 1 } # 设置 trap trap on_interrupt SIGINT echo "开始备份..." mkdir -p "$WORK_DIR" echo "创建临时目录: $WORK_DIR" echo "复制文件中..." cp -r /path/to/data "$WORK_DIR/" sleep 5 # 模拟耗时操作 echo "压缩打包..." tar -czf backup.tar.gz "$WORK_DIR" sleep 3 # 模拟耗时操作 echo "清理临时文件..." rm -rf "$WORK_DIR" echo "备份完成!" ``` ## trap 命令说明 `trap` 命令的基本语法是: ```text trap command signal ``` 其中: - `command` 可以是函数名或直接的命令 - `signal` 是要捕获的信号名称,如 SIGINT、SIGTERM 等 常见的信号包括: - SIGINT (2):用户按下 Ctrl+C - SIGTERM (15):终止信号 - EXIT:脚本退出时 你还可以同时捕获多个信号: ```text trap on_interrupt SIGINT SIGTERM ``` 通过使用 `trap` 命令和 `on_interrupt` 函数,我们实现了: 1. 优雅地处理程序中断 2. 确保临时资源被正确清理 3. 提供了友好的用户提示 这种模式不仅适用于备份脚本,还可以用在任何需要资源清理的脚本中,比如: - 临时文件处理 - 数据库连接清理 - 锁文件删除 - 进程清理 ## 扩展: trap 命令的高级应用 ### 多信号处理 有时我们需要对不同的信号进行不同的处理。比如在一个数据处理脚本中: ```text #!/bin/bash # 定义变量 DATA_FILE="data.txt" TEMP_FILE="temp.txt" LOG_FILE="process.log" # 处理 Ctrl+C on_interrupt() { echo -e "\n收到 SIGINT,正在优雅关闭..." cleanup exit 1 } # 处理 SIGTERM on_terminate() { echo -e "\n收到 SIGTERM,保存进度后退出..." save_progress cleanup exit 1 } # 处理正常退出 on_exit() { echo "程序正常结束,执行清理..." cleanup } # 清理函数 cleanup() { rm -f "$TEMP_FILE" echo "清理完成" } # 保存进度 save_progress() { echo "保存当前进度到 $LOG_FILE" echo "Progress saved at $(date)" >> "$LOG_FILE" } # 设置多重信号处理 trap on_interrupt SIGINT trap on_terminate SIGTERM trap on_exit EXIT # 主程序 echo "开始处理数据..." while true; do echo "处理中..." sleep 1 done ``` ### 临时禁用和恢复信号处理 有时我们需要临时禁用信号处理,比如在执行关键操作时: ```text #!/bin/bash critical_operation() { # 临时禁用 Ctrl+C trap '' SIGINT echo "执行关键操作,这段时间按 Ctrl+C 无效..." sleep 5 # 恢复信号处理 trap on_interrupt SIGINT echo "关键操作完成,恢复正常信号处理" } on_interrupt() { echo -e "\n操作被中断!" exit 1 } trap on_interrupt SIGINT echo "开始执行..." critical_operation echo "继续其他操作..." ``` ### DEBUG 信号与调试处理 DEBUG 并不是中断信号,而是 Bash 的一个特殊 trap 事件。它在执行每个命令之前触发,主要用于调试目的。让我们看一个更实用的例子: ```text #!/bin/bash # 用于控制是否在错误处理函数中触发 DEBUG trap IN_ERROR_HANDLER=0 # 定义调试处理函数 on_debug() { # 如果在错误处理函数中,跳过调试输出 if ((IN_ERROR_HANDLER)); then return fi # $1 是行号,$BASH_COMMAND 是即将执行的命令 echo "[DEBUG] 行 $1: 准备执行 -> $BASH_COMMAND" } # 错误处理函数 on_error() { local err=$? # 立即保存错误码 local line=$1 local cmd=$2 # 设置标志,防止在错误处理中触发 DEBUG trap IN_ERROR_HANDLER=1 echo "[ERROR] 行 $line 执行失败" echo "命令: $cmd" echo "错误码: $err" # 重置标志 IN_ERROR_HANDLER=0 } # 启用调试跟踪 enable_debug() { # 启用 ERR trap set -E # -T 选项可以显示函数调用跟踪 set -T # 设置 DEBUG trap,传入行号参数 trap 'on_debug ${LINENO}' DEBUG trap 'on_error ${LINENO} "$BASH_COMMAND"' ERR } # 关闭调试跟踪 disable_debug() { trap - DEBUG trap - ERR set +E set +T } # 通过环境变量控制是否开启调试 if [[ "${ENABLE_DEBUG}" == "true" ]]; then enable_debug fi # 测试函数 test_function() { echo "执行测试函数" local result=$((2 + 2)) echo "计算结果: $result" # 故意制造一个错误 ls /nonexistent_directory } # 主程序 echo "开始执行..." test_function echo "尝试访问不存在的文件..." cat nonexistent_file.txt ``` 使用方式: ```text # 普通执行 ./script.sh # 开启调试模式执行 ENABLE_DEBUG=true ./script.sh ``` 普通模式输出: ```text 开始执行... 执行测试函数 计算结果: 4 ls: cannot access '/nonexistent_directory': No such file or directory 尝试访问不存在的文件... cat: nonexistent_file.txt: No such file or directory ``` DEBUG 模式输出: ```text [DEBUG] 行 41: 准备执行 -> trap 'on_error ${LINENO} "$BASH_COMMAND"' ERR [DEBUG] 行 67: 准备执行 -> echo "开始执行..." 开始执行... [DEBUG] 行 68: 准备执行 -> test_function [DEBUG] 行 58: 准备执行 -> test_function [DEBUG] 行 59: 准备执行 -> echo "执行测试函数" 执行测试函数 [DEBUG] 行 60: 准备执行 -> local result=$((2 + 2)) [DEBUG] 行 61: 准备执行 -> echo "计算结果: $result" 计算结果: 4 [DEBUG] 行 63: 准备执行 -> ls /nonexistent_directory ls: cannot access '/nonexistent_directory': No such file or directory [DEBUG] 行 63: 准备执行 -> ls /nonexistent_directory [DEBUG] 行 17: 准备执行 -> ls /nonexistent_directory [DEBUG] 行 18: 准备执行 -> local err=$? [DEBUG] 行 19: 准备执行 -> local line=$1 [DEBUG] 行 20: 准备执行 -> local cmd=$2 [DEBUG] 行 23: 准备执行 -> IN_ERROR_HANDLER=1 [ERROR] 行 63 执行失败 命令: ls /nonexistent_directory 错误码: 2 [DEBUG] 行 68: 准备执行 -> ls /nonexistent_directory [DEBUG] 行 17: 准备执行 -> ls /nonexistent_directory [DEBUG] 行 18: 准备执行 -> local err=$? [DEBUG] 行 19: 准备执行 -> local line=$1 [DEBUG] 行 20: 准备执行 -> local cmd=$2 [DEBUG] 行 23: 准备执行 -> IN_ERROR_HANDLER=1 [ERROR] 行 68 执行失败 命令: ls /nonexistent_directory 错误码: 2 [DEBUG] 行 69: 准备执行 -> echo "尝试访问不存在的文件..." 尝试访问不存在的文件... [DEBUG] 行 70: 准备执行 -> cat nonexistent_file.txt cat: nonexistent_file.txt: No such file or directory [DEBUG] 行 70: 准备执行 -> cat nonexistent_file.txt [DEBUG] 行 17: 准备执行 -> cat nonexistent_file.txt [DEBUG] 行 18: 准备执行 -> local err=$? [DEBUG] 行 19: 准备执行 -> local line=$1 [DEBUG] 行 20: 准备执行 -> local cmd=$2 [DEBUG] 行 23: 准备执行 -> IN_ERROR_HANDLER=1 [ERROR] 行 70 执行失败 命令: cat nonexistent_file.txt 错误码: 1 ``` ### 文件锁机制 trap vs flock 让我们比较 trap 和 flock 的锁机制: **使用 trap 的文件锁** ```text #!/bin/bash LOCK_FILE="/tmp/script.lock" PID_FILE="/tmp/script.pid" cleanup() { rm -f "$LOCK_FILE" "$PID_FILE" echo "清理锁文件和PID文件" } get_lock() { if [ -e "$LOCK_FILE" ]; then local pid pid=$(cat "$PID_FILE" 2>/dev/null) if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then echo "另一个实例(PID: $pid)正在运行" exit 1 fi # 如果进程不存在,清理旧的锁 cleanup fi echo $$ > "$PID_FILE" touch "$LOCK_FILE" trap cleanup EXIT } ``` **使用 flock 的实现:** ```text #!/bin/bash LOCK_FILE="/tmp/script.lock" ( # 获取文件锁,等待最多5秒 flock -w 5 200 || { echo "无法获取锁,另一个实例正在运行"; exit 1; } echo "获得锁,开始执行..." sleep 10 echo "执行完成" ) 200>"$LOCK_FILE" ``` **比较分析** 1. **可靠性** - flock 更可靠,它使用内核级文件锁 - trap 方式可能在极端情况下(如系统崩溃)留下孤立的锁文件 1. **使用场景** - flock 适合要求严格的生产环境 - trap 方式适合简单的脚本和开发环境 1. **推荐选择** - 推荐使用 flock,因为它: - 自动处理进程终止 - 支持超时设置 - 提供阻塞和非阻塞模式 - 可靠性更高 ### 事务的实现 ```text #!/bin/bash # 状态变量 TRANSACTION_ACTIVE=false # 动态改变信号处理 update_signal_handler() { if $TRANSACTION_ACTIVE; then # 事务进行中,设置中断处理为提示并结束 trap 'echo "事务进行中,已被强行中断..."; cleanup; exit 1' SIGINT else # 非事务状态,可以安全退出 trap 'echo "正常退出..."; exit 0' SIGINT fi } # 清理函数 cleanup() { echo "执行清理操作..." # 这里添加实际的清理代码 } # 模拟事务 start_transaction() { TRANSACTION_ACTIVE=true update_signal_handler echo "事务开始" # 模拟事务操作 echo "执行事务步骤 1/3" sleep 2 echo "执行事务步骤 2/3" sleep 2 echo "执行事务步骤 3/3" sleep 2 TRANSACTION_ACTIVE=false update_signal_handler echo "事务完成" } # 设置初始信号处理 update_signal_handler # 主程序执行流程 echo "开始执行..." start_transaction echo "继续其他操作..." ``` **执行流程说明:** 1. 脚本启动 - `TRANSACTION_ACTIVE` 初始值为 `false` - 首次调用 `update_signal_handler`,设置正常的中断处理 1. 执行 `start_transaction` - 设置 `TRANSACTION_ACTIVE` 为 `true` - 更新信号处理为事务保护模式 - 执行事务操作 - 完成后,设置 `TRANSACTION_ACTIVE` 为 `false` - 恢复正常的信号处理 1. 信号处理行为 - 事务进行中收到 SIGINT:显示中断消息,执行清理,然后退出 - 非事务状态收到 SIGINT:直接安全退出 通过这些高级用法,我们可以构建更健壮、更可靠的 shell 脚本。无论是处理意外中断、实现锁机制,还是进行调试,`trap` 都是一个强大的工具。