为啥我不能在 bash 脚本中使用作业控制?

Posted

技术标签:

【中文标题】为啥我不能在 bash 脚本中使用作业控制?【英文标题】:Why can't I use job control in a bash script?为什么我不能在 bash 脚本中使用作业控制? 【发布时间】:2010-10-15 23:19:24 【问题描述】:

在this answer 到另一个question,有人告诉我

在您没有作业控制的脚本中 (并且试图打开它是愚蠢的)

这是我第一次听到这个,我仔细研究了作业控制(第 7 章)的 bash.info 部分,发现没有提到任何一个断言。 [更新:手册页稍微好一点,提到了“典型”使用、默认设置和终端 I/O,但没有真正的理由说明作业控制对于脚本来说特别不明智。]

那么为什么基于脚本的作业控制不起作用,是什么使它成为一种不好的做法(又名“愚蠢”)?

编辑: 有问题的脚本启动一个后台进程,启动第二个后台进程,然后尝试将第一个进程放回前台,以便它具有正常的终端 I/O(好像直接运行),然后可以从脚本外部重定向。不能对后台进程执行此操作。

正如accepted answer 对另一个问题所指出的那样,存在其他脚本可以在不尝试作业控制的情况下解决该特定问题。美好的。并且被抨击的脚本使用了一个硬编码的工作编号——显然很糟糕。但我试图了解工作控制是否是一种从根本上注定要失败的方法。似乎它仍然可以工作......

【问题讨论】:

请添加一个简单的示例,说明如果没有作业控制就无法轻松完成此功能。 【参考方案1】:

他的意思是作业控制在非交互模式下默认关闭(即在脚本中。)

来自bash 手册页:

JOB CONTROL
       Job  control refers to the ability to selectively stop (suspend)
       the execution of processes and continue (resume) their execution at a
       later point.
       A user typically employs this facility via an interactive interface
       supplied jointly by the system’s terminal driver and bash.

   set [--abefhkmnptuvxBCHP] [-o option] [arg ...]
      ...
      -m      Monitor mode.  Job control is enabled.  This option is on by
              default for interactive shells on systems that support it (see
              JOB CONTROL above).  Background processes run in a separate
              process group and a line containing their exit status  is
              printed  upon  their completion.

当他说“愚蠢”时,他的意思不仅是:

    是作业控制意味着主要用于促进交互式控制(而脚本可以直接使用 pid),但也 我引用他的原始答案,... 依赖于您之前没有在脚本中开始任何其他工作这一事实,这是一个不好的假设。这是完全正确的。

更新

回答您的评论:是的,没有人会阻止您在 bash 脚本中使用作业控制 -- 强制禁用 set -m 没有困难的情况(即是的,作业控制来自如果您愿意,该脚本将起作用。)请记住,最终,尤其是在脚本编写中,总是有不止一种方法可以给猫剥皮,但有些方法更便携,更可靠,更容易处理错误情况,解析输出等。

您的特定情况可能会或可能不会保证与lhunath(和其他用户)认为的“最佳做法”不同。

【讨论】:

+1 准确详细。作业控制是一种使在(交互式)提示上处理作业更加方便的功能。没有理由任何人都希望在脚本中使用它,因为您只需保留后台进程的 PID 并等待杀死它们。 谢谢!奇怪的是,手册页的信息比 bash.info 文件更好。 好吧,我明白硬编码的工作编号是个坏主意。那里没有问题。但是诸如“默认用于交互”和“用户通常使用”和“意味着大部分用于”之类的词都强烈暗示存在一些脚本中作业控制的深奥用例。否则 set -m 应该在脚本中失败。 在脚本中启用作业控制有一个非常重要的原因:将后台进程放置在自己的进程组中的副作用。这使得通过一个简单的命令将信号发送给他们和他们的孩子变得非常容易:kill -<signal> -$pgid。处理整个进程树的信号的所有其他方法要么涉及复杂的(有时甚至是递归的)函数,这通常是错误的,要么有杀死进程中父进程的风险(没有双关语)。 然后,例如,您将如何启动后台进程,运行几个设置命令,然后使用 pid 将其移至前台(我经常为 docker 容器这样做)?跨度> 【参考方案2】:

正如您所说,Bash 确实支持作业控制。在编写 shell 脚本时,通常假设您不能依赖于您拥有 bash 的事实,而是拥有 vanilla Bourne shell (sh),它在历史上没有作业控制。

