在后台运行多个命令
1. 概述
当我们使用 Linux 命令行工作时,我们通常会在前台运行 Linux 命令。但是,在某些情况下,我们需要在后台运行多个命令。
在本教程中,我们将了解如何使用两种方法将多个命令作为后台作业运行:
- 将多个命令作为单个后台作业运行
- 在多个后台作业中运行多个命令
在本教程中,我们将重点关注 Bash shell。
2. 将多个命令作为一个作业运行
通常,当命令彼此相关时,我们希望将多个命令作为一个作业运行。
我们可以通过三个步骤将多个命令作为一个作业启动:
- 组合命令——我们可以使用“ ; “ , “ && “ , 或“ || “ 根据条件逻辑的要求连接我们的命令,例如:cmd1;命令2 && 命令3 || 命令4
- 对命令进行分组——我们可以按“ {} ”或“ () ”对组合命令进行分组,例如:( cmd1; cmd2 && cmd3 || cmd4 )
- 将 命令组作为作业发送到后台——如果我们在命令组后添加&运算符 ,shell 会将命令组作为后台作业运行,例如:( cmd1; cmd2 && cmd3 || cmd4 ) &
接下来,让我们看一个如何将多个命令作为单个后台作业执行的示例。
比方说,我们想对一个昂贵的计算中的数字进行奇偶校验并输出结果。由于昂贵的计算可能需要一些时间,我们希望将命令作为后台作业运行。所以我们可以应用上面的三个步骤来构建命令:
$ ( echo "Calculating the number..."; sleep 8; \
NUM=$RANDOM; [ $((NUM%2)) -eq 0 ] && echo "$NUM: even" || echo "$NUM: odd" ) &
在上面的命令中,我们使用sleep命令来模拟数字计算过程。现在,让我们通过一个小演示看看它是如何作为一个作业运行的:
如演示所示,我们启动了三次命令。每次我们启动命令时,命令组都作为后台作业运行。我们可以使用jobs 命令监视作业的状态。
3. 控制输出
我们已经学习了如何将多个命令作为单个后台作业启动。如果我们查看演示,我们会发现作业的所有输出都打印到我们的终端。这是因为&运算符启动的后台进程会从 shell 继承 stdout 和 stderr。
在实践中,我们经常希望将作业的输出重定向到一个文件,这样作业的输出就不会弄乱当前的终端。此外,我们可以有效地检查作业的结果或作业执行的日志。为此,我们可以将命令组的输出重定向到一个文件:
$ ( echo "Calculating the number..."; sleep 8; \
NUM=$RANDOM; [ $((NUM%2)) -eq 0 ] && echo "$NUM: even" || echo "$NUM: odd" ) > result.txt &
[1] 40030
$
[1]+ Done ( echo "Calculating the number..."; sleep 8; ... ) > result.txt
$ cat result.txt
Calculating the number...
30027: odd
因此,作业的输出转到文件*result.txt。*我们只能在当前终端看到作业的 PID 和完成通知。
作业的 PID 是必不可少的信息。通常,我们不想压制它。但是,如果我们不想在启动命令后看到任何输出,我们可以隐藏它:
$ { ( echo "Calculating the number..."; sleep 8; NUM=$RANDOM; \
[ $((NUM%2)) -eq 0 ] && echo "$NUM: even" || echo "$NUM: odd" ) > result.txt & } 2>/dev/null
$
$ jobs
[1]+ Running ( echo "Calculating the number..."; sleep 8; ... ) > result.txt &
$
[1]+ Done ( echo "Calculating the number..."; sleep 8; ... ) > result.txt
$ cat result.txt
Calculating the number...
7667: odd
如上面的输出所示,我们将 stderr 重定向到/dev/null以抑制“ & ”运算符的输出,因为它将 PID 信息写入 stderr。**
如果我们仔细查看上面的输出,我们可能会看到作业的完成通知仍在打印。作业的完成通知是 shell 的一个特性。它不能通过 IO 重定向来控制。
如果我们也想隐藏完成通知,我们必须通过将“ {} ”组更改为“ () ” 来在子 shell 中启动作业。然而,副作用是我们失去了对工作的控制。因此,不推荐这样做:
$ ( ( echo "Calculating the number..."; sleep 8; NUM=$RANDOM; \
[ $((NUM%2)) -eq 0 ] && echo "$NUM: even" || echo "$NUM: odd" ) > result.txt & )
$ jobs
如上面的命令所示,如果我们将作业包装在“ ( ) ”组中,则不需要重定向标准错误。这是因为作业没有在当前 shell 中启动,它也没有使用当前 shell 的 stderr。
此外,我们可以看到jobs命令没有输出任何内容。也就是说,当前 shell 并不知道这个作业。因此,我们无法监视或控制工作。我们也不知道什么时候检查我们的result.txt是合适的,因为我们不知道工作是否已经完成。
4. 将多个命令作为多个作业运行
我们还可以将多个命令作为不同的作业运行,让它们并行运行。
为此,我们将“ & ”运算符添加到我们要发送到后台的命令或命令组中,例如:
cmd1 & cmd2 & (cmd3; cmd4) &
在上面的示例中,我们将启动三个作业,它们并行运行:
- 作业 1:cmd1
- 作业 2:cmd2
- 作业 3:(cmd3;cmd4)
一个具体的例子可以帮助我们快速理解:
$ date & (sleep 5; echo "cmd2 done") & (sleep 3; echo "cmd3 done") &
[1] 41012
[2] 41013
[3] 41014
Sun Sep 13 01:31:57 PM CEST 2020
$ jobs
[1] Done date
[2]- Running ( sleep 5; echo "cmd2 done" ) &
[3]+ Running ( sleep 3; echo "cmd3 done" ) &
$ cmd3 done
cmd2 done
[2]- Done ( sleep 5; echo "cmd2 done" )
[3]+ Done ( sleep 3; echo "cmd3 done" )
如上面的输出所示,我们启动了三个后台作业,它们并行运行。
5.等待后台作业完成
到目前为止,我们已经了解了如何将命令作为后台作业启动。此外,我们可以使用jobs命令获取作业的状态报告。 有时,特别是当我们编写 shell 脚本时,我们会启动一些耗时的命令作为后台作业,并希望根据作业的结果进行一些进一步的计算。
换句话说,我们需要等待这些作业完成,然后再执行其他命令。
在这种情况下, jobs命令没有多大帮助,因为如果我们在我们的脚本中不断询问作业状态并解析输出以决定是否所有需要的作业都已完成,它就没有效率。
现在,是时候介绍wait 命令了。
** wait是一个 shell 内置命令。我们可以将 PID 传递给wait命令并要求它等待这些进程完成。**
让我们 通过一个例子来理解wait命令:
$ cat await-jobs.sh
#!/bin/bash
JOB1_RESULT="/tmp/job1.result"
JOB2_RESULT="/tmp/job2.result"
rm -f $JOB1_RESULT $JOB2_RESULT
(sleep 5; echo $RANDOM > "$JOB1_RESULT") &
PID_JOB1=$!
echo "job1 started with PID $PID_JOB1"
(sleep 3; echo $RANDOM > "$JOB2_RESULT") &
PID_JOB2=$!
echo "job2 started with PID $PID_JOB2"
echo "Await two jobs' completion..."
wait $PID_JOB1 $PID_JOB2
awk 'NR==FNR{ r1=$1;printf "job1 result: %d\n", r1; next }
{ r2=$1;printf "job2 result: %d\n", r2 }
END{printf "The Sum: %d\n", r1+r2}' $JOB1_RESULT $JOB2_RESULT
上面的await-jobs.sh脚本启动了两个后台作业。每个作业生成一个随机数并将其写入文件。
为了模拟将运行一段时间的作业,我们使用sleep命令休眠了几秒钟。
值得一提的是shell特殊变量$! 会给我们最后一个进程的 PID。在这里,我们可以使用此变量获取后台作业的 PID。
接下来,我们使用wait命令等待两个作业的完成。
两个作业完成后,已经将结果写入了结果文件。我们使用 awk 命令读取结果文件并打印每个结果及其总和。
让我们通过一个演示看看脚本是如何工作的: