Contents

Linux 中作业控制的概念

1. 概述

当我们使用 Linux 命令行时,有时我们希望启动一个进程并让它作为作业在后台运行。 这样,它不会阻塞终端,我们可以在它运行的同时做一些其他的工作。

有几种方法可以实现这一点。在本教程中,我们将讨论三种常用方法:*disown nohup 命令,以及&*运算符。

2. 简而言之 Linux 作业控制

让我们对Linux作业控制有一个基本的了解。

2.1. 示例长时间运行的进程

首先,让我们创建一个长时间运行的 shell 脚本作为示例进程:

$ cat long_running.sh
#!/bin/bash
while true; do
    sleep 3
    date +"The Process [$$] says: The current date and time is %F %T"
done

脚本非常简单。它每三秒打印一次带有当前时间戳的进程 ID。**特殊变量 $$保存当前进程的进程 ID。

一旦脚本启动,它不会被终止,直到我们手动停止它,例如按下Ctrl-C

2.2. 简而言之,作业控制

我们先来看一些作业控制命令和键盘快捷键:

  • Ctrl-C : 通过发送信号**SIGINT杀死在前台运行的进程
  • Ctrl-Z : 通过发送信号**SIGTSTP暂停在前台运行的进程
  • job :显示作业列表及其状态
  • fg :将后台作业移动到前台
  • bg :通过将暂停的作业作为后台作业运行来恢复它们

为了更好地理解上面的命令和键盘快捷键,我们将通过一个示例来了解它们是如何使用的。

首先,让我们启动示例过程并将输出重定向到一个名为*/tmp/out*的文件:

$ ./long_running.sh > /tmp/out

好的!我们的 shell 脚本在前台运行。由于脚本永久运行,它会阻止终端。 现在,让我们尝试通过按Ctrl-Z来暂停它:

$ ./long_running.sh > /tmp/out
^Z
[1]  + 61299 suspended  ./long_running.sh > /tmp/out
$

输出显示PID61299的作业已暂停,我们再次看到命令提示符,以便我们可以根据需要输入其他命令。

让我们使用jobs命令查看它是否在作业列表中:

$ jobs
[1] + suspended ./long_running.sh > /tmp/out

现在,我们将等待大约半分钟,然后使用 bg命令恢复它:

$ bg
[1]  + 61299 continued  ./long_running.sh > /tmp/out
$

伟大的!暂停作业恢复,并在后台运行。如果我们现在查看工作列表:

$ jobs
[1]  + running    ./long_running.sh > /tmp/out

让我们用fg命令把它带回前台 :

$ fg
[1]  + 61299 running    ./long_running.sh > /tmp/out

它再次在前台运行,并继续阻塞终端。我们可以按Ctrl-C来终止它。

终止作业后,让我们检查输出文件*/tmp/out*:

The Process [61299] says: The current date and time is 2020-05-29 15:50:01
The Process [61299] says: The current date and time is 2020-05-29 15:50:04
The Process [61299] says: The current date and time is 2020-05-29 15:50:07
The Process [61299] says: The current date and time is 2020-05-29 15:50:10
The Process [61299] says: The current date and time is 2020-05-29 15:51:00
The Process [61299] says: The current date and time is 2020-05-29 15:51:03
The Process [61299] says: The current date and time is 2020-05-29 15:51:06
...

如果我们仔细阅读输出文件,我们可以看到第 4 行和第 5 行之间的间隔是 50 秒。在此期间,我们没有任何输出。这是因为我们按Ctrl-Z暂停了作业。

3. *&*运算符

3.1. *&*运算符简介

如果命令以&运算符结尾,则 shell 将在子 shell 的后台运行该命令。shell 不等待它完成,并返回状态 0。 让我们尝试一下long_running.sh 脚本:

$ ./long_running.sh > /tmp/out &
[1] 176933
$ jobs
[1]  + running    ./long_running.sh > /tmp/out
$ fg
[1]  + 176933 running    ./long_running.sh > /tmp/out
^C
$ cat /tmp/out
The Process [176933] says: The current date and time is 2020-05-29 21:28:59
The Process [176933] says: The current date and time is 2020-05-29 21:29:02
The Process [176933] says: The current date and time is 2020-05-29 21:29:05
The Process [176933] says: The current date and time is 2020-05-29 21:29:08
The Process [176933] says: The current date and time is 2020-05-29 21:29:11
The Process [176933] says: The current date and time is 2020-05-29 21:29:14
The Process [176933] says: The current date and time is 2020-05-29 21:29:17

