Linux shell 退出后任务仍然运行引发的探究

4,890 阅读4分钟

今天通过 SSH 连接到服务器的 Shell 时,执行了一个 Python 脚本,并通过 CTRL+Zbg 将其放到了后台运行。

之后我退出了该 shell,但出乎意料的是,当我再次登录 shell 时该脚本仍然在运行,而正常来说,该脚本应该在 shell 退出时被终止。

于是我查阅了相关文档,探究其原因。

通常情况

当我们登录到交互式的 shell 之后,所执行的命令一般都是派生自当前 shell 进程,作为其子进程运行。

退出 shell 时,所有任务(子进程)都会接收到该 shell 发送的 SIGHUP 信号,从而被终止。

要避免任务在退出 shell 时被终止,一般要通过 nohupdisownsetsid 等命令来实现。

具体方法可参考该文章:Linux 技巧:让进程在后台可靠运行的几种方法

探究

为了找到原因,我们对问题进行复现。

首先登录到远程 shell 中,执行 sleep 3600 & 命令,我们可以通过 jobs 看到该命令在后台运行。

root@VPS-WRAY:~# sleep 3600 &
[1] 23960
root@VPS-WRAY:~# jobs
[1]+  Running                 sleep 3600 &

利用 ps 命令查看进程状态:

root@VPS-WRAY:~# ps -Hf
UID        PID  PPID  C STIME TTY          TIME CMD
root     23946 23944  0 16:13 pts/0    00:00:00 -bash
root     23960 23946  0 16:13 pts/0    00:00:00   sleep 3600
root     23961 23946  0 16:13 pts/0    00:00:00   ps -Hf

可以看到 sleep 进程的 ppid 是 23946,也就是说它的父进程是 bash。

由于在创建该任务时并没有使用 nohup 命令,所以当退出 shell 时,该任务应该会收到由 shell 发送的 SIGHUP 信号,从而结束。

我们退出后再次登录远程 Shell,查看进程状态:

root@VPS-WRAY:~# ps -ef | grep sleep
root     23960     1  0 16:13 ?        00:00:00 sleep 3600
root     24003 23993  0 16:27 pts/1    00:00:00 grep sleep

sleep 并没有结束!并且它的父进程变成了 init 进程(ppid = 1)。

我们看看手动发送 SIGHUP 信号能否结束 sleep 进程:

root@VPS-WRAY:~# kill -s SIGHUP 23960
root@VPS-WRAY:~# ps -ef | grep sleep
root     24077 23993  0 16:35 pts/1    00:00:00 grep sleep

sleep 进程被终止了。

这说明了问题出在 shell 上,当我们退出 shell 时,bash 进程终止了,但它并没有给子进程 sleep 发送 SIGHUP 信号,于是 sleep 进程变成了孤儿进程,然后过继给 init 进程,所以 ppid 变为了 1,继续在系统中运行。

但是,shell 在退出时不是会自动发送 SIGHUB 信号吗?

原因

以下段落引自 BASH man page

The shell exits by default upon receipt of a SIGHUP. Before exiting, an interactive shell resends the SIGHUP to all jobs, running or stopped. Stopped jobs are sent SIGCONT to ensure that they receive the SIGHUP. To prevent the shell from sending the signal to a particular job, it should be removed from the jobs table with the disown builtin (see SHELL BUILTIN COMMANDS below) or marked to not receive SIGHUP using disown -h.

If the huponexit shell option has been set with shopt, bash sends a SIGHUP to all jobs when an interactive login shell exits.

也就是说,在 huponexit 选项被设置了的情况下,交互式的 shell 在退出前会发送 SIGHUP 信号给所有任务。但是,bash 中该选项默认是没有设置的,因此在退出时不会向子进程发送 SIGHUP 信号。

通过 shopt -p 命令可以列出 shell 选项,-u 表示未被设置,-s 表示被设置:

root@VPS-WRAY:~# shopt -p | grep huponexit
shopt -u huponexit

该选项处于未被设置的状态,这解释了为什么在退出 shell 时,普通后台任务还能继续运行。

我们可以通过 shopt -s huponexit 命令来设置该选项,将其写入到 /etc/profile 中可以确保每次退出 shell 时向后台任务发送 SIGHUP 信号,进而终止后台任务。

当然,从另一个角度来看,这也是实现后台长期执行任务的一种方法,不过相比之下,从一开始就使用 nohupdisown 等命令来实现会更稳妥一些。

相关文章