Bash 管道信号传播-它是如何工作的?

Bash 管道信号传播-它是如何工作的?

回答时问题,我无法完全解释信号如何通过管道传播。

请考虑以下示例。

用作timeout管道中的第一个元素

这导致gpg抓住了被SIGTERM交付的cattimeout并留下了损坏的文件。

$ timeout 1 cat /dev/urandom | gpg -er [email protected] > ./myfile.gpg

gpg: Terminated caught ... exiting
Terminated
$ gpg -d < ./myfile.gpg > /dev/null

You need a passphrase to unlock the secret key for
user: "Attie Grande <[email protected]>"
4096-bit RSA key, ID C9AEA6AE, created 2016-12-13 (main key ID 7826F053)

gpg: encrypted with 4096-bit RSA key, ID C9AEA6AE, created 2016-12-13
      "Attie Grande <[email protected]>"
gpg: block_filter 0x145e790: read error (size=14775,a->size=14775)
gpg: block_filter 0x145f110: read error (size=10710,a->size=10710)
gpg: WARNING: encrypted message has been manipulated!
gpg: block_filter: pending bytes!
gpg: block_filter: pending bytes!

timeout在管道中间使用

这按预期工作 -gpg干净退出。

$ cat /dev/urandom | timeout 1 cat | gpg -er [email protected] > ./myfile.gpg
$ gpg -qd < ./myfile.gpg > /dev/null

You need a passphrase to unlock the secret key for
user: "Attie Grande <[email protected]>"
4096-bit RSA key, ID C9AEA6AE, created 2016-12-13 (main key ID 7826F053)

使用SIGUSR1而不是SIGTERM

再次,它按预期工作 -gpg干净退出。我期望因为cat退出SIGUSR1,而gpg忽略它。

$ timeout -sUSR1 1 cat /dev/urandom | gpg -er [email protected] > ./myfile.gpg
$ gpg -qd < ./myfile.gpg > /dev/null

You need a passphrase to unlock the secret key for
user: "Attie Grande <[email protected]>"
4096-bit RSA key, ID C9AEA6AE, created 2016-12-13 (main key ID 7826F053)

使用进程替换

再次,这有效 - 尽管我并没有想到它会有效。

$ gpg -er [email protected] > ./myfile.gpg < <( timeout 1 cat /dev/urandom )
$ gpg -qd < ./myfile.gpg > /dev/null

You need a passphrase to unlock the secret key for
user: "Attie Grande <[email protected]>"
4096-bit RSA key, ID C9AEA6AE, created 2016-12-13 (main key ID 7826F053)

我只能假设管道中第一个元素的信号会传播到管道中的其余元素(即使将它们分开也会timeout cat | cat | gpg失败)。

我查看了文档,并且尝试了一下set -eset -o pipefail但它们并没有按照我预期的那样运行。

  • 到底发生了什么事?
  • 语义是什么?
  • 我们能控制这个吗?
  • 有没有比将信号生成过程从管道前端移开更好的方法?

答案1

我只能假设管道中第一个元素的信号会传播到管道中的其余元素。

据我所知,没有这样的传播。我主要回答你的第一个问题:

到底发生了什么事?

简短回答

(这可能稍微简单化了。)

  1. 运行管道时,交互式bash启动进程组中的每个进程,其PGID(进程组 ID)等于PID管道第一部分的(进程 ID)。
  2. timeout将其自身更改PGID为其自身。如果是管道中的第一个命令,PID则不会发生任何变化。timeout
  3. timeout不仅向底层命令发送信号,还向其整个进程组发送信号。如果timeout是管道中的第一个命令,则其进程组仍将包括gpg,因此gpg将获得信号。

下面对该现象进行研究和阐述。


详尽阐述

1.bash行为

运行管道时,交互式bash启动进程组中的每个进程,其PGID进程组 ID 等于PID管道第一部分的进程 ID。这实际上与交互式无关;而是与作业控制是否启用有关。作业控制在交互式 Bash 中默认启用,在非交互式中禁用。您可以进行自己的测试(请参阅是否可以从中获取进程组 ID/proc?)。我还没有研究过更复杂的可能性(例如,如果第一个“命令”是子 shell 会怎么样?),在你的情况下它们并不重要。重要的是gpg在这些命令中

timeout 1 cat /dev/urandom | gpg -er [email protected] > ./myfile.gpg
cat /dev/urandom | timeout 1 cat | gpg -er [email protected] > ./myfile.gpg
timeout -sUSR1 1 cat /dev/urandom | gpg -er [email protected] > ./myfile.gpg
gpg -er [email protected] > ./myfile.gpg < <( timeout 1 cat /dev/urandom )

等于PGIDPID

  • timeout
  • (首先)cat
  • timeout
  • gpg(即其本身)

分别。

2.timeout改变自身PGID(或不改变)

运行strace timeout 1 cat后你会看到以下内容:

setpgid(0, 0)

摘录自man 2 setpgid

int setpgid(pid_t pid, pid_t pgid);

setpgid()PGID将指定的进程的设置pidpgid。如果pid为零,则使用调用进程的进程 ID。如果pgid为零,则将PGID指定的进程的 pid 设置为其进程 ID。

这意味着timeout将其设置PGID为等于其PID。有两种可能性:

  • 如果timeout是第一个命令,其PGID前后相同setpgid,因此仍然与gpg相同;PGIDtimeout
  • 如果timeout不是第一个命令,它PGID就会被改变,即使最初两个命令gpg相同PGID,但现在也是不同的。timeoutPGID

3.timeout发出的信号比你预期的要多

同样的,strace timeout 1 cat还会显示如下内容:

kill(19401, SIGTERM)
kill(0, SIGTERM)

在这个例子中19401PIDcat。如果你使用,-s USR1那么将会有SIGUSR1而不是SIGTERM等。这个第二个kill负责你认为是通过管道的信号传播。参见man 2 kill(摘录):

int kill(pid_t pid, int sig);

如果pid等于0,则sig发送给调用进程的进程组中的每个进程。

调用进程是timeout。它将信号发送给其整个进程组。我承认我不确定这种行为的目的是什么。我猜timeout是故意设计来向其子进程发出信号的其子进程的后代。除非某个后代离开进程组,否则所有后代都会收到信号。

因此,如果timeout是管道中的第一个命令,则所选信号将发送到其每个部分(嗯,几乎;考虑timeout同一管道中的另一个部分)。这包括gpg。然后取决于gpg它如何对信号做出反应。


其他问题

我们能控制这个吗?有没有比将信号生成过程从管道前端移开更好的方法?

我快速搜索后没有找到设置/更改的常用工具PGID。我认为您可以编写自己的程序来调用setpgid(2)或类似的东西。但是现在,当我们知道发生了什么时,timeout从管道前端移动似乎是一种非常明智的方法。还有另一种方法:您可以(暂时)在交互式 Bash 中禁用作业控制。在子 shell 中执行此操作看起来是个好主意:

(set +m; timeout … | gpg …)

还请注意,这是由于timeout行为方式所致。其他信号生成过程可能不需要这样的解决方法。

相关内容