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

发布于 2024-07-16 19:29:29 字数 769 浏览 7 评论 0原文

这个答案中另一个问题,有人告诉我

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

这是我第一次听到这个,我仔细研究了 bash.info 中关于作业控制的部分(第 7 章),发现没有提到任何这些断言。 的原因说明为什么作业控制对于脚本来说特别不明智。]

[更新: 手册页好一点,提到了“典型”使用、默认设置和终端 I/O,但没有真正 为什么基于脚本的作业控制不起作用,是什么使它成为一种不好的做法(又名“愚蠢”)?

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

正如另一个问题的接受的答案所指出的,存在其他脚本可以解决该特定问题而无需尝试作业控制。 美好的。 而且这个饱受诟病的脚本使用了硬编码的工作编号——显然很糟糕。 但我试图了解工作控制是否是一种从根本上注定要失败的方法。 看起来也许它可以工作......

In this answer to another question, I was told that

in scripts you don't have job control
(and trying to turn it on is stupid)

This is the first time I've heard this, and I've pored over the bash.info section on Job Control (chapter 7), finding no mention of either of these assertions. [Update: The man page is a little better, mentioning 'typical' use, default settings, and terminal I/O, but no real reason why job control is particularly ill-advised for scripts.]

So why doesn't script-based job-control work, and what makes it a bad practice (aka 'stupid')?

Edit: The script in question starts a background process, starts a second background process, then attempts to put the first process back into the foreground so that it has normal terminal I/O (as if run directly), which can then be redirected from outside the script. Can't do that to a background process.

As noted by the accepted answer to the other question, there exist other scripts that solve that particular problem without attempting job control. Fine. And the lambasted script uses a hard-coded job number — Obviously bad. But I'm trying to understand whether job control is a fundamentally doomed approach. It still seems like maybe it could work...

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(8

反话 2024-07-23 19:29:29

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

来自 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.

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

  1. 作业控制主要是为了促进交互式控制(而脚本可以直接与 pid 一起工作),而且
  2. 我还引用了他原来的答案,...依赖于您之前没有在脚本中启动任何其他作业的事实,这是一个错误的假设。 这是非常正确的。

更新

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

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

What he meant is that job control is by default turned off in non-interactive mode (i.e. in a script.)

From the bash man page:

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.

and

   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.

When he said "is stupid" he meant that not only:

  1. is job control meant mostly for facilitating interactive control (whereas a script can work directly with the pid's), but also
  2. I quote his original answer, ... relies on the fact that you didn't start any other jobs previously in the script which is a bad assumption to make. Which is quite correct.

UPDATE

In answer to your comment: yes, nobody will stop you from using job control in your bash script -- there is no hard case for forcefully disabling set -m (i.e. yes, job control from the script will work if you want it to.) Remember that in the end, especially in scripting, there always are more than one way to skin a cat, but some ways are more portable, more reliable, make it simpler to handle error cases, parse the output, etc.

You particular circumstances may or may not warrant a way different from what lhunath (and other users) deem "best practices".

做个少女永远怀春 2024-07-23 19:29:29

使用 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 存储在 $! 变量中,因此您也可以等待 ${!}。 另请注意 nice 命令。

有时这样的代码在 makefile 中很有用:

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

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

请注意,& 是行终止符,如 ;(写为 command& 而不是 command&;)。

希望这可以帮助。

Job control with bg and fg is useful only in interactive shells. But & in conjunction with wait is useful in scripts too.

On multiprocessor systems spawning background jobs can greatly improve the script's performance, e.g. in build scripts where you want to start at least one compiler per CPU, or process images using ImageMagick tools parallely etc.

The following example runs up to 8 parallel gcc's to compile all source files in an array:

#!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

There is nothing "stupid" about this. But you'll require the wait command, which waits for all background jobs before the script continues. The PID of the last background job is stored in the $! variable, so you may also wait ${!}. Note also the nice command.

Sometimes such code is useful in makefiles:

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

This gives much finer control than make -j.

Note that & is a line terminator like ; (write command& not command&;).

Hope this helps.

笑看君怀她人 2024-07-23 19:29:29

仅当您运行交互式 shell 时,即您知道 stdin 和 stdout 连接到终端设备(Linux 上的 /dev/pts/*)时,作业控制才有用。 然后,在前台有一些东西,在背景上有其他东西等等是有意义的。

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

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

Job control is useful only when you are running an interactive shell, i.e., you know that stdin and stdout are connected to a terminal device (/dev/pts/* on Linux). Then, it makes sense to have something on foreground, something else on background, etc.

Scripts, on the other hand, doesn't have such guarantee. Scripts can be made executable, and run without any terminal attached. It doesn't make sense to have foreground or background processes in this case.

You can, however, run other commands non-interactively on the background (appending "&" to the command line) and capture their PIDs with $!. Then you use kill to kill or suspend them (simulating Ctrl-C or Ctrl-Z on the terminal, it the shell was interactive). You can also use wait (instead of fg) to wait for the background process to finish.

长亭外,古道边 2024-07-23 19:29:29

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

每当作业状态发生变化时,shell 都会立即学习。 通常情况下,
bash 等到要打印提示时才报告
作业状态的变化以免中断任何其他输出。 如果
bash 报告,set 内置命令的 -b 选项已启用
立即进行此类更改。 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)要么更便携(自己发送信号)。

It could be useful to turn on job control in a script to set traps on
SIGCHLD. The JOB CONTROL section in the manual says:

The shell learns immediately whenever a job changes state. Normally,
bash waits until it is about to print a prompt before reporting
changes in a job's status so as to not interrupt any other output. If
the -b option to the set builtin command is enabled, bash reports
such changes immediately. Any trap on SIGCHLD is executed for each
child that exits.

(emphasis is mine)

Take the following script, as an example:

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

Note: CHLD trapping seems to be broken as of bash 4.3

In bash 4.3, you could use 'wait -n' to achieve the same thing,
though:

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

You could argue that there are other ways to do this in a more
portable way, that is, without CHLD or 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

So, in conclusion, set -m is not that useful in scripts, since
the only interesting feature it brings to scripts is being able to
work with SIGCHLD. And there are other ways to achieve the same thing
either shorter (wait -n) or more portable (sending signals yourself).

夏至、离别 2024-07-23 19:29:29

正如你所说,Bash 确实支持作业控制。 在编写 shell 脚本时,通常会假设您不能依赖 bash,而是使用普通的 Bourne shell (sh),该 shell 历史上没有作业控制功能。

这些天我很难想象一个系统,在这个系统中你真的只能使用真正的 Bourne shell。 大多数系统的 /bin/sh 将链接到 bash。 不过,这是可能的。 您可以做的一件事是,而不是指定

#!/bin/sh

您可以这样做:

#!/bin/bash

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

Bash does support job control, as you say. In shell script writing, there is often an assumption that you can't rely on the fact that you have bash, but that you have the vanilla Bourne shell (sh), which historically did not have job control.

I'm hard-pressed these days to imagine a system in which you are honestly restricted to the real Bourne shell. Most systems' /bin/sh will be linked to bash. Still, it's possible. One thing you can do is instead of specifying

#!/bin/sh

You can do:

#!/bin/bash

That, and your documentation, would make it clear your script needs bash.

余生共白头 2024-07-23 19:29:29

正如 @ack 在评论 作业控制在脚本中很有用:

a.sh

set -m
bash b.sh &
set +m
sleep 4
kill -- -$!

b.sh

while true; do
    echo -n .
    sleep 2
done
$ bash a.sh
..done

bash b.sh 仍在运行时的进程树:

$ ps -eHo pid,pgid,tty,stat,args
    PID    PGID TT       STAT COMMAND
...
     68      68 pts/1    S      bash
    555     555 pts/1    S+       bash a.sh
    557     555 pts/1    S+         sleep 4
    556     556 pts/1    S          bash b.sh
    559     556 pts/1    S            sleep 2
...

bash b.sh 及其子进程最终位于一个单独的进程组 (556) 中。 它提供了一个简单的方法来杀死它们:kill -- -556

我还实现了 @Jonathan 建议的案例

c.sh

set -m
bash b.sh &
echo do something
fg

@SethP:

d.sh:

set -m
bash e.sh
trap 'kill -SIGINT %' SIGINT
bg
wait

e.sh

echo doing some setup
sleep 2
kill -SIGSTOP $
while true; do
    echo -n .
    sleep 2
done

虽然在这两种情况下我不太确定是否有没有更好的解决方案。

As @ack suggested in a comment job control can be useful in a script:

a.sh

set -m
bash b.sh &
set +m
sleep 4
kill -- -$!

b.sh:

while true; do
    echo -n .
    sleep 2
done
$ bash a.sh
..done

The process tree while bash b.sh is still running:

$ ps -eHo pid,pgid,tty,stat,args
    PID    PGID TT       STAT COMMAND
...
     68      68 pts/1    S      bash
    555     555 pts/1    S+       bash a.sh
    557     555 pts/1    S+         sleep 4
    556     556 pts/1    S          bash b.sh
    559     556 pts/1    S            sleep 2
...

bash b.sh and its children end up in a separate process group (556). Which provides a simple way to kill them: kill -- -556.

Also I implemented cases suggested by @Jonathan:

c.sh:

set -m
bash b.sh &
echo do something
fg

and @SethP:

d.sh:

set -m
bash e.sh
trap 'kill -SIGINT %' SIGINT
bg
wait

e.sh:

echo doing some setup
sleep 2
kill -SIGSTOP $
while true; do
    echo -n .
    sleep 2
done

Although in these 2 cases I'm less sure if there are no better solutions.

笑叹一世浮沉 2024-07-23 19:29:29

可能是o/t,但当我在一个长时间运行的作业上通过ssh进入服务器时,我经常使用nohup,这样如果我注销,作业仍然可以完成。

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

如果我想暂停当前正在运行的 sudo,我也

kill -STOP pid

经常这样做,例如:

kill -STOP $

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

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

kill - 9 pid # note the space

,在过去,有时您可能会关闭机器,因为它会杀死 init!

Possibly o/t but I quite often use nohup when ssh into a server on a long-running job so that if I get logged out the job still completes.

I wonder if people are confusing stopping and starting from a master interactive shell and spawning background processes? The wait command allows you to spawn a lot of things and then wait for them all to complete, and like I said I use nohup all the time. It's more complex than this and very underused - sh supports this mode too. Have a look at the manual.

You've also got

kill -STOP pid

I quite often do that if I want to suspend the currently running sudo, as in:

kill -STOP $

But woe betide you if you've jumped out to the shell from an editor - it will all just sit there.

I tend to use mnemonic -KILL etc. because there's a danger of typing

kill - 9 pid # note the space

and in the old days you could sometimes bring the machine down because it would kill init!

海拔太高太耀眼 2024-07-23 19:29:29

jobs 确实可以在 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

jobs DO work in bash scripts

BUT, you ... NEED to watch for the spawned staff
like:

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

jobs will have different context on each side of the |

bypassing this may be using for instead of while:

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

this should demonstrate how to use jobs in a script ...
with the mention that my commented note is ... REAL (dunno why that behaviour)

    #!/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
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文