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命令等待子进程时,信号可能会中断等待
最佳实践:
  1. 始终在脚本开头设置信号处理
  2. 为临时文件使用mktemp命令创建唯一的文件名
  3. 在清理函数中检查文件/目录是否存在再删除
  4. 记录重要的清理操作日志
  5. 为长时间运行的操作设置超时处理
  6. 在信号处理函数中避免复杂的逻辑

调试技巧

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"
}