> 同时运行多个任务在当前的操作系统下已经十分容易了,然而你知道如何对作业进行有效地管理吗?
为了对作业进行有效的管理,首先得知道作业是什么。
在Linux下,内核管理着进程,进程可以理解为一个程序及其在运行时产生或依赖的数据在处理机上的产生的活动。而作业,则是由一个或多个进程(使用管道线)构成的准备完成某项任务的处理序列。
在任何时候,可以使用`ps`(process status)命令显示进程列表,使用`jobs`命令显示作业列表。
----------
# 前台进程与后台进程
为了更好的使用作业管理,首先要明白的是,进程有前后台之分。
当shell在提示让用户输入一条命令之前等待当前程序结束时,我们称这样的进程为**前台进程**。
当shell启动一个程序,但是有让该程序自己运行时,我们称这样的进程为**后台进程**。
大多数Unix程序都从*sdtin*读取输入,向*stdout*写入输出,错误信息则写入到*stderr*。当从shell提示行运行程序时,*stdin*与键盘相连,*stdout*和*stderr*与显示器相连。如果希望改变这种方式,可以进行重定向。
当前台运行进程时,从键盘读取数据,将结果写入到显示器没有问题。但是,当后台运行程序时,进程自己运行,用户可以继续输入另一条命令。那么,如果后台进程试图从*stdin*读取或向*stdout*写入时,会发生什么情况呢?答案就是输入无法连接上,输出连接不会发生变化。
这有两方面的重要意义:
1. 如果在后台运行的进程试图从*stdin*读取输入时,*stdin*没有任何内容,改进程将无限期地等待输入,直到使用`fg`命令将进程移至前台。
2. 如果后台运行的进程向*stdout*或*stderr*写入数据,输出将显示在显示器上。但是,由于您可能正在处理其他事情,因此输出将与正在处理的其他事情混杂在一起,从而时结果陷入混乱。
----------
# 作业控制
对于作业控制,默认情况下,除非进程结束,否则用户无法再输入其他命令。如果有必要在进程自己结束之前终止进程,可以按`^C`键发送**intr**信号,或者按`^\`键发送**quit**信号。
另外,用户还可以在命令末尾键入一个**&**字符以**异步进程**(asynchronous process)运行程序。异步进程(即后台进程)有两个明确的特征:
1. 默认情况下,标准输入与空文件**/dev/null**相连
2. 由于进程自己运行,不需要用户输入,所以该进程不响应**intr**和**quit**信号,只能使用`kill`命令发送信号
作业控制的本质特性就是将每条输入的命令视为一个**作业**,该作业由一个唯一的**作业号**(job number | job ID)来标识。为了控制和管理作业,可以将作业ID和一系列命令、变量、终端设置、shell变量以及shell选项一起使用,出于参考的目的,以下列出这些工具:
----------
## 作业控制命令
|命令|含义|
|---|
|jobs|显示作业列表|
|ps|显示进程列表|
|fg|将作业移至前台|
|bg|将作业移至后台|
|suspend|挂起当前shell|
|^Z|挂起当前前台作业|
|kill|向作业发送信号; 默认情况下,终止作业|
## 变量
|变量|含义|
|---|
|echo \$\$|显示当前shell的PID|
|echo $!|显示上一条移至后台的命令的PID|
## 终端设置
|设置|含义|
|---|
|stty tostop|挂起试图向终端写数据的后台作业|
|stty -tostop|关闭tostop|
## shell选项
|选项|含义|
|---|
|set -o monitor|运行作业控制|
|set +o monitor|关闭monitor|
|set -o notify|当后台作业结束时立即通报|
|set +o notify|关闭notify|
----------
# 在后台运行作业
为了在后台运行作业,需要在命令末键入一个**&**字符。例如,下述命令在后台运行`ls`命令,并且将输出重定向到文件**temp**中:
```sh
$ ls > temp &
```
每次在后台运行作业时,shell都会显示作业的作业号和进程ID。shell从1开始,自己为作业分配作业号。例如,执行了上述命令后,shell的显示将如下所示:
```sh
$ [1] 4074
```
这意味着作业号#1已经被启动,其进程ID为4074。如果一个作业由多个程序构成的管道线,那么看到的进程ID时管道线中最后一个程序的进程ID。例如,假设您输入了:
```sh
$ who | cut -c 1-8 | sort | uniq -c &
[2] 4353
```
这说明了您已经启动了作业#2,最后一个程序(uniq)的进程ID是4353。
因为后台作业自己运行,所以没有什么简单的方法来了解他们的进展。基于这一原因,每当后台作业结束时,shell都会发送一个短的状态消息。例如,当第一个例子中的作业结束时,shell将提示一个类似于下面的消息:
```sh
[1] Done ls > temp
```
该消息通知您作业#1已经结束。
如果您正在等待一个特定的后台作业结束,那么这样的通知非常重要。但是,当您正在做某些事情时(例如,编辑文件或着阅读manual),如果shell随意地显示这样的状态消息,这将会有点烦恼。基于这一原因,当后台作业结束时,shell不会立即通知您,他会一直等待直到要显示下一个shell提示。
如果不希望等待,则可以设置**notify**选项来关闭提示:
```sh
# 关闭notify
$ set +o notify
# 显示notify
$ set -o notify
```
----------
# 挂起作业
在任何时候,每个作业都处于3中状态的一种:
- 前台运行
- 后台运行
- 暂停,等待信号恢复执行
当需要暂停前台作业时,可以按`^Z`,发送**susp**信号,从而使进程暂停。当通过这种方式暂停进程时,我们将称进程**挂起**(suspend),或者将进程**停止**(stop)。
实际上,一个被**挂起**或被**停止**的作业还可以重新启动,其只是被暂停罢了。若想永久地终止进程,必须按`^C`或使用`kill`命令。
当停止程序时,shell就会中断程序,并显示一个新的shell提示,从而可以继续输入新的命令。当希望恢复挂起的程序时,可以使用`fg`命令将该程序移回前台。
通过使用`^Z`和`fg`,可以挂起程序,输入一些命令,然后再在希望时返回原来的程序。
当挂起作业时,进程会无限期暂停。如果试图注销系统,则所有挂起的作业将被自动终止。
----------
# 挂起shell: suspend
按下`^Z`键将挂起在前台运行的任何作业。但有一个进程不会挂起,这个进程就是当前的shell。如果希望暂停当前的shell,则需要使用`suspend`命令。该命令的语法为:
```sh
$ suspend [-f]
```
为什么希望挂起shell呢?例如,假设您以普通用户标识登录,然后希望做一些要求超级用户身份的工作,因此使用`su`命令切换成**root**。尔后又需要以普通用户身份做一些工作,那么此时如果停止超级用户的shell,稍后再重新启动超级用户的shell并不是理想的方法,因为这样就会失去当前的工作目录、变量的改变等。作为替代,可以输入:
```sh
# suspend
$
```
这将暂定当前作为超级用户的shell,并返回到普通用户的shell。当准备好再次返回到超级用户,继续完成工作时,可以使用`fg`命令将超级用户的shell移回前台。
挂起shell的唯一限制是,默认情况下不允许挂起登录shell。这样可以防止在停止主shell的情况下陷入危险的边缘。但是,在特定的环境下,有时页可能希望暂停登录shell。例如,当通过`su -`而不是`su`启动超级用户shell时,将创建一个登录shell。如果希望挂起这个新shell,则必须使用`-f`(force)选项:
```sh
# suspend -f
```
这将告诉`suspend`暂停当前的shell,而不管该shell是否为登录shell。
# 显示作业列表:jobs
在任何时候,可以使用`jobs`命令显示所有作业的列表。该命令的语法为:
```sh
$ jobs [-l]
```
在大多数情况下,不需要使用选项`-l`,这将只显示作业ID、作业状态及命令名。
```sh
$ jobs
[1] Stopped vim document
[2]- Stopped less /etc/passwd
[3]+ Stopped man cal
```
如果还希望查看作业的进程ID,则可以使用`-l`(long listing)选项:
```sh
$ jobs -l
[1] 4595 Stopped vim document
[2]- 4613 Stopped less /etc/passwd
[3]+ 4627 Stopped man cal
```
注意,在两个列表中,各有一个作业标记**+**(plus)字符,表示该作业为当前作业;还有一个作业标记**-**(minus),表示为前一个作业。
如上述中,当前作业为作业ID为3的作业,前一个作业为作业ID为2的作业。
# 将作业移至前台:fg
当需要将作业移至前台时,可以使用`fg`命令。该命令的语法有3种变体:
```sh
$ fg
$ fg %[job]
$ %[job]
```
其中*job*表示一个特定的作业。
- 当不使用参数时,即只是输入`fg`命令本身,那么shell将启动当前作业
- 为了启动一个不是当前作业的作业,必须明确标识这个作业,实现方法如下:
|job|含义|
|---|
|%%|当前作业|
|%+|当前作业|
|%-|前一个作业|
|%n|作业#n|
|%name|命令开头含有name的作业|
|%?name|命令中任意位置含有name的作业|
例如,为了将作业#1移至前台,可以使用一个**%**字符,后面跟着作业号,如:
```sh
$ fg %1
```
如果命令的开头部分足够使其能够从所有作业中区分出来,那么可以指定其开头,如指定为**v**将启动作业#1 **vim document**:
```sh
$ fg %v
```
也可以使用**%?**,后面跟部分命令名,如将**man cal**移至前台:
```sh
$ fg %?cal
```
为了方便起见,一些shell可以只是输入**%**字符开头的命令行,就当作是使用`fg`命令。例如,为了恢复作业#1 **vim document**,可以是如下命令:
```sh
$ %1
```
----------
> 提示
为了在两个作业之间快速地切换,可以使用:
`fg %-`和`^Z`
一旦习惯了这一命令,您就会大量地使用这条命令。
----------
# 将作业移至后台:bg
为了将作业移至后台,需要使用`bg`命令。该命令的语法为:
```sh
$ bg [%job...]
```
其中*job*标识一个特定的作业。
在指定作业时,需要遵循和`fg`命令相同的规则,例如,为了将2号作业移至后台,可以使用:
```sh
$ bg %2
```
如果喜欢,还可以同时将多个作业移至后台:
```sh
$ bg %2 %4 %8
```
在一种重要情形下,`bg`命令会提供极大的便利。假设您输入了一条似乎要运行好处时间的命令,如果这个程序不是交互式的,您可以先挂起该程序,再将其移至后台运行,此时终端可以继续使用,作业也得以继续工作。
----------
> 提示
当准备在后台运行程序,但是在输入命令的过程中忘了键入**&**字符时,`bg`命令将特别有用。如果没有键入**&**字符,那么作业将在前台运行。
这种情况下,只需按下`^Z`,然后使用`bg`命令将作业移至后台即可。
----------
# Reference
[1] Haeley Hahn. Unix & Linux 大学教程[M]. 张杰良, 译. 北京:清华大学出版社, 2010.1:700-714.