在Linux中建立后台任务的若干种姿势
本文完整阅读约需 28 分钟,如时间较长请考虑收藏后慢慢阅读~
由于Linux中SIGHUP信号的存在,导致了当终端Detach(退出终端或网络断开)时,从属于终端的进程也被销毁,导致当前所执行任务停止的问题。而且在日常的使用中,也会遇到需要在一个任务执行的过程中,使用同一个Shell来执行其他任务的情况。这篇文章即来盘点在Linux中建立后台任务的若干种姿势。
姿势1 使用nohup忽略HUP信号
上面提到了,之所以Linux在Shell下运行的进程无法在Shell关闭后继续运行,是因为Shell在退出之后,会对其所有子进程发送SIGHUP信号,以实现真正的『退出』。
但我们需要子进程无视SIGHUP信号,以实现在Shell退出后,进程依旧执行,就需要使用nohup
命令。
该命令的使用方法如下:
nohup {Your Command} &
其中nohup的目的是忽略SIGHUP命令,同时将stdout与stderr输出到$HOME/nohup.out文件,如果你需要指定输出位置,可以使用如下命令:
nohup {Your Command} > /your-folder/your-file &
我们可以来进行以下实验,验证其效果:
# ===终端1===
echo $$ # 输出当前Shell所在的PID
# 23488
nohup ping baidu.com &
# [1] 23561
# nohup: 忽略输入并把输出追加到"nohup.out"
# ===终端2===
pstree -p 23488
# zsh(23488)───ping(23561)
ps -eF | grep 23561
# root 23561 23488 0 37495 4448 3 22:02 ? 00:00:00 ping baidu.com # 23488为父进程ID
# ===终端1===
exit # 或者直接在终端2内执行kill -1 23488(发送SIGHUP信号) or kill -9 23488(强制退出)
# ===终端2===
pstree -p 23488
# 空
ps aux | grep 23488
# 空
ps aux | grep 23561
# root 23561 0.0 0.2 149980 4448 pts/1 SN 22:02 0:00 ping baidu.com
ps -eF | grep 23561
# root 23561 1 0 37495 4448 3 22:02 ? 00:00:00 ping baidu.com # 1为父进程ID
pstree -p 23561
# ping(23561)
实验中可以看出,当终端退出后(无论是正常退出,是只发送退出信号还是强制退出),我们的ping命令都可以独立于终端运行,且当终端退出后,该进程的父进程变为1,即不受Session关闭的影响,可以实现后台执行、断开终端后继续运行的需求,使用这个Stack Overflow问答中非常有意思的一句话就是:
Your script was not killed – it became daemon.
姿势2 使用setsid『断绝父子关系』
上面我们了解到,之所以运行的软件会在Shell退出时跟随退出,是因为软件所在进程属于Shell的Session,而nohup可以实现在Shell关闭时,将自己的父进程修改为1(即init进程),因此我们也可以使用『将软件运行在新的Session』中这样一种方法来实现让软件运行于后台,且不受终端状态影响。
setsid
的使用方法和nohup
一样简单:
setsid {Your Command}
我们同样进行以下实验,验证该命令的使用:
# ===终端1===
setsid ping baidu.com
# ===终端2===
ps -eF | grep ping
# root 23726 1 0 37495 4496 3 22:36 ? 00:00:00 ping baidu.com # 父进程为1,而非Shell所在进程
# ===终端1===
exit # 或者直接在终端2内执行kill -1 {终端1的PID}(发送SIGHUP信号) or kill -9 {终端1的PID}(强制退出)
# ===终端2===
ps -eF | grep ping
# root 23726 1 0 37495 4496 3 22:36 ? 00:00:00 ping baidu.com # 进程在Shell退出后依旧存在,且父进程为1
从实验中,我们可以看出,使用setsid命令之后,进程直接脱离当前Shell的束缚,以独立进程的方式存在,通过这种方式避免终端断开后,造成进程被关闭,也可以实现建立后台任务的功能。
姿势3 子Shell的另类用途
刚刚提到了,Session退出时会发送SIGHUP给其他子进程,导致任务无法后台执行,我们可以使用setsid
来直接脱离父子关系,但其实也可以通过新建子Shell,在子Shell中执行任务的方式来实现后台运行。
子Shell的使用方法非常简单,只需要在命令外面套上一个括号即可:
({Your Command} &)
该命令将当前Shell的环境通过fork()系统调用,复制一份(值复制,非引用复制)到新的Shell,同时执行括号内的程序。
注意:由于fork()原理是复制环境变量,导致了子Shell中对环境变量的修改是不会应用到父Shell中的,除非使用文件(子Shell写,父Shell读)的方式来进行环境变量共享。
我们依旧来进行一个小实验:
# ===终端1===
echo $$
# 24341
(echo $$ && ping baidu.com &)
# 24341
# PING baidu.com (220.181.57.216) 56(84) bytes of data.
# ...
# ===终端2===
pstree -p 24341
# zsh(24341) # 刚刚运行的脚本不属于这个Shell
ps -eF | grep ping
# root 24447 1 0 37495 4492 3 11:30 pts/2 00:00:00 ping baidu.com
# 父进程为1
# ===终端1===
exit # 或者直接在终端2内执行kill -1 {终端1的PID}(发送SIGHUP信号) or kill -9 {终端1的PID}(强制退出)
# ===终端2===
pstree -p 24341
# 无
ps -eF | grep ping
# root 24447 1 0 37495 4492 3 11:30 pts/2 00:00:00 ping baidu.com
# 父进程为1,进程依旧存在
姿势4 使用输出重定向避免干扰当前工作
上面提到了使用nohup
和setsid
来实现隔离Shell与待执行后台进程,然而这两个方法分别有两个大问题:
– 使用nohup,无法自定义输出位置,且错误会直接输出到终端而非输出位置
– 使用setsid,无论是STDOUT还是STDERR,均会被输出到终端
而对于子Shell,同样也无法避免STDOUT和STDERR输出到终端,干扰正常工作的问题。
这个时候就要轮到输出重定向出场!
命令格式如下:
nohup {Your Command} > /tmp/bgjob.log 2>&1 &
setsid {Your Command} > /tmp/bgjob.log 2>&1 &
({Your Command} > /tmp/bgjob.log 2>&1 &)
其中>
符号建立的是STDOUT的重定向输出,即将标准输出重定向到所设置的文件中(清空后重新写入),如果使用>>
符号,则为追加到文件末尾(不清空直接写入)。
而2>&1
指的是将STDERR重定向到STDOUT,以避免错误信息输出到终端。也就是说如果我们需要让错误信息输出到/tmp/bgjob.err.log
文件,也可以写成2>bgjob.err.log
或2>>bgjob.err.log
这样的形式。
姿势5 使用disown&gdb亡羊补牢
上面介绍的所有命令,都是要求在执行命令的最初就配置后台运行相关属性,但如果我们在正常操作过程中(例如编译、安装、定时任务等)突然因为特殊原因需要离开当前Shell(例如网络不稳定、有其他重要工作等),却又不希望此前的过程中断,就可以使用disown与gdb的结合来实现亡羊补牢。
首先我们来介绍一下disown命令,这个命令主要的目的是让属于当前Shell的job与当前Shell『解除父子关系』,避免在终端退出后造成job跟随退出的问题。
disown的使用方法如下:
disown jobspec # 使某个作业忽略HUP信号
看到这里,大家可能意识到一个问题:如果有任务正在运行,我们要怎么做才能输入disown命令,job又从何而来呢?接下来我们就来讲解如何将一个前台任务转换成后台job。
将一个前台任务转换为后台job,首先需要在任务执行时按下Ctrl+Z(注意,这不是撤销键,不要和Windows下的快捷键搞混淆),MacOS键盘下是Control+Z。
ping baidu.com
# ......
# C^z
# [1] + 24850 suspended ping baidu.com
这个时候,我们所执行的任务会被挂起,可以通过jobs
命令查看属于当前Shell的任务列表:
jobs
# [1] + suspended ping baidu.com
可以看到,这个时候任务是处于挂起状态,尽管正在后台,却不能进行任何操作,我们需要让这一任务继续执行,就需要做更多的工作。
首先我们需要修改其STDOUT与STDERR的位置,由于重定向符号是在命令输入阶段定义的,在命令执行的时候,无法通过这样的方式修改,在这里,我推荐使用gdb来修改其输出。
gdb -p 24850 # 24850是上面所执行任务的进程号
# ...
call close(1) # 关闭STDOUT
call close(2) # 关闭STDERR
call dup2(creat("/tmp/stdlog.log", 0755), 1) # 建立新的STDOUT
call dup2(creat("/tmp/stdlog.log", 0755), 2) # 建立新的STDERR
quit # 退出gdb,退出的时候输入y来解除debug
然后我们来恢复这一进程的正常运行,使其解除挂起状态:
jobs
# [1] + suspended ping baidu.com
bg %1 # 这里的%1指的是编号为1的job
# [1] + 24850 continued ping baidu.com
jobs
# [1] + running ping baidu.com
在进行以上操作后,我们使用disown来解除关系:
disown %1
这个时候,我们再来看jobs,就会发现之前的job不见了,进程依旧在运行,只不过这个时候已经与当前Shell脱离关系:
# ===终端1===
jobs
# 空
ps -eF | grep 24850
# root 24850 24028 0 37495 4516 3 21:18 ? 00:00:00 ping baidu.com
# 注意,这个时候该进程还属于Shell的子进程
exit
# ===终端2===
ps -eF | grep 24850
# root 24850 1 0 37495 4516 3 21:18 ? 00:00:00 ping baidu.com
# 进程不仅没有退出,而且父进程变成了init
姿势6 使用screen一劳永逸
上面说了五种不同类型、各有特色的方法来实现在Linux中建立后台任务,最后我们再来说一个一劳永逸的方法,即使用screen。
screen是UNIX/Linux中用来管理多个Shell的实用工具,在GUI出现之前,其地位等同于WM(Window Manager)。
下面介绍Screen的一些常用操作:
screen -S {Session Name} {Your Command}# 新建一个新的Shell会话,并命名为Session Name,且执行Your Command(可省略)
screen -r {Session Name} # 恢复一个已有的Shell会话
screen -X -S {Session Name} quit # 从Session外退出Session
screen -wipe # 退出一个因为任务结束而死掉的Session
screen -ls # 显示当前所有的Session
screen -dm {Your Command} # 新建一个后台Session,并执行Your Comman(可省略)
在进入Screen之后,最小化当前Screen,回到主终端需要按下Ctrl+A之后,再按D键(这个组合键有些类似Emacs,与现在常用的组合键方式有所区别。
其他的快捷键与命令,可以参考Screen – GNU Project – Free Software Foundation.
而我们在这里需要使用的就是最后一条,即新建一个后台Session:
# screen -dmS {Session Name} {Your Command}
screen -dmS ping ping baidu.com
# 无
screen -ls
# There is a screen on:
# 25319.ping (Detached)
# 1 Socket in /var/run/screen/S-root.
但其实第一条新建一个前台Session也是可行的,只是如果需要将『放入后台执行』这一过程至于Shell脚本中,或是觉得每一次进入Screen再退出太麻烦,可以使用最后一条。
附:关于SIGHUP的一个有趣故事
SIGHUP全称为Signal Hangup,即挂断信号,这是因为这一信号伴随着早期的计算机网络而而出现,但在当时还没有调制解调器,让计算机连接上网络要依靠声音耦合器,即一个电话听筒对应一个座子,座子上一个扬声器对应电话麦克风,座子上一个麦克风对应电话扬声器,靠这种方式在电话线路中传递二进制信号。
具体的故事以及该信号的技术细节,可查看SIGHUP – Wikipedia
1 条评论