Linux shell 使用 trap 命令优雅处理程序中断: shell 中的回调、锁与事务、以及 debug 调试

来看一个常见的场景

假设你正在开发一个数据备份脚本。这个脚本需要执行以下操作:

  1. 创建临时工作目录
  2. 将数据复制到临时目录
  3. 压缩打包
  4. 清理临时文件
#!/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 触发)就是最常见的信号之一。

首先,我们定义一个中断处理函数:

on_interrupt() {
    echo -e "\n程序被中断!"
    echo "清理临时文件..."
    rm -rf "$WORK_DIR"
    exit 1
}

然后,在脚本开头使用 trap 设置信号处理:

trap on_interrupt SIGINT

完整的改进版脚本如下:

#!/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 命令的基本语法是:

trap command signal

其中:

  • command 可以是函数名或直接的命令
  • signal 是要捕获的信号名称,如 SIGINT、SIGTERM 等

常见的信号包括:

  • SIGINT (2):用户按下 Ctrl+C
  • SIGTERM (15):终止信号
  • EXIT:脚本退出时

你还可以同时捕获多个信号:

trap on_interrupt SIGINT SIGTERM

通过使用 trap 命令和 on_interrupt 函数,我们实现了:

  1. 优雅地处理程序中断
  2. 确保临时资源被正确清理
  3. 提供了友好的用户提示

这种模式不仅适用于备份脚本,还可以用在任何需要资源清理的脚本中,比如:

  • 临时文件处理
  • 数据库连接清理
  • 锁文件删除
  • 进程清理

扩展: trap 命令的高级应用

多信号处理

有时我们需要对不同的信号进行不同的处理。比如在一个数据处理脚本中:

#!/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

临时禁用和恢复信号处理

有时我们需要临时禁用信号处理,比如在执行关键操作时:

#!/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 事件。它在执行每个命令之前触发,主要用于调试目的。让我们看一个更实用的例子:

#!/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

使用方式:

# 普通执行
./script.sh

# 开启调试模式执行
ENABLE_DEBUG=true ./script.sh

普通模式输出:

开始执行...
执行测试函数
计算结果: 4
ls: cannot access '/nonexistent_directory': No such file or directory
尝试访问不存在的文件...
cat: nonexistent_file.txt: No such file or directory

DEBUG 模式输出:

[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 的文件锁

#!/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 的实现:

#!/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 方式可能在极端情况下(如系统崩溃)留下孤立的锁文件
  2. 使用场景

    • flock 适合要求严格的生产环境
    • trap 方式适合简单的脚本和开发环境
  3. 推荐选择

    • 推荐使用 flock,因为它:
      • 自动处理进程终止
      • 支持超时设置
      • 提供阻塞和非阻塞模式
      • 可靠性更高

事务的实现

#!/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,设置正常的中断处理
  2. 执行 start_transaction

    • 设置 TRANSACTION_ACTIVEtrue
    • 更新信号处理为事务保护模式
    • 执行事务操作
    • 完成后,设置 TRANSACTION_ACTIVEfalse
    • 恢复正常的信号处理
  3. 信号处理行为

    • 事务进行中收到 SIGINT:显示中断消息,执行清理,然后退出
    • 非事务状态收到 SIGINT:直接安全退出

通过这些高级用法,我们可以构建更健壮、更可靠的 shell 脚本。无论是处理意外中断、实现锁机制,还是进行调试,trap 都是一个强大的工具。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/937385.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

jenkins构建 Webhook 触发器

目的是要让gitlab上面的项目更新了版本,Jenkins上面自动去执行新版本 项目地址 不用插件实现标签触发任务 Jenkins上面创建任务 这里面需要更改下 #网址http://jenkins.wang.org:8080/job/webhook-demo/configure生成个随机密码,测试用123456也可以 […

iPhone苹果相册视频怎么提取音频?

在数字时代,视频已成为我们记录生活、分享故事的重要方式。然而,有时候我们只想保留视频中的音频部分,比如一段动人的背景音乐或是一段珍贵的对话。那么,苹果相册视频怎么提取音频呢?本文将介绍三种简单且实用的方法&a…

前端成长之路:CSS字体、文本属性和引入方式

本文主要介绍CSS的字体属性和文本属性,最后再介绍CSS在HTML中的引入方式。 CSS字体属性 CSS Fonts(字体)属性能用于定义字体系列属性,包括但不限于字体大小、粗细、字体样式等。 字体系列 在CSS中使用font-family属性定义文本…

PostgreSQL 常用运维SQL整理

一、查询并杀会话 -- 查询会话 select pid,usename,client_addr,client_port,query_start,query,wait_event from pg_stat_activity; -- 杀会话 select pg_terminate_backend(pid号); -- 使用如下命令自动生成杀会话语句 select datid,datname,pid,usesysid,usename,applicat…

item2 for macos

安装Item2 brew install iterm2 查看终端类型 cat /etc/shells Mac OS X 10.15 已经将默认的shell从Bash换成了zsh,所以不用安装,10.15以前的可以使用下面的命令进行安装 brew install zsh 安装Oh My ZSH # curl sh -c "$(curl -fsSL https://ra…

Python的3D可视化库【vedo】2-1 (plotter模块) 绘制器的使用

文章目录 1 相关用语及其关系2 Plotter类的基本使用3 Plotter类具体的初始化设置3.1 全部初始化参数3.2 使用不同的axes vedo是Python实现的一个用于辅助科学研究的3D可视化库。 vedo的plotter模块封装了绘制器类Plotter。 Plotter实例可以用于显示3D图形对象、控制渲染器行为、…

特征交叉-CAN学习笔记代码解读

一 核心模块coaction 对于每个特征对(feature_pairs)weight, bias 来自于P_inductionP_fead是MLP的input 举个例子:如果是用户ID和产品ID的co-action,且产品ID是做induction,用户ID是做feed。 step1 用户ID/产品ID都先形成一个向量&#xf…

【Java面试】深拷贝、浅拷贝和引用拷贝三者的区别

浅拷贝:浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点),不过,如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用同一个内部对象。…

