【问题标题】:bash trap and process substitutionbash 陷阱和进程替换
【发布时间】:2018-04-11 06:44:10
【问题描述】:

更新

我为我发布的答案使用了一个更好的测试用例。我在这里添加更新的测试用例,以防有人想进一步试验:

#!/bin/bash

mypts="$( tty )"

# main traps
trap "echo 'trapped SIGCHLD' >$mypts" SIGCHLD 
trap "echo 'trapped SIGHUP' >$mypts" SIGHUP 
trap "echo 'trapped SIGINT' >$mypts" SIGINT
trap "echo 'trapped SIGPIPE' >$mypts" SIGPIPE
trap "echo 'trapped SIGSEGV' >$mypts" SIGSEGV
trap "echo 'trapped SIGSYS' >$mypts" SIGSYS
trap "echo 'trapped SIGTERM' >$mypts" SIGTERM

function h4 {
    # function traps
    # these mask the main traps
    #trap "echo 'trapped h4 SIGCHLD'" SIGCHLD 
    #trap "echo 'trapped h4 SIGHUP'" SIGHUP 
    #trap "echo 'trapped h4 SIGINT'" SIGINT 
    #trap "echo 'trapped h4 SIGPIPE'" SIGPIPE 
    #trap "echo 'trapped h4 SIGSEGV'" SIGSEGV 
    #trap "echo 'trapped h4 SIGSYS'" SIGSYS 
    #trap "echo 'trapped h4 SIGTERM'" SIGTERM 

    {
        # compound statement traps
        # these mask the function traps
        #trap "echo 'trapped compound SIGCHLD'" SIGCHLD 
        #trap "echo 'trapped compound SIGHUP'" SIGHUP 
        #trap "echo 'trapped compound SIGINT'" SIGINT
        #trap "echo 'trapped compound SIGPIPE'" SIGPIPE 
        #trap "echo 'trapped compound SIGSEGV'" SIGSEGV 
        #trap "echo 'trapped compound SIGSYS'" SIGSYS 
        #trap "echo 'trapped compound SIGTERM'" SIGTERM 

        echo begin err 1>&2
        echo begin log
        # enable one of sleep/while/find
        #sleep 63
        #while : ; do sleep 0.1; done
        find ~ 2>/dev/null 1>/dev/null
        echo end err 1>&2
        echo end log
    } \
    2> >(
            trap "echo 'trapped 2 SIGCHLD' >$mypts" SIGCHLD
            trap "echo 'trapped 2 SIGHUP' >$mypts" SIGHUP
            trap "echo 'trapped 2 SIGINT' >$mypts" SIGINT
            trap "echo 'trapped 2 SIGPIPE' >$mypts" SIGPIPE
            trap "echo 'trapped 2 SIGSEGV' >$mypts" SIGSEGV
            trap "echo 'trapped 2 SIGSYS' >$mypts" SIGSYS
            trap "echo 'trapped 2 SIGTERM' >$mypts" SIGTERM
            echo begin 2 >$mypts
            awk '{ print "processed by 2: " $0 }' >$mypts &
            wait
            echo end 2 >$mypts
        ) \
    1> >(
            trap "echo 'trapped 1 SIGCHLD' >$mypts" SIGCHLD
            trap "echo 'trapped 1 SIGHUP' >$mypts" SIGHUP
            trap "echo 'trapped 1 SIGINT' >$mypts" SIGINT
            trap "echo 'trapped 1 SIGPIPE' >$mypts" SIGPIPE
            trap "echo 'trapped 1 SIGSEGV' >$mypts" SIGSEGV
            trap "echo 'trapped 1 SIGSYS' >$mypts" SIGSYS
            trap "echo 'trapped 1 SIGTERM' >$mypts" SIGTERM
            echo begin 1 >$mypts
            awk '{ print "processed by 1: " $0 }' >$mypts &
            wait
            echo end 1 >$mypts
        )
    echo end fnc
}

h4

echo finish

获取 ascii-art 进程树(在单独的终端中):

ps axjf | less

---


---

我很难理解信号是如何在 bash 中传播的,以及哪个陷阱会处理它们。

我这里有 3 个例子。每个示例都使用 2 个变体进行了测试,即任一行都未注释。示例是由这个伪代码构建的:

main_trap
func
    compound_statement(additional_traps) > process_redirection(additional_traps)

我对每个示例都尝试了几次。我得到的结果很少,我发布了我找到的那种。

