linux trap命令
简介: trap命令用于在shell脚本中捕获和处理信号,可以指定当接收到特定信号时要执行的命令或函数。
命令语法
# 基本语法
trap [command] [signal]
trap [action] [signal...]
trap -[signal]
trap -l
trap -p [signal]
常用信号列表
| 信号编号 |
信号名称 |
说明 |
1 |
SIGHUP |
挂起信号,终端断开连接时发送 |
2 |
SIGINT |
中断信号,通常由Ctrl+C产生 |
3 |
SIGQUIT |
退出信号,通常由Ctrl+\产生 |
9 |
SIGKILL |
强制终止信号,无法被捕获或忽略 |
15 |
SIGTERM |
终止信号,kill命令默认发送的信号 |
0 |
EXIT |
伪信号,脚本退出时触发 |
DEBUG |
伪信号 |
每个命令执行前触发 |
ERR |
伪信号 |
命令返回非零状态时触发 |
常用选项
| 选项 |
说明 |
-l 或 --list-signals |
列出所有可用的信号名称和编号 |
-p 或 --print |
显示已设置的信号处理程序 |
| 无选项(仅信号) |
重置指定信号的处理程序为默认 |
-- |
将后续参数解释为命令,即使以"-"开头 |
基本用法示例
1. 列出所有信号
# 显示所有信号及其编号
trap -l
# 输出示例(部分):
# 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL
# 5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE
# 9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2
# 13) SIGPIPE 14) SIGALRM 15) SIGTERM 16) SIGSTKFLT
# ...
# 使用kill命令查看
kill -l
2. 捕获Ctrl+C(SIGINT)信号
#!/bin/bash
# trap_example1.sh
echo "脚本开始运行,按Ctrl+C测试信号捕获"
# 捕获SIGINT信号(Ctrl+C)
trap 'echo "捕获到Ctrl+C,但不会退出";' SIGINT
# 或者使用信号编号
# trap 'echo "捕获到Ctrl+C";' 2
# 等待用户输入或Ctrl+C
read -p "请输入任意内容或按Ctrl+C: " input
echo "输入的内容: $input"
echo "脚本结束"
3. 捕获多个信号
#!/bin/bash
# trap_example2.sh
cleanup() {
echo "正在清理临时文件..."
rm -f /tmp/myscript_*.tmp
echo "清理完成"
}
# 捕获多个信号,都调用同一个清理函数
trap cleanup SIGINT SIGTERM SIGHUP
echo "创建临时文件..."
touch /tmp/myscript_1.tmp /tmp/myscript_2.tmp
echo "按Ctrl+C或等待10秒..."
sleep 10
echo "正常结束"
4. 捕获EXIT信号(脚本退出时)
#!/bin/bash
# trap_example3.sh
temp_file="/tmp/my_temp_$$.tmp"
# 无论脚本如何退出都会执行清理
cleanup() {
echo "执行退出清理..."
if [ -f "$temp_file" ]; then
rm -f "$temp_file"
echo "临时文件已删除: $temp_file"
fi
}
# 捕获EXIT信号(脚本退出时)
trap cleanup EXIT
echo "创建临时文件: $temp_file"
echo "测试内容" > "$temp_file"
# 模拟一些操作
read -p "请输入数字(1-正常退出,2-出错退出): " choice
if [ "$choice" = "2" ]; then
echo "模拟出错退出..."
exit 1
else
echo "正常退出..."
exit 0
fi
5. 捕获ERR信号(命令出错时)
#!/bin/bash
# trap_example4.sh
# 捕获ERR信号(任何命令返回非零状态时)
error_handler() {
echo "错误发生在第$1行: $2"
echo "时间: $(date)"
}
trap 'error_handler $LINENO "$BASH_COMMAND"' ERR
# 启用错误捕获(严格模式)
set -euo pipefail
echo "开始执行命令..."
# 以下命令会失败,触发ERR信号
ls /不存在的目录
echo "这行不会被执行,因为上面的命令出错了"
6. 显示已设置的信号处理程序
#!/bin/bash
# trap_example5.sh
# 设置一些信号处理
trap 'echo "SIGINT捕获"' SIGINT
trap 'echo "脚本退出"' EXIT
# 显示所有已设置的信号处理
echo "当前设置的信号处理程序:"
trap -p
# 显示特定信号的处理
echo "SIGINT的处理程序:"
trap -p SIGINT
7. 忽略信号
#!/bin/bash
# trap_example6.sh
echo "忽略SIGINT信号(Ctrl+C无效)"
trap '' SIGINT
echo "请按Ctrl+C测试(将不会起作用)"
sleep 5
echo "恢复SIGINT的默认处理"
trap - SIGINT
echo "现在可以按Ctrl+C退出了"
sleep 5
高级用法示例
1. 临时文件清理模式
#!/bin/bash
# safe_script.sh
set -euo pipefail
# 临时文件列表
declare -a temp_files=()
# 清理函数
cleanup() {
local exit_code=$?
echo "退出代码: $exit_code"
# 删除所有临时文件
for file in "${temp_files[@]}"; do
if [ -f "$file" ]; then
rm -f "$file"
echo "已删除: $file"
fi
done
# 如果有错误,记录日志
if [ $exit_code -ne 0 ]; then
echo "脚本异常退出于: $(date)" >> /var/log/myscript_error.log
fi
}
# 注册信号处理
trap cleanup EXIT SIGINT SIGTERM SIGHUP
# 创建临时文件
temp1=$(mktemp /tmp/script1.XXXXXX)
temp2=$(mktemp /tmp/script2.XXXXXX)
temp_files+=("$temp1" "$temp2")
echo "临时文件创建成功:"
echo " $temp1"
echo " $temp2"
# 业务逻辑
echo "执行主要任务..."
sleep 3
echo "任务完成"
2. 超时处理模式
#!/bin/bash
# timeout_script.sh
TIMEOUT=5 # 5秒超时
# 超时处理函数
timeout_handler() {
echo "操作超时!"
kill -TERM $child_pid 2>/dev/null
exit 124 # timeout命令的退出码
}
# 设置超时陷阱
trap timeout_handler SIGALRM
# 设置闹钟
( sleep $TIMEOUT; kill -ALRM $$ ) &
alarm_pid=$!
echo "开始长时间操作,超时时间: ${TIMEOUT}秒"
# 模拟长时间操作
sleep 10 &
child_pid=$!
# 等待子进程
wait $child_pid
child_exit=$?
# 取消闹钟
kill $alarm_pid 2>/dev/null
echo "操作完成,退出码: $child_exit"
3. 信号转发模式
#!/bin/bash
# signal_forward.sh
# 启动后台进程
some_daemon &
daemon_pid=$!
# 信号转发函数
forward_signal() {
echo "收到信号$1,转发给子进程$daemon_pid"
kill -$1 $daemon_pid
}
# 捕获信号并转发
trap 'forward_signal SIGTERM' SIGTERM
trap 'forward_signal SIGINT' SIGINT
trap 'forward_signal SIGHUP' SIGHUP
echo "主进程PID: $$"
echo "守护进程PID: $daemon_pid"
echo "等待信号..."
# 等待后台进程
wait $daemon_pid
实际应用场景
场景1:数据库备份脚本
#!/bin/bash
# backup_mysql.sh
set -euo pipefail
# 配置文件
BACKUP_DIR="/var/backups/mysql"
TEMP_DIR=$(mktemp -d)
DB_NAME="mydatabase"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="$BACKUP_DIR/backup_${DB_NAME}_${TIMESTAMP}.sql.gz"
# 清理函数
cleanup() {
local exit_code=$?
# 删除临时目录
if [ -d "$TEMP_DIR" ]; then
rm -rf "$TEMP_DIR"
echo "临时目录已清理: $TEMP_DIR"
fi
# 记录日志
if [ $exit_code -eq 0 ]; then
echo "$(date): 备份成功 - $BACKUP_FILE" >> /var/log/backup.log
else
echo "$(date): 备份失败 - 退出码 $exit_code" >> /var/log/backup.log
# 发送告警邮件
echo "数据库备份失败" | mail -s "备份失败告警" admin@example.com
fi
}
# 注册信号处理
trap cleanup EXIT SIGINT SIGTERM SIGHUP
echo "开始备份数据库: $DB_NAME"
# 创建备份目录
mkdir -p "$BACKUP_DIR"
# 执行备份
mysqldump -u root -p"$DB_PASSWORD" "$DB_NAME" | gzip > "$TEMP_DIR/backup.sql.gz"
# 移动备份文件
mv "$TEMP_DIR/backup.sql.gz" "$BACKUP_FILE"
echo "备份完成: $BACKUP_FILE"
echo "文件大小: $(du -h "$BACKUP_FILE" | cut -f1)"
场景2:服务管理脚本
#!/bin/bash
# service_wrapper.sh
SERVICE_NAME="my_service"
PID_FILE="/var/run/${SERVICE_NAME}.pid"
LOG_FILE="/var/log/${SERVICE_NAME}.log"
# 服务状态变量
service_pid=""
is_running=false
# 清理函数
cleanup() {
echo "正在停止服务..."
if [ -n "$service_pid" ] && kill -0 "$service_pid" 2>/dev/null; then
# 优雅停止
kill -TERM "$service_pid"
sleep 2
# 强制停止(如果需要)
if kill -0 "$service_pid" 2>/dev/null; then
kill -KILL "$service_pid"
fi
fi
# 清理PID文件
rm -f "$PID_FILE"
echo "服务已停止"
}
# 启动服务
start_service() {
echo "启动 $SERVICE_NAME..."
# 检查是否已在运行
if [ -f "$PID_FILE" ]; then
local existing_pid=$(cat "$PID_FILE")
if kill -0 "$existing_pid" 2>/dev/null; then
echo "服务已在运行 (PID: $existing_pid)"
return 0
fi
fi
# 启动服务(后台运行)
nohup /usr/local/bin/my_service >> "$LOG_FILE" 2>&1 &
service_pid=$!
is_running=true
# 保存PID
echo "$service_pid" > "$PID_FILE"
echo "服务启动成功 (PID: $service_pid)"
}
# 信号处理
trap cleanup SIGINT SIGTERM SIGHUP EXIT
# 启动服务
start_service
echo "服务正在运行,按Ctrl+C停止..."
# 等待服务进程
if [ "$is_running" = true ]; then
wait $service_pid
service_exit=$?
echo "服务退出,退出码: $service_exit"
fi
场景3:交互式脚本
#!/bin/bash
# interactive_menu.sh
# 临时文件
TEMP_MENU=$(mktemp)
# 清理函数
cleanup() {
rm -f "$TEMP_MENU"
echo -e "\n程序退出。"
tput cnorm # 恢复光标显示
}
# 显示菜单
show_menu() {
clear
cat << 'EOF'
===========================
主菜单
===========================
1. 显示系统信息
2. 显示磁盘使用
3. 显示内存信息
4. 退出
===========================
EOF
}
# 捕获信号
trap cleanup EXIT SIGINT
# 隐藏光标
tput civis
while true; do
show_menu
# 读取用户选择
read -p "请选择 (1-4): " choice
case $choice in
1)
echo "系统信息:"
uname -a
read -p "按回车键继续..."
;;
2)
echo "磁盘使用:"
df -h
read -p "按回车键继续..."
;;
3)
echo "内存信息:"
free -h
read -p "按回车键继续..."
;;
4)
echo "退出程序..."
break
;;
*)
echo "无效选择!"
sleep 1
;;
esac
done
注意事项
SIGKILL(信号9)和SIGSTOP(信号19)无法被捕获、阻塞或忽略
- 在函数内部设置的trap默认是全局的,除非使用
local命令声明
- 子shell会继承父shell的信号处理设置
- 使用
trap - SIGNAL可以恢复信号的默认处理方式
- 在清理函数中避免使用可能被中断的系统调用
- 使用
wait命令等待子进程时,信号可能会中断等待
最佳实践:
- 始终在脚本开头设置信号处理
- 为临时文件使用
mktemp命令创建唯一的文件名
- 在清理函数中检查文件/目录是否存在再删除
- 记录重要的清理操作日志
- 为长时间运行的操作设置超时处理
- 在信号处理函数中避免复杂的逻辑
调试技巧
1. 调试信号处理
#!/bin/bash
# debug_trap.sh
# 启用调试模式
set -x
# 简单的信号处理
trap 'echo "收到信号: $?"' EXIT SIGINT SIGTERM
echo "PID: $$"
echo "按Ctrl+C发送SIGINT"
# 显示当前的信号处理
trap -p
# 等待信号
sleep 10
2. 信号处理链
#!/bin/bash
# signal_chain.sh
handler1() {
echo "处理程序1"
}
handler2() {
echo "处理程序2"
}
# 设置多个处理程序(后者会覆盖前者)
trap handler1 SIGINT
echo "设置handler1"
trap -p SIGINT
trap handler2 SIGINT
echo "设置handler2"
trap -p SIGINT
echo "按Ctrl+C测试"
sleep 5
不同Shell的差异
| Shell |
trap命令差异 |
| Bash |
支持伪信号(EXIT, DEBUG, ERR, RETURN),功能最全面 |
| Zsh |
类似Bash,但有额外的伪信号(ZERR, DEBUG) |
| Dash |
只支持真实信号,不支持伪信号 |
| Ksh |
类似Bash,但某些选项语法不同 |
相关命令
kill - 向进程发送信号
pkill - 通过名称向进程发送信号
killall - 杀死指定名称的所有进程
nohup - 忽略SIGHUP信号运行命令
timeout - 运行命令并设置时间限制
strace - 跟踪系统调用和信号
常见问题解答
SIGKILL(信号9)和SIGSTOP(信号19)由操作系统内核直接处理,不允许用户空间程序捕获或忽略。这是为了确保系统管理员始终有一种方式可以终止失控的进程。
在EXIT信号的处理函数中,可以使用
$?获取脚本的退出状态码。但要注意,如果在处理函数中执行了其他命令,
$?的值会被覆盖,所以应该立即保存它:
cleanup() {
local exit_code=$?
echo "脚本退出码: $exit_code"
# ... 其他清理操作
}
trap cleanup EXIT
在Bash中,trap设置默认是全局的。如果在函数内部设置trap,它会影响整个shell。如果需要在函数中使用局部trap,可以使用子shell或通过保存和恢复原来的trap设置:
func() {
local old_trap=$(trap -p SIGINT)
trap 'echo "局部处理"' SIGINT
# ... 执行操作
# 恢复原来的trap
eval "$old_trap"
}