bash陷阱和流程替换

时间:2017-10-30 10:10:00

标签: linux bash shell process-substitution

更新

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

#!/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
  4. 注意:简单地将这些脚本复制粘贴到现有的bash shell中会产生与从文件执行时得到的结果不同的结果。为了使这个问题的篇幅有限,我没有附上这些结果。

    我的最终问题是:

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

    有没有办法使用bash / trap保护我的脚本免受信号的影响(并安装正确的关机序列)?

    信号首先会消除记录,所以我无法捕捉主要过程中的死线...

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

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

    TEST SETUPS:

    TEST SETUP 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
    

    TEST SETUP 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。我coredumped它,但无法找到它,它来自哪里。它似乎在某种程度上取决于主陷阱中的回声是否重定向到/dev/stderr变体1 )或不是(变体2 )。

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

    这是重点,我没有真正的线索,会发生什么。 (我甚至都不知道,如果到目前为止我做到了......)

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

    现在,我想,故事会在这里结束,一个戒指被Isildur摧毁,佛罗多留在家里......但不是。它以某种方式起泡,并设法杀死sleep。即使有2 cat s,所以如果一个被销毁,则子shell保持活动状态。我发现很可能SIGPIPE是杀死睡眠的东西,因为没有捕获它,我看到的行为与我在这里发布的行为不同。但有趣的是,似乎我需要在每个位置trap SIGPIPE,而不仅仅是在睡眠子shell中,或者再次显示不同的行为。

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

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

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

    我能做什么,我想要这个?

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

    1)首先,正如我所看到的,如果" 业务流程",此处由sleep / cat表示不有自己的信号处理,trap没有任何数量可以防止它们被杀死。

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

    3)没有什么比一个进程组能够以公共方式处理信号,无论哪个进程信号发生罢工就会发挥作用,并且那里被杀死的进程的结果可能会在进程树中进一步传播。

    但是,我不清楚,如果一个进程无法处理一个信号,它会将它抛给它的包含shell吗?或者它是另一个信号,交付什么?肯定会有事情发生,否则信号处理程序就不会被触发。

    在/我的理想世界中,trap会保护安装它的shell中的任何内容不接收信号,因此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

    尽管如此,我尝试用sleep 63代替while : ; 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,仍然不知道,它在哪里来自。

1 个答案:

答案 0 :(得分: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上,终端会在fg进程组中向{em>进程每个进程提供一个SIGINT。在打印过程树之后结果显示,记录器过程是阵列中的第一个打印过程,因此可能它们是第一个要交付并处理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中设置一些魔法以保护由cat启动的内容。没有安装信号处理程序的shell。

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)

我找不到在进程组级别将cmdlogger分开的好方法。对logger进行后台操作会禁止它们接收输出,而是立即终止,可能是通过SIGSTOP

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

我最终选择的方法是简单地背景整个过程树(cmd + logger s),然后让另一个级别处理信号。

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

trap ...

set -m
f &
wait

UPDATE:

我意识到简单的后台操作是不够的,因为非交互式shell(从文件运行脚本)不会在单独的进程组中运行后台进程。为此,最简单的选择是将shell设置为交互模式:set -m。 (我希望这不会导致更新的问题,到目前为止似乎很好。)

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

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