测试如下:

  1. 将脚本放入文件中
  2. 运行脚本文件
  3. 在脚本仍在运行时按Ctrl+C

注意:简单地将这些脚本复制粘贴到现有的 bash shell 中会产生与我从文件执行时得到的不同结果。为了限制这个问题的长度,我没有附上这些结果。

我的终极问题是:

我已经使用这种布局(复合语句+进程重定向)来运行一些代码,并过滤并保存输出。现在出于某种原因,我决定最好保护此设置不因中断而终止,但我发现很难做到这一点。我很快就发现,仅仅在脚本开头调用 trap 是不够的。

有什么方法可以使用 bash / trap 保护我的脚本免受信号的影响(并安装正确的关闭顺序)?

信号往往会先清除日志记录,所以我无法捕捉到主进程的垂死线......

(我在问题的末尾添加了更多的想法和分析。)

这将是一个很长的问题,但我认为发布我已经完成的工作将有助于了解正在发生的事情:

测试设置:

测试设置 1(1 只猫):

#!/bin/bash

# variation 1:
trap "echo 'trapped' >/dev/stderr" SIGTERM SIGINT SIGHUP SIGPIPE

# variation 2:
#trap "echo 'trapped'" SIGTERM SIGINT SIGHUP SIGPIPE

h {
    {
        echo begin
        ( trap "echo 'trapped inner' >/dev/stderr" SIGTERM SIGINT SIGHUP SIGPIPE;
          sleep 63 )
        echo end
    } \
    2> >( trap "echo 'trapped 2' >/dev/stderr" SIGTERM SIGINT SIGHUP SIGPIPE;
          cat ) \
    1> >( trap "echo 'trapped 1' >/dev/stderr" SIGTERM SIGINT SIGHUP SIGPIPE;
          cat )
    echo end 2
}

h
echo finish

结果:

# variation 1:
# trap "echo 'trapped' >/dev/stderr" SIGTERM SIGINT SIGHUP SIGPIPE
begin
^Ctrapped 2
Segmentation fault

# variation 2:
# trap "echo 'trapped'" SIGTERM SIGINT SIGHUP SIGPIPE
begin
^Cend 2
finish
trapped 2

begin
^Ctrapped 2
end 2
finish

begin
^Ctrapped 2
Segmentation fault

测试设置 2(2 只猫):

#!/bin/bash

# variation 1:
trap "echo 'trapped' >/dev/stderr" SIGTERM SIGINT SIGHUP SIGPIPE

# variation 2:
#trap "echo 'trapped'" SIGTERM SIGINT SIGHUP SIGPIPE

h2 {
    {
        echo begin
        ( trap "echo 'trapped inner' >/dev/stderr" SIGTERM SIGINT SIGHUP SIGPIPE;
          sleep 63 )
        echo end
    } \
    2> >( trap "echo 'trapped 2' >/dev/stderr" SIGTERM SIGINT SIGHUP SIGPIPE;
          cat; cat ) \
    1> >( trap "echo 'trapped 1' >/dev/stderr" SIGTERM SIGINT SIGHUP SIGPIPE;
          cat; cat )
    echo end 2
}

h2
echo finish

结果:

# variation 1:
# trap "echo 'trapped' >/dev/stderr" SIGTERM SIGINT SIGHUP SIGPIPE
begin
^Ctrapped 2
end 2
finish
end
trapped 1
trapped

begin
^Ctrapped 2
end 2
finish
end
trapped

begin
^Cend 2
finish
trapped 2
end
trapped inner
trapped
trapped 1

# variation 2:
# trap "echo 'trapped'" SIGTERM SIGINT SIGHUP SIGPIPE
begin
^Ctrapped 2
end 2
finish
trapped inner
trapped 1
trapped
end

begin
^Ctrapped 2
end 2
finish
trapped
end
trapped inner
trapped 1

begin
^Ctrapped 2
end 2
finish
trapped inner
trapped 1
trapped
end

测试设置 3(2 只猫,无睡眠子外壳):

#!/bin/bash

# variation 1:
trap "echo 'trapped' >/dev/stderr" SIGTERM SIGINT SIGHUP SIGPIPE

# variation 2:
#trap "echo 'trapped'" SIGTERM SIGINT SIGHUP SIGPIPE

