回答时这问题,我无法完全解释信号如何通过管道传播。
请考虑以下示例。
用作timeout
管道中的第一个元素
这导致gpg
抓住了被SIGTERM
交付的cat
,timeout
并留下了损坏的文件。
$ 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 -e
,set -o pipefail
但它们并没有按照我预期的那样运行。
- 到底发生了什么事?
- 语义是什么?
- 我们能控制这个吗?
- 有没有比将信号生成过程从管道前端移开更好的方法?
答案1
我只能假设管道中第一个元素的信号会传播到管道中的其余元素。
据我所知,没有这样的传播。我主要回答你的第一个问题:
到底发生了什么事?
简短回答
(这可能稍微简单化了。)
- 运行管道时,交互式
bash
启动进程组中的每个进程,其PGID
(进程组 ID)等于PID
管道第一部分的(进程 ID)。 timeout
将其自身更改PGID
为其自身。如果是管道中的第一个命令,PID
则不会发生任何变化。timeout
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 )
等于PGID
PID
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
将指定的进程的设置pid
为pgid
。如果pid
为零,则使用调用进程的进程 ID。如果pgid
为零,则将PGID
指定的进程的pid
设置为其进程 ID。
这意味着timeout
将其设置PGID
为等于其PID
。有两种可能性:
- 如果
timeout
是第一个命令,其PGID
前后相同setpgid
,因此仍然与gpg
相同;PGID
timeout
- 如果
timeout
不是第一个命令,它PGID
就会被改变,即使最初两个命令gpg
相同PGID
,但现在也是不同的。timeout
PGID
3.timeout
发出的信号比你预期的要多
同样的,strace timeout 1 cat
还会显示如下内容:
kill(19401, SIGTERM)
…
kill(0, SIGTERM)
在这个例子中19401
是PID
的cat
。如果你使用,-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
行为方式所致。其他信号生成过程可能不需要这样的解决方法。