这些天来,我很难想象一个系统,其中您确实被限制在真正的 Bourne shell 中。大多数系统的/bin/sh 将链接到bash。不过,这是可能的。您可以做的一件事是不要指定

#!/bin/sh

你可以这样做:

#!/bin/bash

那和您的文档将明确您的脚本需要bash

【讨论】:

在 Ubuntu 上,/bin/sh 没有链接到 Bash。所以你需要#!/bin/bash。【参考方案3】:

作业控制仅在您运行交互式 shell 时有用,即您知道标准输入和标准输出连接到终端设备(Linux 上的 /dev/pts/*)。然后,将某些内容放在前台,将其他内容放在后台等是有意义的。

另一方面,脚本没有这样的保证。脚本可以是可执行的,并且可以在不连接任何终端的情况下运行。在这种情况下,拥有前台或后台进程是没有意义的。

但是,您可以在后台以非交互方式运行其他命令(将“&”附加到命令行)并使用$! 捕获它们的 PID。然后使用kill 杀死或挂起它们(在终端上模拟 Ctrl-C 或 Ctrl-Z,shell 是交互式的)。您也可以使用wait(而不是fg)等待后台进程完成。

【讨论】:

“fg 1”专门用于使 &'d 进程的标准输入和标准输出重新连接到交互式终端会话。然后脚本的调用者(一个人或另一个脚本)可以选择是否重定向它们。【参考方案4】:

可能是 o/t,但我经常在 ssh 进入服务器以执行长时间运行的作业时使用 nohup,这样即使我退出,作业仍然可以完成。

我想知道人们是否对从主交互式 shell 停止和启动以及产生后台进程感到困惑? wait 命令允许你生成很多东西,然后等待它们全部完成,就像我说的我一直使用 nohup。它比这更复杂,也没有得到充分利用 - sh 也支持这种模式。看看说明书。

你也有

kill -STOP pid

如果我想暂停当前正在运行的 sudo,我经常会这样做,例如:

kill -STOP $$

但是,如果你从编辑器跳到 shell 中,那你就有祸了——它只会坐在那里。

我倾向于使用助记符 -KILL 等,因为有打字的危险

kill - 9 pid # note the space

在过去,您有时可以让机器停机,因为它会杀死 init!

【讨论】:

OT 但有趣的信息。非常非常OT:你的名字发音和“fish”一样吗? 是的,我的姓是 Fish,我经常使用 ghoti,看看有没有人注意到!【参考方案5】:

作业确实在 bash 脚本中工作

但是,你……需要注意生成的员工 喜欢:

ls -1 /usr/share/doc/ | while read -r doc ; do ... done

作业在 | 的每一侧都有不同的上下文

绕过这个可能使用 for 而不是 while:

for `ls -1 /usr/share/doc` ; do ... done

这应该演示如何在脚本中使用作业... 提到我的评论是......真实的(不知道为什么这种行为)

    #!/bin/bash


for i in `seq 7` ; do ( sleep 100 ) &  done

jobs

while [ `jobs | wc -l` -ne 0 ] ; do

    for jobnr in `jobs | awk 'print $1' | cut -d\[ -f2- |cut -d\] -f1` ; do
        kill %$jobnr
    done
    #this is REALLY ODD ... but while won't exit without this ... dunno why
    jobs >/dev/null 2>/dev/null
done

sleep 1
jobs

【讨论】:

工作确实有效,但问题不在于工作,而在于工作控制fgbg 等)【参考方案6】:

带有bgfg 的作业控制仅在交互式shell 中有用。但是&wait 在脚本中也很有用。

在多处理器系统上生成后台作业可以大大提高脚本的性能,例如在构建脚本中,您希望每个 CPU 至少启动一个编译器,或使用 ImageMagick 工具并行处理图像等。

以下示例运行多达 8 个并行 gcc 来编译数组中的所有源文件:

#!bash
...
for ((i = 0, end=$#sourcefiles[@]; i < end;)); do
    for ((cpu_num = 0; cpu_num < 8; cpu_num++, i++)); do
        if ((i < end)); then gcc $sourcefiles[$i] & fi
    done
    wait
done

这没有什么“愚蠢”的。但是您需要wait 命令,该命令在脚本继续之前等待所有后台作业。最后一个后台作业的PID存储在$!变量中,所以你也可以wait $!。还要注意nice 命令。

有时此类代码在 makefile 中很有用:

buildall:
    for cpp_file in *.cpp; do gcc -c $$cpp_file & done; wait

这比make -j 提供了更好的控制。

注意&amp; 是像; 一样的行终止符(写成command&amp; 而不是command&amp;;)。

希望这会有所帮助。

【讨论】:

对于读者:wait 还允许多个 pid,例如wait 3940 4001 4012 4024,但会等待所有完成后再继续。 最近我在脚本中将 Emacs/Lisp .el 文件字节编译为 .elc。在wait 之后,所有 Emacssen 都完成了,但并不是所有的 .elc 文件都在那里。我还必须等待他们,比如while [[ ! -e $file ]]; do :; done。发生在 Windows/Cygwin 下,但我认为这可能发生在任何文件系统下。所以在 Makefiles 之外,如果需要文件,最好不要简单地信任 wait【参考方案7】:

在脚本中打开作业控制以设置陷阱可能很有用 SIGCHLD。手册中的作业控制部分说:

每当作业更改状态时,shell 会立即学习。一般, bash 等到它要打印提示才报告 更改作业状态,以免中断任何其他输出。如果 启用了 set 内置命令的 -b 选项,bash 报告 立即进行此类更改。 SIGCHLD 上的任何陷阱都会针对每个 退出的孩子。

(重点是我的)

以下面的脚本为例:

dualbus@debian:~$ cat children.bash 
#!/bin/bash

set -m
count=0 limit=3
trap 'counter &&  job & ' CHLD
job() 
  local amount=$((RANDOM % 8))
  echo "sleeping $amount seconds"
  sleep "$amount"

counter() 
  ((count++ < limit))

counter &&  job & 
wait
dualbus@debian:~$ chmod +x children.bash 
dualbus@debian:~$ ./children.bash 
sleeping 6 seconds
sleeping 0 seconds
sleeping 7 seconds

注意:从 bash 4.3 开始,CHLD 捕获似乎已被破坏

在 bash 4.3 中,您可以使用 'wait -n' 来实现相同的目的, 不过:

dualbus@debian:~$ cat waitn.bash 
#!/home/dualbus/local/bin/bash

count=0 limit=3
trap 'kill "$pid"; exit' INT
job() 
  local amount=$((RANDOM % 8))
  echo "sleeping $amount seconds"
  sleep "$amount"

for ((i=0; i<limit; i++)); do
  ((i>0)) && wait -n; job & pid=$!
done
dualbus@debian:~$ chmod +x waitn.bash 
dualbus@debian:~$ ./waitn.bash 
sleeping 3 seconds
sleeping 0 seconds
sleeping 5 seconds

您可以争辩说还有其他方法可以做到这一点。 便携的方式,也就是不用CHLD或者wait -n:

dualbus@debian:~$ cat portable.sh 
#!/bin/sh

count=0 limit=3
trap 'counter &&  brand; job & ; wait' USR1
unset RANDOM; rseed=123459876$$
brand() 
  [ "$rseed" -eq 0 ] && rseed=123459876
  h=$((rseed / 127773))
  l=$((rseed % 127773))
  rseed=$((16807 * l - 2836 * h))
  RANDOM=$((rseed & 32767))

job() 
  amount=$((RANDOM % 8))
  echo "sleeping $amount seconds"
  sleep "$amount"
  kill -USR1 "$$"

counter() 
  [ "$count" -lt "$limit" ]; ret=$?
  count=$((count+1))
  return "$ret"

counter &&  brand; job & 
wait
dualbus@debian:~$ chmod +x portable.sh 
dualbus@debian:~$ ./portable.sh 
sleeping 2 seconds
sleeping 5 seconds
sleeping 6 seconds

因此,总而言之,set -m 在脚本中不是那么有用,因为 它为脚本带来的唯一有趣的功能是能够 与 SIGCHLD 合作。还有其他方法可以实现相同的目标 更短(wait -n)或更便携(自己发送信号)。

【讨论】:

以上是关于为啥我不能在 bash 脚本中使用作业控制?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 ubuntu 上的 emacs 中运行 shell 命令,同时避免 bash 作业控制错误?

Bash脚本实现批量作业并行化

为啥在 python 脚本完成之前不执行打印作业?

Cron 作业可以使用 Gnome-Open 吗?

结束 mpirun 进程会终止 bash 循环

argparse 处理 bash 命令中的字符串和空格