该示例显示,如果我们使用*&*运算符启动一个进程,该进程将作为后台作业运行。我们可以在前台并行做一些其他的工作。

3.2. 输出

刚才,当我们启动long_running.sh脚本时,我们将标准输出重定向到文件 /tmp/out

如果我们不重定向它的输出会发生什么?让我们尝试一下:

/uploads/job_control_disown_nohup/1.gif

在演示中,我们看到即使作业在后台运行,输出也会打印到终端。这是因为*&运算符启动的后台进程将从 shell继承stdoutstderr* 。

3.3. SIGHUP信号

我们可以使用 *&*运算符启动一个进程,使其在后台运行。我们可能会问,如果我们关闭终端,它还会运行吗?让我们来了解一下。

首先,我们在后台启动脚本:

$ ./long_running.sh > /tmp/out &
[1] 184314
$ jobs
[1]  + running    ./long_running.sh > /tmp/out
$ ps -ef | grep '[l]ong'
kent      184314   55850  0 22:01 pts/1    00:00:00 /bin/bash ./long_running.sh

输出显示进程正在使用 PID 184314运行。现在我们关闭当前终端并启动一个新终端,以检查作业是否仍在运行:

$ jobs
$ ps -ef | grep '[l]ong'
$

哎呀!作业列表是空的,进程也消失了。为什么会这样?我们后台作业进程的进程树可能有助于回答这个问题。

让我们使用*pstree *命令检查后台作业进程的进程树:

$ ./long_running.sh > /tmp/out &
[1] 191536
$ pstree -s 191536
systemd───xfsettingsd───urxvt───bash───long_running.sh───sleep

输出清楚地表明我们的后台作业进程是shell的一个子进程,在本例中是bash。此外,终端进程,在本例中为urxvt,是 shell 进程的父进程。

**我们知道SIGHUP用于表示发生了终端挂断。**让我们了解当我们终止终端进程时会发生什么:

  1. 我们关闭终端。终端向其子进程发送SIGHUP信号。
  2. shell 进程接收到SIGHUP信号。之后,它向其子进程发送一个SIGHUP信号。
  3. 作为 shell 的一个子进程,我们的后台作业进程接收到一个SIGHUP信号。

*如果进程收到*SIGHUP,默认操作是立即停止执行。这样,后台运行的作业进程就终止了。

不难想象,如果我们想在附加终端关闭后保持后台作业处于活动状态,我们应该以某种方式阻止作业进程接收SIGHUP信号。

4. disown命令

4.1. disown命令介绍

disown命令是许多现代 shell(例如 Bash 和 Zsh)中的 内置命令。

来自不同 shell的disown命令的行为可能略有不同。在本教程中,我们将讨论 Bash 内置的disown 命令。

使用此命令的基本语法是:

disown [options] jobID1 jobID2 ... jobIDN

如果我们不给它任何选项,  disown命令将从活动作业表中删除给定的作业。

jobID以 %字符开头。例如,%x 标识 作业 x

让我们看一个该命令的默认用法示例:

$ ./long_running.sh > /tmp/out &
[1] 414860
$ jobs -l
[1]  + 414860 running    ./long_running.sh > /tmp/out
$ disown %1
$ jobs -l
$

在我们执行 disown命令后,该作业将从作业列表中删除。但是,作业的进程仍在运行:

$ ps -ef | grep '[l]ong'
kent    414860  185196  0 21:31 pts/0    00:00:00 /bin/bash ./long_running.sh

disown命令具有三个选项:

  • -a :如果没有提供jobID,则删除所有作业
  • -r:仅删除状态为running 的作业
  • -h:不从表中删除每个作业。相反,它被标记为如果 shell 收到SIGHUP则不会将 SIGHUP 发送到作业

** -h选项很重要允许我们在退出终端或断开与远程服务器的连接后保持后台作业处于活动状态**。

我们将在后面的部分详细讨论这个选项。

4.2. 输出

与使用*&运算符启动的作业相同, 被取消的作业的输入和输出不会改变。它重用 shell 中的stdoutstderr*。

4.3. SIGHUP和*-h*选项

我们已经了解到disown命令具有三个选项:-a-r和*-h。**-h*选项是一个特殊选项。 

如果我们执行 disown -h JobID,该作业不会从作业列表中删除。相反,它标志着工作。当 shell 发送SIGHUP信号时,该信号不会发送到标记的作业。因此,即使控制终端关闭,标记的后台作业也将继续运行。

让我们通过一个例子来理解这一点:

$ ./long_running.sh > /tmp/out &
[1] 546754
$ jobs -l
[1]+ 546754 Running                 ./long_running.sh > /tmp/out &
$ disown -h %1
$ jobs -l
[1]+ 546754 Running                 ./long_running.sh > /tmp/out &

如输出所示,在我们执行* disown -h 后,作业%1仍在作业表中。让我们看一下作业进程546754*的进程树:

$ pstree -s 546754
systemd───xfsettingsd───urxvt───bash───long_running.sh───sleep
$ ps -ef | grep '[l]ong'
kent      546754       1  0 13:53 pts/0    00:00:00 /bin/bash ./long_running.sh

pstree命令的输出显示后台作业进程是终端urxvt的子进程,而 ps命令的输出显示该进程正在终端 pts/0中运行。

现在,让我们关闭终端:

$ exit

然后我们打开另一个终端,检查进程546754是否还活着:

$ ps -ef | grep '[l]ong'
kent      546754       1  0 13:53 ?        00:00:00 /bin/bash ./long_running.sh
$ pstree -s 546754
systemd───long_running.sh───sleep

ps命令告诉我们long_running.sh 进程仍在运行。然而,如果我们仔细阅读它的输出,一个问号“ ” 显示在TTY列中。这意味着该进程没有附加终端。

pstree命令的输出 验证了这一点*——long_running.sh进程现在是系统*进程的直接子进程。

我们知道后台作业将重用 shell 中的stdoutstderr

值得一提的是,如果我们执行disown -h %X,那么在控制终端关闭后,由作业 X 写入stdoutstderr的输出将被丢弃。这是因为附加到终端的 shell 进程消失了。

5. nohup 命令

nohup实用程序是GNU Coreutils 软件包的 成员。它的名字代表“no hup”——也就是说,它可以保护命令免受SIGHUP信号的影响。

5.1. nohup命令介绍

在前面的部分中,我们了解到disown -h可以保护后台作业免受SIGHUP的影响。nohup命令非常相似。 但是,它不限于后台作业。我们可以在常规命令上使用nohup实用程序:

$ nohup ./long_running.sh
nohup: ignoring input and appending output to 'nohup.out'

因为我们没有在命令行末尾添加*&*操作符,所以脚本会在前台运行。

我们看一下long_running.sh进程的进程层次结构:

$ ps -ef | grep '[l]ong'
kent      566207  565675  0 18:46 pts/0    00:00:00 /bin/bash ./long_running.sh
$ pstree -s 566207
systemd───xfsettingsd───urxvt───bash───long_running.sh───sleep

ps命令的输出 显示进程 ID,它还告诉我们该进程已附加到终端pts/0,而pstree输出告诉我们该进程已附加到终端进程urxvt

接下来,我们将关闭终端并再次运行 pspstree命令。让我们看看他们报告了什么:

$ ps -ef | grep '[l]ong'
kent      566207       1  0 18:59 ?        00:00:00 /bin/bash ./long_running.sh
$ pstree -s 566207
systemd───long_running.sh───sleep

关闭终端后,ps命令显示 long_running.sh进程仍在运行,但未连接到终端。然后,pstree命令的输出显示它是systemd进程的直接子进程。

在上面的例子中,我们让nohup在前台启动一个进程。如果愿意,我们可以使用nohup实用程序启动一个进程并让它作为后台作业运行。我们只是在末尾添加*&*运算符:

$ nohup ./long_running.sh &
[1] 571215
nohup: ignoring input and appending output to 'nohup.out'
$ jobs -l
[1]  + 571215 running    nohup ./long_running.sh

如果我们关闭终端,long_running.sh进程将仍在运行

5.2. 输出

nohup命令启动的进程不会继承 shell 的stdoutstderr。** nohup命令将忽略标准输入并将stdoutstderr重定向到一个名为nohup.out的文件。**实际上,我们已经在前面的 nohup示例中看到了该消息:

nohup: ignoring input and appending output to 'nohup.out'

6. 比较作业控制方法

到目前为止,我们已经讨论了三种启动后台作业进程的方法以及它们如何处理SIGHUP信号。 让我们总结一下我们讨论的策略:

功能 stdout/stderr 关闭终端后(SIGHUP
& 将命令作为后台作业运行 从shell继承 作业将停止
disown - 删除作业列表中的作业– 保护工作免受SIGHUP – 从shell继承– 如果使用*-h*选项并且控制终端已关闭,则丢弃 只有使用*-h*选项时,作业进程才会继续运行
nohup 保护来自SIGHUP的命令 重定向到nohup.out文件 命令进程将继续运行