h3 {
    {
        echo begin
        sleep 63
        echo end
    } \
    2> >( trap "echo 'trapped 2' >/dev/stderr" SIGTERM SIGINT SIGHUP SIGPIPE;
          cat; cat ) \
    1> >( trap "echo 'trapped 1' >/dev/stderr" SIGTERM SIGINT SIGHUP SIGPIPE;
          cat; cat )
    echo end 2
}

h3
echo finish

结果:

# variation 1:
# trap "echo 'trapped' >/dev/stderr" SIGTERM SIGINT SIGHUP SIGPIPE
begin
^Ctrapped 2
end 2
finish
end
trapped 1
trapped

begin
^Ctrapped 2
end 2
finish
trapped 1
trapped
end

begin
^Cend 2
finish
trapped 2
trapped 1
trapped
end

begin
^Cend 2
finish
end
trapped 2
trapped 1
trapped

begin
^Cend 2
finish
trapped 2
end
trapped
trapped 1

begin
^Cend 2
finish
end
trapped 2

# variation 2:
# trap "echo 'trapped'" SIGTERM SIGINT SIGHUP SIGPIPE
begin
^Cend 2
trapped 2
finish
trapped
end
trapped 1

begin
^Ctrapped 2
end 2
finish
trapped
end
trapped 1

begin
^Ctrapped 2
end 2
finish
trapped 1
trapped
end

我的分析:

我添加所有 3 个测试用例的主要原因是因为有时我得到了 SEGFAULT。我对它进行了转储,但找不到它的来源。这似乎在某种程度上取决于主陷阱中的回声是否重定向到/dev/stderrvariation 1)或没有(variation 2)。

Ctrl+C之后,通常"trapped 2"首先被激活,很少"end 2"。这表明(与我最初的看法相反),处理信号时不涉及进程层次结构。正在运行的进程(复合语句、2 个进程替换、在 h 和 h2 中的子shell、sleep 进程、cat 进程)并行运行,并且在传递信号时恰好正在运行,将处理它。出于某种原因,这主要是 stderr 重定向的进程替换。我想cat 是主接收器,它没有安装信号处理程序,所以它就死了(这就是为什么我尝试添加 2 个cats,以便第二个可以保持子shell 运行)。

这就是重点,我没有真正的线索,会发生什么。 (我什至不知道,如果我做到这一点......)

我认为,信号将从cat 传播到它的包含进程,即进程替换 bash shell,它安装了信号处理程序,并打印"trapped 2"

现在,我原以为故事会到此结束,一枚戒指被伊熙尔杜摧毁,佛罗多留在家里……但没有。不知何故,它冒了出来,并设法杀死了sleep。即使有 2 个cats,所以如果一个被销毁,子shell 仍然保持活动状态。我发现SIGPIPE 很可能是杀死睡眠的原因,因为没有捕获它,我看到的行为与我在此处发布的不同。但有趣的是,我似乎需要在每个位置trapSIGPIPE,而不仅仅是在睡眠子shell中,或者再次显示不同的行为。

我猜,SIGPIPE 信号到达sleep,杀死它,所以复合语句中只剩下一个echo,它执行,并且那个子shell 完成了。 stdout 重定向的进程替换也被杀死了,可能是另一个SIGPIPE 被杀死的复合语句/函数外壳?

更有趣的是,有时"trapped 1" 根本不显示。

奇怪的是我没有看到 50% "trapped 2" 和 50% "trapped 1"

我可以做什么,我想要什么?

请记住,我的目标是有序关闭系统/服务/脚本。

1) 首先,如我所见,如果“业务流程”,这里由sleep/cat 表示,没有自己的信号处理,没有多少@ 987654358@可以让他们免于被杀。

2) 信号处理程序不是继承的,每个子shell 都必须有自己的陷阱系统。

3) 没有什么能像进程组那样以公共方式处理信号,无论信号碰巧碰到哪个进程都会做它的事情,并且在那里被杀死的进程的结果可能会在进程树中传播得更远。

不过,我不清楚,如果一个进程不能处理一个信号,它会把它扔到它的包含外壳吗?或者是另一个信号,传递了什么?有些东西肯定会通过,否则不会触发信号处理程序。

在/我的理想世界中,trap 将保护外壳内安装它的任何东西不接收信号,因此sleep-s、cat-s 将通过指定的清理关闭功能:杀死sleep,其余的将记录它的最后几行,然后跟随 - 相反:所有的记录都被清除,只有在此之后,主进程才会最终被杀死。 ..

