作业控制
在shell
中通过command &
可以创建后台作业, 通过jobs -l
命令可以查看当前shell
中维护的作业列表, 包括他们的作业号, 进程号, 运行状态. 其中作业号(jobID
或JOB_SPEC
)是作业在当前shell
中的唯一标识.
作业与进程
作业相比于进程是更高级的调度单位, 其定位类似于进程组, 但与进程组不同的是, 作业只维护其初始进程, 一旦所有的初始进程退出则代表作业执行完毕, 未结束的子进程不会被作业追踪, 如下例:
#!/usr/bin/bash
(sleep 20 & sleep 2) &
jobs -l
pstree -apg $$
ps -ejfH | awk '$10 ~ /^sleep/ {printf "%s %s -- PPID:%s, PGID:%s\n",$10,$11,$3,$4}'
sleep 2
echo
jobs -l
pstree -apg $$
ps -ejfH | awk '$10 ~ /^sleep/ {printf "%s %s -- PPID:%s, PGID:%s\n",$10,$11,$3,$4}'
pkill sleep
运行这个例子可以得到如下输出, 可以看到整个作业初始只包括一个没有wait
的bash
进程19318
, 运行2
秒后19321
退出导致19318
退出, 因此作业结束, 此时19319
由于父进程退出而变为孤儿进程, 但其进程组号未改变:
[1]+ 19318 Running ( sleep 20 & sleep 2 ) &
bash,19317,19317 pgroup.sh
├─bash,19318,19317 pgroup.sh # 作业的初始进程
│ ├─sleep,19319,19317 20 # 作业运行中创建的子进程
│ └─sleep,19321,19317 2 # 作业运行中创建的子进程
└─pstree,19320,19317 -apg 19317
sleep 20 -- PPID:19318, PGID:19317
sleep 2 -- PPID:19318, PGID:19317
# 进程 19321 退出 由于没有 wait 导致整个作业退出
[1]+ 19318 Done ( sleep 20 & sleep 2 )
bash,19317,19317 pgroup.sh
└─pstree,19328,19317 -apg 19317
sleep 20 -- PPID:1, PGID:19317 # 变为孤儿进程 但进程组号未变
作业的存在是为了方便shell
对前后台进程(进程组)进行管理, 一个shell
进程在同一时刻只能存在一个前台作业但可以存在多个后台作业. 每个作业可以包含一个或多个进程, 具体体现为如下情况:
- 作业中包含单个进程
> sleep 50 &
[1] 20536 # 作业号为 1, 包含的进程号为 20536
> jobs -l
[1]+ 20536 Running sleep 50 &
- 作业中包含进程及其子进程
> (sleep 20 & sleep 10 & wait) &
[1] 21111
> pstree -ap $$
bash,21023
├─bash,21111 # 作业的初始进程
│ ├─sleep,21112 20
│ └─sleep,21113 10
└─pstree,21116 -ap 21023
- 作业中包含多个进程
> sleep 10 | sleep 8 & # 管道两侧在 sub shell 中运行
[1] 20689
> jobs -l # 作业中包含两个初始进程
[1]+ 20688 Running sleep 10
20689 | sleep 8 &
前台作业管理
在一个shell
会话中直接输入命令执行的作业会在前台运行, 前台作业会一直阻塞shell
直到其执行完毕或被挂起. 前台作业能够优先处理传递给进程组的信号, 并且优先占用输入文件描述符. 以外部命令cat
命令为例, 在启动后, shell
会把当前的前台进程组指定为新建立的cat
进程组, 这样cat
就接管了整个shell
的标准输入, 标准输出和标准错误文件描述符. 命令的组合也可以作为前台任务被执行, 例如:
while true; do echo "Hello World"; sleep 10; done
要退出上面这个死循环需要给进程发送中断信号, 为了方便, 我们一般使用Ctrl + C
直接给整个进程组发送SIGINT
信号. 但如果我们要在不退出前台作业的基础上拿回终端控制权, 就需要给作业发送SIGTSTP
或SIGSTOP
信号来让他暂时挂起.
> ping 127..0.0.1
^Z # 按下 Ctrl + Z
[1]+ Stopped ping 127.0.0.1
通过Ctrl + Z
可以给shell
中的前台作业发送SIGTSTP
, 这样ping
命令就暂时停止了, 我们重新拿回了shell
的控制权, 通过ps T
命令可以查看当前shell
终端关联的所有进程:
> ps T
PID TTY STAT TIME COMMAND
4731 pts/1 Ss 0:00 /bin/bash
4738 pts/1 T 0:00 ping 127.0.0.1
4891 pts/1 R+ 0:00 ps T
可以看到ping
命令并没有退出, 而且他的状态被改成了T
, 通过查阅man ps
可以知道, T
状态代表进程被作业控制信号停止. 此外, 可以使用内置命令suspend
挂起当前shell
.
后台作业管理
除了让前台作业挂起将其变为后台作业之外, 通过command &
的方式可以让命令直接在后台运行, 我们在刚才挂起的ping
基础上再添加两个运行中的后台作业:
> xeyes &
> xload &
通过jobs -l
可以查看当前shell
中的后台作业:
> jobs -l
[1]+ 6528 Stopped ping 127.0.0.1
[2] 7101 Running xeyes &
[3]- 7104 Running xload &
第一列[1]
表示作业号JOB_SPEC
, 后面跟随的+
表示其为当前作业, 或者说最近的被调往前台的作业, 而-
表示当前作业的前一个作业, 我们可以通过%JOB_SPEC %+ %-
的方式在命令中访问这些作业:
> fg %2 # 或 fg 2, 因为 fg 只对作业有效所以可以省略百分号
xeyes
^Z # 挂起
[2]+ Stopped xeyes
> jobs # 不带 l 选项则不显示进程号
[1]- Stopped ping 127.0.0.1 # 减号表示前一个作业
[2]+ Stopped xeyes # 加号表示当前作业
[3] Running xload &
> fg # 等价于 fg + 或 fg %+ 或 fg %%
第二列数字6528
为该作业包含的初始进程进程号, 初始进程可以为多个, 所有初始进程执行完毕则作业执行完毕. 第三列表示作业的状态, 包括以下几种:
> sleep 5 &
> kill %+ # 等价于 kill %2 百分号不可省略
> jobs
[1]+ Stopped ping 127.0.0.1 # 停止
[2] Terminated xeyes # 被终止
[3]- Running xload & # 运行态
[4] Done sleep 5 # 成功执行完毕并已经退出, 只会显示一次
[5] Hangup ping 127.0.0.1 > /dev/null # 挂起 无特殊处理则退出
最后一列是作业的运行命令文本, 在命令控制任务中可以使用%s
和%?s
通过匹配作业文本的方式指定作业:
> kill -s int %ping # 给作业文本以 ping 开头的作业发送 SIGINT
> fg %?eye # 把作业文本包含 eye 的作业提到前台
我们还可以使用bg
命令让挂起的后台作业直接运行, 而不必将其提到前台, 其用法类似于fg
:
> sleep 100
^Z # 挂起
[1]+ Stopped sleep 100
> bg 1 # 等价于 bg %1
[1]+ sleep 100 &
> jobs
[1]+ Running sleep 100 &
但要注意, 为了防止后台作业与前台作业争抢输入资源, 只要后台作业执行到需要读取输入的代码段, 就会导致该作业被挂起, 这时使用bg
命令无效.
> while true; do read line; done &
[1] 20780
> bg 1
[1]+ while true; do
read line;
done &
# 被输入挂起 仍处于停止状态
[1]+ Stopped while true; do
read line;
done
作业控制命令与符号
作业控制命令及其用法可以总结为如下表格:
符号 | 描述 | 例子 |
---|---|---|
& | 把作业放到后台 | command & |
%n | 作业列表中作业号为n 的作业 |
kill %1 |
%s | 作业列表中名称以字符串s 开头的作业 |
kill %xe |
%?s | 作业列表中名称包括字符串s 的作业 |
jobs %?ping |
%% 或 %+ | 表示当前作业 |
kill %% kill %+
|
%- | 表示当前作业的上一个作业 | bg %- |
Ctrl + Z | 挂起或停止作业 | kill -s stop %ping |
jobs -l | 列出所有作业 | jobs -l |
jobs -r | 列出所有正在运行的作业 | jobs -r |
jobs -s | 列出所有被挂起的作业 | jobs -s |
bg | 让作业在后台运行 | bg %% |
fg | 把作业提到前台 | fg %apt-get |
处理 SIGHUP 信号
无论是前台作业还是后台作业, 他们都呈树状结构依附于shell
进程, 在shell
终端退出时如果存在后台作业, 则会提示There are stopped jobs.
, 如果忽略这个提示继续退出, shell
会向所有作业发送SIGHUP
信号来进行清理. 如果我们需要某些后台进程在终端退出时仍然继续运行, 就需要对SIGHUP
信号进行处理.
外部命令 nohup
使用nohup
命令启动作业可以让该作业忽略SIGHUP
, 其用法如下:
> nohup ping 127.0.0.1 &
[1] 21222
nohup: ignoring input and appending output to 'nohup.out'
# exec ping 127.0.0.1 & 也可以实现类似的效果
# 但 nohup 会自动帮你处理输出重定向
退出当前终端并在另一个终端执行查找ping
进程:
> ps -ef | awk '$8~/^ping/ {print "PID:"$2", PPID:"$3}'
PID:21222, PPID:1 # 变为孤儿进程继续运行
为了防止后台作业阻塞, nohup
会让作业忽略输入, 并将所有输出默认重定向到~/nohup.out
文件中, 我们可以手动进行输出重定向, 而且会自动帮我们将标准错误重定向到标准输出:
> nohup ping 127.0.0.1 > outfile &
[1] 22505
ignoring input and redirecting stderr to stdout
> nohup ping 127.0.0.1 &> outfile &
[2] 22523
但要注意, nohup
命令后不可以通过(...)
的方式在subshell
中执行命令, 这样做会导致解释器将nohup
视为函数从而导致语法错误:
> nohup (sleep 120; echo "job done") & # 这么写会报错
> nohup bash -c 'sleep 120; echo "job done"' & # 可以正常执行
[1] 25758
> pstree -ap $$
bash,23897
├─bash,25758 -c sleep 120; echo "job done"
│ └─sleep,25759 120
└─pstree,25764 -ap 23897
内置命令 disown
nohup
的不足之处在于必须在程序运行前指定是否忽略SIGHUP
信号, 如果我们希望对运行中的作业进行修改可以使用内置命令disown
, 其用法如下:
-
disown %n
: 把编号为n
的作业从列表中剥离, 这回该作业所有的输出消失, 而且无法进行作业控制.
> ping 127.0.0.1 > /dev/null &
[1] 29748
> disown %1
> jobs # 没有输出
> ps -f 29748
UID PID PPID C STIME TTY STAT TIME CMD
remilia 29748 28309 0 15:10 pts/3 S 0:00 ping 127.0.0.1
# 退出终端并在另一个终端中查看该进程
> ps -f 29748 # 变为孤儿进程继续运行
UID PID PPID C STIME TTY STAT TIME CMD
remilia 29748 1 0 15:10 ? S 0:00 ping 127.0.0.1
-
disown -h %n
: 让编号为n
的作业忽略退出时产生的SIGHUP
, 这种方法不会从作业列表中删除该作业, 因此可以继续使用作业控制命令进行管理. -
disown -r
: 从作业列表中剥离所有运行中的作业. -
disown -a
: 从作业列表中剥离所有作业.
该命令的一个缺点是不会自动进行输出重定向, 如果我们需要保存作业的输出可以使用gdb
在程序运行时修改文件描述符的指向.
> python3 logerr.py &
> sudo gdb -p `pgrep python3`
(gdb) p close(2) # 删除标准错误
$1 = 0
(gdb) p creat("/tmp/pyout", 0600) # 创建文件自动连接到标准错误
$2 = 2
(gdb) q # 选 yes
> sudo ls -l /proc/31586/fd/2
l-wx------ 1 remilia remilia 64 Mar 22 15:36 /proc/31586/fd/2 -> /tmp/pyout
选项 huponexit
huponexit
是bash
中的选项, 其用法如下:
> shopt huponexit # 查看选项是否开启
> shopt -s huponexit # 开启选项
开启该选项则会让该shell
会话中的所有后台作业忽略由该会话退出时执行exit
所产生的SIGHUP
, 其他方式传递来的SIGHUP
则不会被忽略, SIGHUP
的传播过程如下:
会话 -- SIGHUP --> bash ( huponexit ) -- SIGHUP --> 作业
如果bash
中开启了huponexit
, 则在会话退出时不会给子进程(作业)分发SIGHUP
信号.
其他方式
我们也可以通过让后台作业变为孤儿进程的方式实现忽略父shell
传递来的SIGHUP
信号:
# subshell 没有 wait 他自己的后台任务因此提前退出
# 这种方式可以自定义重定向输出文件 但无法进行作业管理
> (ping 127.0.0.1 >/dev/null &)
> ps -f `pgrep ping`
UID PID PPID C STIME TTY STAT TIME CMD
remilia 847 1 0 15:49 pts/4 S 0:00 ping 127.0.0.1
另外还有一些工具如screen
, tmux
, dtach
等可以实现更高级的功能.
参考内容
高级Bash脚本编程指南
How To Use Bash's Job Control
Redirecting Output from a Running Process
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。