524 lines
12 KiB
Markdown
524 lines
12 KiB
Markdown
### 来看一个常见的场景
|
|
|
|
假设你正在开发一个数据备份脚本。这个脚本需要执行以下操作:
|
|
|
|
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` 都是一个强大的工具。 |