linux作业控制 信号,bash中的作业控制机制

作业控制

在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等可以实现更高级的功能.

参考内容

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值