Bash学习 - 第7章:Job Control

本文为 Bash Reference Manual第7章:Job Control 的读书笔记。

本章讨论什么是作业控制,它是如何工作的,以及 Bash 如何让你访问其功能。

7.1 Job Control Basics

作业控制是指有能力选择性地**停止(挂起)进程的执行,并在稍后继续(恢复)**其执行。用户通常通过操作系统内核的终端驱动程序和 Bash 共同提供的交互式界面来使用这一功能。

Shell 会为每个管道关联一个作业。它会保持一个当前正在执行作业的表,jobs 命令会显示该表。每个作业都有一个作业编号 ,jobs 会将其显示在括号中。作业编号从 1 开始。当 Bash 异步启动一个作业时,它会打印一行看起来像这样的内容:

bash 复制代码
[1] 25647

这表明此作业的作业编号为 1,并且与此作业相关的管道中最后一个进程的进程 ID 是 25647。单个管道中的所有进程都是同一作业的成员。Bash 使用作业抽象作为作业控制的基础。

为了便于将用户界面与作业控制实现,每个进程都有一个进程组ID,操作系统维护当前终端进程组ID的概念。该终端进程组ID与控制终端相关联。

具有相同进程组ID的进程被称为属于同一进程组。前台进程组的成员(进程组ID 等于 当前终端进程组ID的进程)会接收到键盘生成的信号,例如SIGINT。前台进程组中的进程称为前台进程。后台进程是那些进程组ID与控制终端 不同 的进程;这些进程对键盘生成的信号免疫。只有前台进程被允许从控制终端读取数据,或者如果用户通过stty设置了tostop,也可以向其写入数据。系统会向试图从终端读取(在tostop生效时写入)的后台进程发送SIGTTIN(SIGTTOU)信号,除非该信号被捕获,否则会挂起该进程。

bash 复制代码
## 后台进程的进程组ID(pgrp) 和 当前终端进程组ID(tpgid) 不同。
$ sleep 1000 &
[3] 15064
$ ps -p 15064 -o pgrp,tpgid
   PGRP   TPGID
  15064   15065

## 前台进程的进程组ID(pgrp) 和 当前终端进程组ID(tpgid) 相同。
$ sleep 9999
## 从其他终端查看
$ ps -ef|grep sleep|grep -v grep
vagrant    15120   14928  0 03:31 pts/0    00:00:00 sleep 9999

$ ps -o pgrp,tpgid -p 15120
   PGRP   TPGID
  15120   15120

如果 Bash 所运行的操作系统支持作业控制,Bash 内部就包含了使用它的功能。在进程运行时输入挂起字符(通常是 ^ZControl-Z )会停止该进程并将控制权返回给 Bash。输入延迟挂起字符(通常是 ^YControl-Y )会在进程尝试从终端读取输入时停止该进程,并将控制权返回给 Bash。然后用户可以操作该作业的状态,使用 bg 命令在后台继续运行它,使用 fg 命令在前台继续运行它,或者使用 kill 命令终止它。挂起字符会立即生效,并且会附带丢弃任何待处理输出和预输入的附加效果。如果你想强制停止后台进程,或者停止与终端会话无关的进程,可以使用 kill 向其发送 SIGSTOP 信号。

在 shell 中有多种方式可以引用一个作业。%字符用于引入作业规格(jobspec)。

作业号 n 可以称为 %n。作业也可以通过启动它时使用的名称前缀,或通过出现在其命令行中的子字符串来引用。例如,%ce 指的是命令名称以 'ce' 开头的作业。另一方面,使用 %?ce 指的是命令行中包含字符串 'ce' 的任何作业。如果前缀或子字符串匹配多个作业,Bash 会报告错误。