EasyGBS点对点穿透P2P远程访问技术在安防视频监控中的应用

随着信息技术的快速发展,安防视频监控系统在公共安全领域的应用变得越来越广泛。传统的视频监控系统多依赖于中心服务器进行视频流的集中处理和分发,这不仅增加了网络带宽的负担,还可能成为系统性能瓶颈。为了解决这些问题,P2P&am…

Vue入门到精通:核心语法—模板语法

Vue入门到精通:核心语法—模板语法 Vue.js因其简单、易用和高效的特点,自推出以来一直受到广泛关注。Vue.js的核心概念和技术包括模板语法、计算属性、事件监听、动态样式绑定、条件渲染指令(如v-if)、列表渲染指令(如…

C++中如何实现接口继承与实现继承,以及它们的区别?

概念 在 C 中,接口继承和实现继承是两种不同的继承方式,它们在设计模式、代码复用和多态性方面有着不同的应用。下面将分别解释这两者的概念、实现方式及其区别。 接口继承 接口继承指的是只继承类的接口(即公共的成员函数声明&#xff09…

WPF+MVVM案例实战与特效(三十八)- 封装一个自定义的数字滚动显示控件

文章目录 1、运行效果2、案例实现1、功能设计2、页面布局3、控件使用4、运行效果3、拓展:多数字自定义控件1、控件应用4、总结1、运行效果 在Windows Presentation Foundation (WPF)应用程序中,自定义控件允许开发者创建具有特定功能和外观的独特UI元素。本博客将介绍一个名…

2024年12月HarmonyOS应用开发者高级认证全新题库

注意事项:切记在考试之外的设备上打开题库进行搜索,防止切屏三次考试自动结束,题目是乱序,每次考试,选项的顺序都不同,作者已于2024年12月15日又更新了一波题库,题库正确率99%! 新版…

【Java学习笔记】JUnit

一、为什么需要 JUnit 二、基本介绍 三、实现方法 第一次添加: 在需要测试的方法处输入 Test注解,快捷键AltInsert选择添加版本(常用JUnit5.4) 出现绿色箭头可进行测试和编译

MySQL误删除 binlog 还原 恢复已删除数据 实战 超详细

硬盘有价,数据无价,数据库执行,谨慎操作! binlog日志还原不适用于直接删表删库的误操作! 目录 实战恢复 1、导出相关时间binlog数据 2、找到对应语句以及pos区间 3、导出改动区间的sql 4、将binlog导出的sql转换…

百度地图JavaScript API核心功能指引

百度地图JavaScript API是一套由JavaScript语言编写的应用程序接口,它能够帮助您在网站中构建功能丰富、交互性强的地图应用,包含了构建地图基本功能的各种接口,提供了诸如本地搜索、路线规划等数据服务。百度地图JavaScript API支持HTTP和HT…

《拉依达的嵌入式\驱动面试宝典》—C/CPP基础篇(五)

《拉依达的嵌入式\驱动面试宝典》—C/CPP基础篇(五) 你好,我是拉依达。 感谢所有阅读关注我的同学支持,目前博客累计阅读 27w,关注1.5w人。其中博客《最全Linux驱动开发全流程详细解析(持续更新)-CSDN博客》已经是 Linux驱动 相关内容搜索的推荐首位,感谢大家支持。 《拉…

C语言简单日志宏

最近调试C代码,发现要写很多打印的内容不是很方便,于是简单写一下C语言的日志来方便自己调试: 1. 简单打印带标识的日志信息 #include "stdio.h" #define PRINT(...) \do \{ …

【算法】—— 前缀和

一、区间求和问题 给定一个长度为n的序列a,有m次查询,每次查询输出一个连续区间的和。 使用暴力做法求解是将每次查询都遍历该区间求和 //暴力做法import java.util.Scanner;public class Test {public static void main(String[] args){Scanner scan…

详解下c语言下的多维数组和指针数组

在实际c语言编程中,三维及以上数组我们使用的很少,二维数组我们使用得较多。说到数组,又不得关联到指针,因为他们两者的联系太紧密了。今天我们就详细介绍下c语言下的多维数组(主要是介绍二维数组)和指针。 一、二维数组 1.1&am…