为什么在脚本中的子 shell 中运行时,“作业”总是为已完成的进程返回一行?

ter*_*don 4 shell bash shell-script jobs

正常情况下,当一个job在后台启动时,在jobjobs完成后第一次运行会报告它已经完成,并且后续执行什么都不做:

$ ping -c 4 localhost &>/dev/null &
[1] 9666
$ jobs
[1]+  Running                 ping -c 4 localhost &> /dev/null &
$ jobs
[1]+  Done                    ping -c 4 localhost &> /dev/null
$ jobs  ## returns nothing
$ 
Run Code Online (Sandbox Code Playgroud)

但是,当在脚本的子 shell 中运行时,它似乎总是返回一个值。这个脚本永远不会退出:

#!/usr/bin/env bash
ping -c 3 localhost &>/dev/null &
while [[ -n $(jobs) ]]; do
    sleep 1; 
done
Run Code Online (Sandbox Code Playgroud)

如果我tee[[ ]]构造中使用来查看 的输出jobs,我会看到它总是在打印该Done ...行。正如我所料,不仅一次,而且显然是永远。

更奇怪的是,jobs在循环中运行会导致它按预期退出:

#!/usr/bin/env bash
ping -c 3 localhost &>/dev/null &
while [[ -n $(jobs) ]]; do
    jobs
    sleep 1; 
done
Run Code Online (Sandbox Code Playgroud)

最后,正如@mury 所指出的,第一个脚本按预期工作,如果从命令行运行则退出:

$ ping -c 5 localhost &>/dev/null & 
[1] 13703
$ while [[ -n $(jobs) ]]; do echo -n . ; sleep 1; done
...[1]+  Done                    ping -c 5 localhost &> /dev/null
$ 
Run Code Online (Sandbox Code Playgroud)

这是在我回答关于超级用户的问题时出现的,所以请不要发布推荐更好的方法来做循环的答案。我自己能想到几个。我很好奇的是

  1. 为什么jobs[[ ]]构造中的行为不同?为什么它总是会返回该Done...行,而手动运行时却不会?

  2. 为什么jobs在循环内运行会改变脚本的行为?

Sco*_*ott 6

当然,您知道这$(…)会导致括号内的命令在子shell 中运行。当然,你知道这jobs是一个内置的 shell。好吧,jobs一旦报告了它的死亡,它看起来就像从 shell 的内存中清除了一个作业。但是,当您运行时$(jobs),该jobs命令在子shell中运行,因此它没有机会告诉父shell(正在运行脚本的那个ping)已经报告了后台作业的死亡(在您的示例中) . 所以,每次 shell 产生一个子 shell 来运行这个$(jobs)东西时,这个子 shell 仍然有一个完整的作业列表(即,ping作业在那里,即使它在第 5 次迭代后就死了),等等jobs仍然(再次)认为它需要报告ping 工作(即使它在过去四秒钟内已经死亡)。

这解释了为什么jobs在循环中运行一个纯粹的命令会导致它按预期退出:一旦您jobs 在父 shell 中运行,父 shell 就知道作业的终止已报告给用户。

为什么它在交互式 shell 中有所不同?因为,每当交互式 shell 的前台子 进程终止时,shell 都会报告在前台进程运行时已终止的任何后台作业1。所以,ping当终止sleep 1正在运行,而当sleep终止,在后台作业的死壳报告。等等。


1说“在前台进程运行时改变状态的任何后台作业”可能更准确。我相信它也可能报告已暂停(kill -SUSP,相当于Ctrl+的程序化Z)或未暂停(kill -CONT,这是fg命令所做的)的作业。

  • 因为直接在运行脚本的 shell(“父 shell”)中运行 `jobs` 会更改父 shell 内存中的信息。而`$(jobs)`的_subsequent_调用受该父shell的fork(和_not_一个exec)影响,导致子shell进程在fork时具有父shell的内存副本。 (2认同)