符号%%%+表示 shell 中的当前作业。单独的%(没有伴随的作业指定)也表示当前作业。%-表示前一个作业。当作业在后台启动、作业在前台停止或作业在后台恢复时,它会成为当前作业。原来的当前作业则变为前一个作业。当前作业终止时,前一个作业会成为当前作业。如果只有一个作业,%%-都可以用来指该作业。在与作业相关的输出中(例如 jobs 命令的输出),当前作业总是标记为+,前一个作业标记为-

bash 复制代码
$ jobs
$ sleep 1000 &
[1] 15135
$ sleep 9999 &
[2] 15136
$ jobs -x echo %%
15136
$ jobs -x echo %1
15135
$ jobs -x echo %2
15136
$ jobs -x echo %+
15136
$ jobs -x echo %-
15135
$ jobs -x echo %
15136
$ jobs -x echo %sl
-bash: jobs: sl: ambiguous job spec
%sl
$ jobs -x echo %9999
%9999
$ jobs -x echo %?9999
15136
$ jobs -x echo %?1000
15135
$ jobs -x echo %+
15136
$ kill %+
$
[2]+  Terminated              sleep 9999
$ jobs -x echo %+
15135

仅仅通过命名一个作业就可以将其置于前台:%1fg %1 的同义词,可以将作业 1 从后台带到前台。同样,%1 & 可以在后台恢复作业 1,相当于 bg %1

Shell 会在作业状态发生变化时立即感知。通常,Bash 会等到即将打印提示符时 才通知用户作业状态的变化,以免打断其他输出,不过在列表中的前台命令完成后、执行列表中的下一个命令之前,它会通知作业状态的变化。如果启用了 set 内建命令的 -b 选项,Bash 会立即报告状态变化(参见 Set 内建命令)。Bash 会对每个终止的子进程执行 SIGCHLD 的任何 trap。

当一个作业终止并且 Bash 通知用户时,Bash 会将该作业从作业表中移除。它不会出现在 jobs 命令的输出中,但只要提供与该作业关联的进程 ID 作为参数,wait 命令仍会报告其退出状态。当表为空时,作业编号将从 1 重新开始。

如果用户尝试在作业被停止时退出 Bash(或者如果启用了 checkjobs 选项,则在作业运行时退出------参见 Shopt 内置命令),Shell 会显示警告信息,并且如果启用了 checkjobs 选项,还会列出作业及其状态。然后可以使用 jobs 命令来检查它们的状态。如果用户随后立即再次尝试退出,而没有执行其他命令,Bash 不会再显示警告,并会终止所有已停止的作业。

当 Shell 使用 wait 内置命令等待一个作业或进程时,并且作业控制已启用,wait 会在作业状态改变时返回。-f 选项会使 wait 等待作业或进程终止后再返回。

7.2 Job Control Builtins


bg

bash 复制代码
bg [jobspec ...]

在后台恢复每个被挂起 的作业,就像使用'&'启动一样。如果未提供 jobspec,shell 将使用其当前作业。bg 返回零,除非在作业控制未启用时运行,或者在启用作业控制时,未找到任何作业说明或指定的作业是在没有作业控制的情况下启动的。

bash 复制代码
## 使用Ctrl+Z 暂停改进程
$ sleep 1000
^Z
[1]+  Stopped                 sleep 1000
$ jobs
[1]+  Stopped                 sleep 1000
$ bg
[1]+ sleep 1000 &
$ jobs
[1]+  Running                 sleep 1000 &

fg

bash 复制代码
fg [jobspec]

在前台继续执行作业 jobspec 并将其设为当前作业。如果未提供 jobspec,fg 将继续当前作业。返回状态为置于前台的命令的状态;如果在禁用作业控制时运行则返回非零状态,或者在启用作业控制时运行但 jobspec 未指定有效作业,或 jobspec 指定的作业是在未启用作业控制的情况下启动的,也返回非零状态。

bash 复制代码
$ sleep 1000
^Z
[1]+  Stopped                 sleep 1000
$ jobs
[1]+  Stopped                 sleep 1000
$ fg %%
sleep 1000
^Z
[1]+  Stopped                 sleep 1000
$ fg
sleep 1000

