本文完整阅读约需 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 使用输出重定向避免干扰当前工作

上面提到了使用nohupsetsid来实现隔离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.log2>>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

声音耦合器图片: