4

更新

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

#!/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 中会产生与我从文件执行时得到的不同结果。为了限制这个问题的长度,我没有附上这些结果。

我的终极问题是:

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

有什么方法可以使用 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/stderr变体 1)或不(变体 2)。

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

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

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

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

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

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

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

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

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

1)首先,正如我所看到的,如果这里由/表示的“业务流程”没有自己的信号处理,那么再多的也无法挽救它们免于被杀死。sleepcattrap

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

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

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

在一个/我的理想世界中,atrap将保护安装它的 shell 中的任何东西不接收信号,因此sleep-s, cat-s 将被指定的清理功能关闭:杀死sleep,其余的将记录它的最后一个行,然后跟随- 而不是:所有日志记录都被清除,只有在那之后主进程才会最终被杀死......

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

问题:

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

从哪里来SEGFAULT

最重要的:

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

经测试:

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

进一步说明:

完成测试后,我发现了这些 QA-s,我认为这可能与我的案例有关,但我不知道,我究竟该如何使用它们:

如何使用运行前台子进程的 Bash 可靠地使用陷阱

子后台进程中的陷阱信号

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

测试设置 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,仍然不知道它来自哪里。

4

1 回答 1

1

经过无数次的实验,我得出了一个结论,即不可能做我想做的事,但我仍然不了解每一个细节。

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

看来,我搞错了好几件事……

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

将标准输出重定向到文件而不使用标准错误后写入终端?

回显或打印 /dev/stdin /dev/stdout /dev/stderr

“> /dev/stdout”的可移植性</a>

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

Ctrl-C 如何终止子进程?

如何让读取stdin的程序在linux后台运行?

控制通过 Ctrl+C 取消哪个进程

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

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

2> >( cat & )

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

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

写入后台进程的标准输入

后台的 Linux 进程 - 在作业中“停止”?

为什么 SIGINT 在发送到其父进程时不会传播到子进程?

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

使用输入/输出重定向在后台运行进程

更多参考资料:

向后台进程发送命令

信号

进程组

作业控制 (Unix)

为什么 Bash 是这样的:信号传播

如何在 Bash 脚本中将 SIGTERM 传播到子进程

结论

在设置中:

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

我没有找到在进程组级别与 scmd分开的好方法。logger后台loggers 使它们无法接收输出,而是立即终止,可能通过SIGSTOP.

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

我最终选择的方式是简单地将整个进程树(cmd+ loggers)作为背景,并让另一个级别处理信号。

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

trap ...

set -m
f &
wait

更新:

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

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

防止 SIGINT 中断函数调用和子进程

于 2017-11-01T20:58:41.307 回答