jobs

bash 复制代码
jobs [-lnprs] [jobspec]
jobs -x command [arguments]

第一种形式列出了活动的作业。选项的含义如下:

-l

除了常规信息外,还列出进程ID。

-n

仅显示自上次通知用户状态以来状态已更改的作业信息。

-p

仅列出作业进程组领导的进程ID。

-r

仅显示正在运行的作业。

-s

仅显示已停止的作业。

如果提供了 jobspec,jobs 将输出限制为有关该作业的信息。如果未提供 jobspec,jobs 将列出所有作业的状态。除非遇到无效选项或提供了无效的 jobspec,否则返回状态为零。

如果提供了 -x 选项,jobs 会将命令或参数中找到的任何 jobspec 替换为相应的进程组 ID,并执行命令,传递给它参数,并返回其退出状态。

bash 复制代码
$ sleep 1000 &
[1] 14263
$ sleep 9999 &
[2] 14264
$ jobs
[1]-  Running                 sleep 1000 &
[2]+  Running                 sleep 9999 &
$ jobs -l
[1]- 14263 Running                 sleep 1000 &
[2]+ 14264 Running                 sleep 9999 &
$ jobs -p
14263
14264
$ sleep 5 &
[3] 14265

## running jobs and stopped jobs
$ jobs -s
[3]+  Done                    sleep 5
$ jobs -r
[1]-  Running                 sleep 1000 &
[2]+  Running                 sleep 9999 &

## notify job change
$ sleep 2 &
[3] 14266
$ jobs -n
[3]+  Done                    sleep 2
$ jobs -n
$

jobs -x用法的核心是将命令和参数中的 jobspec 替换为 pid,然后执行。

bash 复制代码
## kill 命令本身支持jobspec, jobs -x对其用处不大
$ sleep 1000 &
[1] 14280
$ jobs
[1]+  Running                 sleep 1000 &
$ kill %%
$
[1]+  Terminated              sleep 1000
$ jobs
$

## 但对于其他不支持jobspec的命令则有用
$ jobs -x ps -p %%
    PID TTY          TIME CMD
  14281 pts/0    00:00:00 sleep

💡 jobspec 仅在未被引号包裹且直接作为独立参数时,才会被 bash 识别为作业标识符。

bash 复制代码
$ jobs -x ls -l /proc/%%
ls: cannot access '/proc/%%': No such file or directory

$ ls -l /proc/$(jobs -x echo %%) |more
total 0
-r--r--r--.  1 vagrant vagrant 0 Jan 13 01:48 arch_status
dr-xr-xr-x.  2 vagrant vagrant 0 Jan 13 01:48 attr
-r--------.  1 vagrant vagrant 0 Jan 13 01:48 auxv
-r--r--r--.  1 vagrant vagrant 0 Jan 13 01:48 cgroup
...

kill

bash 复制代码
kill [-s sigspec] [-n signum] [-sigspec] id [...]
kill -l|-L [exit_status]

向由每个 ID 指定的进程发送由 sigspec 或 signum 指定的信号。每个 id 可以是作业规范 jobspec 或进程 ID pid。sigspec 可以是不区分大小写的信号名称,例如 SIGINT(可以带或不带 SIG 前缀)或信号编号;signum 是信号编号。如果未提供 sigspec 和 signum,kill 将发送 SIGTERM。

-l 选项会列出信号名称。如果在使用 -l 时提供了任何参数,kill 会列出与这些参数对应的信号名称,并且返回状态为零。exit_status 是一个数字,指定信号编号或由信号终止的进程的退出状态;如果提供了该参数,kill 会打印导致进程终止的信号的名称。kill 假定进程的退出状态大于 128;小于 128 的任何数都是信号编号。-L 选项等同于 -l。