我错过了一些琐碎的事情吗?设置 -o 魔法?继续添加更多的陷阱,直到它突然起作用??

问题:

Ctrl+C 之后信号如何真正传播?

SEGFAULT 来自哪里?

最重要的:

我可以从记录开始保护这个结构不被信号夷为平地吗?或者我应该避免进程替换,并提出另一种类型的输出过滤/日志记录?

测试:

GNU bash,版本 4.4.12(1)-release (x86_64-pc-linux-gnu)

补充说明:

在完成测试后,我发现了这些 QA-s,我认为这可能与我的情况有关,但我不知道具体如何使用它们:

How to use trap reliably using Bash running foreground child processes

Trap signal in child background process

不过,我尝试用while : ; do sleep 0.1; done 替换sleep 63,结果如下:

测试设置 1:

# (both variations)
# 1 Ctrl + C got me a SEGFAULT
begin
^Ctrapped 2
Segmentation fault

# 2 Ctrl + C got me a SEGFAULT
begin
^Ctrapped 2
^CSegmentation fault

测试设置 2:

# variation 1
# trap "echo 'trapped' >/dev/stderr" SIGTERM SIGINT SIGHUP SIGPIPE
begin
^Ctrapped 2
trapped 1
trapped inner
^Ctrapped 2
^CSegmentation fault

# variation 2
# trap "echo 'trapped'" SIGTERM SIGINT SIGHUP SIGPIPE
begin
^Ctrapped 2
trapped inner
trapped 1
^Ctrapped 2
Segmentation fault

begin
^Ctrapped 2
trapped inner
trapped 1
^Ctrapped 2
^CSegmentation fault

测试设置 3:

# variation 1
# trap "echo 'trapped' >/dev/stderr" SIGTERM SIGINT SIGHUP SIGPIPE
begin
^Ctrapped 2
trapped 1
trapped
^Ctrapped 2
^CSegmentation fault

# variation 2
# trap "echo 'trapped'" SIGTERM SIGINT SIGHUP SIGPIPE
begin
^Ctrapped 2
trapped 1
trapped
^Ctrapped 2
^CSegmentation fault

^Ctrapped 2
trapped 1
trapped
^Ctrapped 2
Segmentation fault

所以,虽然这让我可以利用 2 个cat-s,允许 2 个Ctrl+C-s,但它总是让我得到SEGFAULT,仍然不知道它来自哪里。

【问题讨论】:

  • @shellter 是的,为什么不呢? Ctrl-C 只是一个 SIGINT,它是可屏蔽的。请注意,bash 的行为取决于交互性、作业控制是否有效,bash 手册页特别提到了 SIGINT 在不同条件下的行为不同。不幸的是,我不是这方面的专家。我的问题是专门针对从脚本文件(不是来源,不是复制粘贴)运行脚本的情况。
  • 是的,刚刚测试过,你可以捕获Ctrl-C。好的..我记得一位 Unix 老师向我们展示(30 年前;-/!)如何使用嵌套陷阱来捕捉用户反复按下 break 键。而且我想我已经在这里发布的至少一个代码块中看到了这一点,但这是一件罕见的事情。也许它被记录在某个地方并且可以帮助你(只是给你一些想法,因为 bash a-team 没有回应)。祝你好运!
  • 谢谢!您最初的评论已经让我开始对信号进行更详细的研究,希望能找到一些有用的东西。

标签: linux bash shell process-substitution


【解决方案1】:

经过无数次的实验,我得出了一个结论:想要做我想做的事是不可能的,但我还是不明白每一个细节。

我发布了我的发现,但暂时不会接受我的回答,以防万一——希望——有人对正在发生的事情有更好的了解。

看来,我错了好几处……

1) SEGFAULT 来自于写入已关闭的 fd (stderr)。但是我认为这是在 bash 深处甚至在内核级别触发的,可能是某种竞争条件 - 我会假设,一个 bash 管理的进程树会在关闭的 I/O 的剩余虚拟内存地址上出现段错误(我怀疑,这会导致错误)。无论如何,用正确的 TTY 设备替换 /dev/stderr 似乎可以解决这个问题。

Write to terminal after redirecting stdout to a file without using stderr?

echo or print /dev/stdin /dev/stdout /dev/stderr

Portability of “> /dev/stdout”

2)日志在记录进程之前停止的整个问题来自于它们都在前台进程组中。在Ctrl+C 上,终端会将SIGINT 传递给fg 进程组中的每个 进程。打印进程树后结果显示,记录器进程是打印数组中的第一个,因此它们可能是第一个被交付并处理 SIGINT 的进程。

How does Ctrl-C terminate a child process?

How to make a program reading stdin run in background on linux?

Control which process gets cancelled by Ctrl+C

3) 生成进程的 shell 无法控制信号传递,实际上它正在等待,因此无法在该 shell 中设置一些魔法来保护由 shell 启动的 cat 之类的东西没有安装信号处理器。

4) 看到问题是由 fg 进程组中的所有进程引起的,显然将不必要的进程移到后台将是解决方案,例如:

2> >( cat & )

不幸的是,在这种情况下,cat 没有输出,而是立即终止。

我怀疑,这与获得SIGSTOP 的后台工作有关,如果它的stdin 在后台运行时是打开的。

Writing to stdin of background process

Linux process in background - “Stopped” in jobs?

Why is SIGINT not propagated to child process when sent to its parent process?

注意:setsid cmd 将使cmd 在其自己的会话中启动,该会话将有一个全新的进程组,其中将单独包含cmd,因此它可能用于分隔记录器和已记录。我没有考虑清楚,也没有尝试过。

Running a process in the background with input/output redirection

进一步参考:

Send command to a background process

Signals

Process group

Job control (Unix)

Why Bash is like that: Signal propagation

How to propagate SIGTERM to a child process in a Bash script

结论

在设置中:

{
    cmd
} \
2> >(logger) \
1> >(logger)

我没有找到在进程组级别将cmdloggers 分开的好方法。后台处理 loggers 会禁用它们接收输出,而是立即终止,可能通过 SIGSTOP

一种解决方案可能是使用命名管道,这将允许更好的控制,并且可以分离记录的和记录器进程。但是,我最初决定使用 bash 提供的进程替换,以避免手动编码管道所增加的复杂性。

我最终选择的方式是简单地将整个进程树(cmd + loggers)放在后台,让另一个级别处理信号。

f {
    {
        cmd
    } \
    2> >(logger) \
    1> >(logger)
}

trap ...

set -m
f &
wait

更新:

我意识到仅仅后台是不够的,因为非交互式 shell(从文件运行脚本)不会在单独的进程组中运行后台进程。为此,最简单的选择是将 shell 设置为交互模式:set -m。 (我希望这不会引起新的问题,目前看来还不错。)

注意:setsid 不适用于函数,因此主脚本需要自己的文件并从第二个脚本文件开始。

Prevent SIGINT from interrupting function call and child process(es) within

【讨论】:

  • 哇,感谢您的更新。只是一些cmets: 1.我本来想抱怨你使用进程替换。 (现在我会;-))。为什么不用管道? (带有进程组,即{ cmd ; } 2>&1 | { logger ; })。 2. 可以考虑使用ksh吗? (不幸的是不再那么好支持了)。它有一些不同的高级功能(不太确定它如何处理陷阱),但您可能会在那里找到一些关键信息。 3. 你的 Q 有很多需要考虑的地方。你能在它的前面加上一个“执行摘要”吗?-)(有一个简单/小测试用例,你的预期输出与当前输出)。祝你好运
  • cmets 的 TY。我会考虑总结一下。我不想将 stderr 和 stdout 引导到 1 个流中,因为我认为这是一个很好的错误信号,如果有东西进入不应该进入的流。我的整个脚本如果相当复杂,而且我认为日志块越小越好。进程替换似乎很有吸引力,因为{ cmd ; } 1> >( filter | log 2>&1 | logger_errors) 2> >( filter | log 2>&1 | logger_errors) 可以很好地缩进。如果 bash 为我提供了相当于很多管道的功能,为什么不使用它呢?另一方面,我不知道这些限制。
  • (我的字符用完了。)基本上,我觉得进程替换是一种更简洁的方法。在 1-2-3-liners 的情况下,cmd 有一个简短的参数列表,或者如果脚本不打算只在 2-3 台机器上运行(所以每一位日志都是宝藏)无人看管,你不希望几天后挠头“那么这个输出到底是从哪里来的?” - 如果这些都不是问题,我会使用管道。但是,如果所有这些都是问题,我会尝试按来源分隔我的日志,并且在该设置中,我认为进程替换提供了更清晰的代码。我可能错了。而且这种方法有其不灵活...
猜你喜欢
  • 1970-01-01
  • 2019-05-20
  • 2016-12-18
  • 2023-03-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多