如果至少成功发送了一个信号,返回状态为零;如果发生错误或遇到无效选项,则返回非零。

💡 当进程被信号终止时,其退出状态值为128 + 信号编号。因此 kill -l 后跟参数的范围为[1,64] 和 [129,192]。

bash 复制代码
$ kill -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX
$ kill -l 1
HUP

$ kill -l 129
HUP
$ kill -l 192
RTMAX
$ kill -l 193
-bash: kill: 193: invalid signal specification

wait

bash 复制代码
wait [-fn] [-p varname] [id ...]

等待每个 id 指定的子进程退出,并返回最后一个 id 的退出状态。每个 id 可以是进程 ID(pid )或作业规范(jobspec);如果提供的是作业规范,wait 会等待该作业中的所有进程。

如果未提供任何选项或 id ,wait 会等待所有正在运行的后台作业以及最近执行的进程替换(如果其进程 ID 与 $! 相同),并且返回状态为零。

如果提供了 -n 选项,wait 会等待任意一个指定的 id ,或者如果没有提供 id ,则等待任意一个作业或进程替换完成,并返回其退出状态。如果提供的 id 中没有一个是 shell 的子进程,或者如果没有提供参数且 shell 没有未等待的子进程,退出状态为 127。

如果提供了 -p 选项,wait 会将返回退出状态的作业的进程或作业标识符赋值给由选项参数指定的变量 varname。该变量不能是只读的,在任何赋值之前会被初始化为未设置状态。此功能仅在与 -n 选项一起使用时才有用。

当启用作业控制时,提供 -f 选项会强制 wait 等待每个 id 终止后再返回其状态,而不是在其状态变化时就返回。

如果没有任何 id 指定一个活动的子进程,则返回状态为 127。如果 wait 被信号中断,任何变量名将保持未设置,返回状态将大于 128,如上所述(参见信号)。否则,返回状态为最后一个 id 的退出状态。

bash 复制代码
## 等待所有任务结束
$ jobs
$ sleep 10 &
[1] 14397
$ sleep 15 &
[2] 14398
$ wait
[1]-  Done                    sleep 10
[2]+  Done                    sleep 15
$ jobs
$

## 等待任一任务结束
$ jobs
$ sleep 10 &
[1] 14399
$ sleep 15 &
[2] 14400
$ wait -n
[1]-  Done                    sleep 10
$ jobs
[2]+  Running                 sleep 15 &

## use variable to store PID
$ jobs
$ sleep 10 &
[1] 14421
$ wait -n -p var1
[1]+  Done                    sleep 10
$ echo $var1
14421
$ ps -p 14421
    PID TTY          TIME CMD

## wait -f demo
$ jobs
$ sleep 10
^Z
[1]+  Stopped                 sleep 10
$ jobs -l
[1]+ 14495 Stopped                 sleep 10
$ wait %%
-bash: warning: wait_for_job: job 1 is stopped
$ wait -f %%
-bash: warning: wait_for_job: job 1 is stopped

## 从另一终端发信号给sleep进程 kill -CONT 14495
## 然后wait -f命令返回,并显示如下信息
[1]+  Done                    sleep 10

disown

bash 复制代码
disown [-ar] [-h] [id ...]

如果没有选项,则从活动作业表中删除每个 id 。每个 id 可能是作业规范 jobspec 或进程ID pid ;如果 idpid ,disown 将使用包含 pid 的作业作为 jobspec

如果提供了 -h 选项,disown 不会从作业表中删除每个 id 对应的作业,而是将其标记,以便当 shell 接收到 SIGHUP 时不向该作业发送 SIGHUP。

如果未提供 id ,-a 选项表示删除或标记所有作业;未提供 id 时使用 -r 选项则删除或标记正在运行的作业。如果未提供 id,且未提供 -a 或 -r 选项,则 disown 删除或标记当前作业。

返回值为 0,除非 id 未指定有效作业。

💡 按照帮助,disown命令最重要的作用是 Remove jobs from current shell。如此,shell接受到的信号就不会转发给他了。

bash 复制代码
## 普通的disown
## 以下从终端1执行
$ jobs
$ sleep 1000 &
[1] 14786
$ jobs -l
[1]+ 14786 Running                 sleep 1000 &
$ ps -p 14786
    PID TTY          TIME CMD
  14786 pts/0    00:00:00 sleep
$ disown
$ jobs -l
$ ps -p 14786
    PID TTY          TIME CMD
  14786 pts/0    00:00:00 sleep
$ echo $$
14726

## 切换到终端2执行。此时终端1退出,但sleep命令仍在
$ kill -HUP 14726
$ ps -p 14786
    PID TTY          TIME CMD
  14786 ?        00:00:00 sleep

suspend

bash 复制代码
suspend [-f]

暂停执行此 shell,直到它接收到 SIGCONT 信号。登录 shell 或未启用作业控制的 shell 不能被暂停;-f 选项将覆盖此设置并强制暂停。返回状态为 0,除非 shell 是登录 shell,或者未启用作业控制且未提供 -f。

例1:

bash 复制代码
$ ( suspend; )
-bash: suspend: cannot suspend: no job control
$ suspend
-bash: suspend: cannot suspend a login shell
$ echo $$
14819
$ suspend -f
## 当前shell暂停
## 在其他终端发送命令 kill -CONT 14819 可使其继续

例2:

bash 复制代码
$ echo $$
14928
$ suspend
-bash: suspend: cannot suspend a login shell
$ bash
$ echo $$
14973
$ suspend

[1]+  Stopped                 bash
$ echo $$
14928
$ fg
bash
$ echo $$
14973
$ jobs -l
$ exit
exit
$ echo $$
14928

当作业控制未激活时,kill 和 wait 内建命令不接受 jobspec 参数。它们必须提供进程 ID。

7.3 Job Control Variables


auto_resume

该变量控制 shell 与用户及作业控制的交互方式。如果该变量存在,那么只包含单个单词且没有重定向的简单命令,会被视为恢复已有作业的候选命令。不可允许歧义;如果有多个作业其名称以该单词开头或包含该单词,则会选择最近访问的作业。在此上下文中,已停止作业的名称是用于启动该作业的命令行,如 jobs 命令显示。如果该变量设置为"exact",则该单词必须与已停止作业的名称完全匹配;如果设置为"substring",则该单词需要匹配已停止作业名称的子字符串。"substring"值提供了类似于"%?string"作业 ID 的功能(见作业控制基础)。如果设置为其他任何值(例如"prefix"),则该单词必须是已停止作业名称的前缀;这提供了类似于"%string"作业 ID 的功能。

相关推荐
dingdingfish9 小时前
Bash学习 - 第8章:Command Line Editing,第1-2节:Intro & Readline Interaction
bash·shell·readline
only_Klein18 小时前
Shell 三剑客
shell·sed·grep·awk
dingdingfish2 天前
Bash学习 - 第6章:Bash Features,第11节:Bash and POSIX
bash·posix
dingdingfish2 天前
Bash学习 - 第6章:Bash Features,第12节:Shell Compatibility Mode
bash·shell·compat·compatibility
alanesnape2 天前
一个支持在线deBug的编辑器/调试器功能详解
shell·在线编译器·在线debug
dingdingfish2 天前
Bash学习 - 第6章:Bash Features,第9节:Controlling the Prompt
prompt·bash·ps1
dingdingfish3 天前
Bash学习 - 第6章:Bash Features,第10节:The Restricted Shell
bash·shell·rbash·restrict
数形长夏3 天前
命令行界面的神秘符号,是上一代程序复用的尝试
架构·bash·batch
dingdingfish3 天前
Bash学习 - 第6章:Bash Features,第7节:Arrays
bash·shell·array·index·associate