如果我们关闭启动Linux的终端,它会杀死后台进程吗?

28

我有一个嵌入式系统,在其中我使用telnet,运行一个应用程序在后台:

./app_name &

现在,如果我关闭终端并从其他终端执行telnet,然后检查,我可以看到该进程仍在运行。

为了检查这一点,我编写了一个小程序:

#include<stdio.h>
main()
{
    while(1);
}

我在本地Linux计算机上以后台模式运行了这个程序,并关闭了终端。

现在,当我从其他终端检查此进程时,我发现该进程也被杀死了。

我的问题是:

  • 为什么相同类型的进程会出现未定义的行为?
  • 它依赖于什么?
  • 它是否依赖于Linux的版本?
5个回答

46

谁应该杀死进程?

通常情况下,前台和后台进程会在不同的情况下被内核或 shell 发送的 SIGHUP 信号杀死。


内核何时发送SIGHUP信号?

内核会向控制进程发送SIGHUP信号:

  • 对于真实(硬件)终端:在终端驱动程序中检测到断开连接,例如在调制解调器线路上挂起时;
  • 对于伪终端(pty):当关闭最后一个引用主端的描述符时,例如关闭终端窗口。

内核会向其他进程组发送SIGHUP信号:

  • 控制进程终止时,会向前台进程组发送信号;
  • 当孤立的进程组变为孤儿进程组且具有停止成员时,会向孤儿进程组发送信号。

控制进程是建立与控制终端的连接的会话领导者。

通常,控制进程是您的shell。因此,总结如下:

  • 当真实或伪终端被断开/关闭时,内核向shell发送SIGHUP
  • 当shell终止时,内核会向前台进程组发送SIGHUP
  • 如果孤立的进程组包含已停止的进程,则内核会向其发送SIGHUP

请注意,如果后台进程组中不包含已停止的进程,则内核不会向其发送SIGHUP


什么时候bash会发送SIGHUP信号?

Bash会向所有作业(前台和后台)发送SIGHUP信号:

  • 当它接收到SIGHUP信号,且它是一个交互式shell(并且编译时启用了作业控制支持);
  • 当它退出时,它是一个交互式登录shell,并且设置了huponexit选项(并且编译时启用了作业控制支持)。

更多详情请点击这里

注意:

  • bash不会向使用disown从作业列表中删除的作业发送SIGHUP信号;
  • 使用nohup启动的进程会忽略SIGHUP信号。

更多详情请点击这里


其他shell有什么不同?

通常,shell会传播SIGHUP信号。在正常退出时生成SIGHUP信号较少见。


Telnet或SSH

在使用Telnet或SSH时,当连接关闭(例如当您关闭PC上的telnet窗口时),应该发生以下情况:

  1. 客户端被终止;
  2. 服务器检测到客户端连接已关闭;
  3. 服务器关闭pty的主端;
  4. 内核检测到主pty已关闭并向bash发送SIGHUP
  5. bash接收到SIGHUP,向所有作业发送SIGHUP并终止;
  6. 每个作业都会接收到SIGHUP并终止。

问题

我可以使用busyboxdropbear SSH服务器中的bashtelnetd重现您的问题:有时候,当客户端连接关闭时,后台作业不会接收到SIGHUP(并且不会终止)。

似乎在服务器(telnetddropbear)关闭pty的主侧时发生了一个竞争条件

  1. 通常情况下,bash会收到SIGHUP并立即杀死后台作业(按预期),然后终止;
  2. 但有时,bash在处理SIGHUP之前就在pty的从侧检测到EOF

bash检测到EOF时,默认情况下会立即终止而不发送SIGHUP。而后台作业仍在运行!


解决方案

可以配置 bash 在正常退出(包括EOF)时发送SIGHUP信号:

  • 确保以登录shell的方式启动bash。据我所知,huponexit只对登录shell有效。

    可以通过-l选项或者argv[0]中的前导连字符来启用登录shell。您可以配置telnetd运行/bin/bash -l或更好的是运行/bin/login,它会以登录shell模式调用/bin/sh

    例如:

    telnetd -l /bin/login
    
  • 启用huponexit选项。

    例如:

    shopt -s huponexit
    

    每次在bash会话中键入此命令,或将其添加到.bashrc/etc/profile中。


为什么会出现这场竞争?

bash 只有在安全时才解除信号阻塞,当某些代码区段不能被信号处理程序安全中断时则会阻塞它们。

这样的 临界区段 会不时地调用 中断点,如果在执行临界区段时接收到了信号,则其处理程序将被延迟到下一个中断点发生或退出临界区段时才执行。

你可以从源代码中的 quit.h 开始查找。

因此,在我们的情况下,似乎 bash 有时会在临界区段中收到 SIGHUPSIGHUP 处理程序的执行被延迟,bash 在退出临界区段或调用下一个中断点之前读取 EOF 并终止。


参考资料

  • 官方Glibc手册中的"作业控制"章节。
  • "The Linux Programming Interface"一书中的第34章"进程组、会话和作业控制"。

完美,感谢您的回答。 - Chirag
@Chirag,我已经在答案中添加了一个猜测,为什么“bash”中可能会发生竞争。 - gavv
@gavv 谢谢。不过我使用 trap 进行了一些本地测试。Bash 接收到了 SIGCLD 和 SIGCONT 以及 SIGHUP。为什么?我们为什么需要 确保 bash 作为登录 shell 启动 - Amos
@Amos 当子进程终止时,通常会发送SIGCLD信号。当进程组变成孤儿进程组时,将发送SIGCONT信号。不确定是否为此情况,需要更多数据。 - gavv
@Amos "我们为什么需要确保bash作为登录shell启动?" -- huponexit只对登录shell有效,如果我没记错的话 https://dev59.com/YWEi5IYBdhLWcg3wbrzS#21294799 "如果使用shopt设置了huponexit shell选项,则当交互式登录shell退出时,bash会向所有作业发送SIGHUP信号。" 我会更新答案。 - gavv
@gavv 感谢您的澄清。这是我的完整测试用例。https://paste.wentropy.com/zAcW - Amos

9

当您关闭终端时,shell会向所有后台进程发送SIGHUP信号-这会导致它们被杀死。这可以通过多种方式抑制,最重要的是:

nohup

当您使用nohup运行程序时,它会捕获SIGHUP并重新定向程序输出。

$ nohup app &

disown

disown命令告诉shell不要发送

$ app &
$ disown

它是否依赖于Linux的版本?

它取决于您使用的shell。至少适用于bash


请注意,shell通常只在收到SIGHUP时发送SIGHUP(请参见我的答案)。据我所知,OP的问题是,shell要么没有接收到SIGHUP,要么没有传播SIGHUP。 - gavv

6
据我所知,在这两种情况下,进程应该被终止。为了避免这种情况,您需要发出类似以下的 nohup 命令:
> nohup ./my_app &

这样,您的进程将继续执行。可能是因为类似于此BUG的原因导致了telnet部分的问题:

https://bugzilla.redhat.com/show_bug.cgi?id=89653


5
为了完全理解正在发生的事情,您需要稍微了解一下Unix内部。
当您运行像这样的命令时: ./app_name &
app_name被发送到后台进程组。您可以在此处了解有关Unix进程组的信息here
当您使用正常退出关闭bash时,它会向所有作业触发SIGHUP挂起信号。有关Unix作业控制的一些信息在此处here
为了使您的应用程序在退出bash时继续运行,您需要使用nohup实用程序使应用程序免受挂起信号的影响。
nohup - 运行一个免疫挂起的命令,输出到非tty
最后,这就是您需要做的。

nohup 应用程序名 & 2> /dev/null;


Bash并不总是(从来没有?)向所有正常退出的作业发送SIGHUP信号。它会转发来自其运行终端的SIGHUP信号。请参见我对此问题的回答:http://serverfault.com/questions/117152/do-background-processes-get-a-sighup-when-logging-off 。奇怪的是,即使使用shopt -s huponexit,我也从未看到bash发送SIGHUP信号,至少在CentOS7.1中是如此。当bash作为/bin/sh运行时,我也没有看到它发送SIGHUP信号,当然在/bin/csh中也没有。 - Mike S
如果我从Java ProcessBuilder运行后台进程,那么这个后台Linux进程是否会通过这个信号终止? - the_prole

0
在现代的 Linux 系统中——也就是带有 systemd 的 Linux 系统——出现这种情况还有另外一个原因需要你知道: "linger"。
即使进程已经正确地守护化并且受到了 HUP 保护,systemd 仍然会杀掉从登录 shell 中留下来的进程。这是现代 systemd 配置的默认行为。
如果您运行
loginctl enable-linger $USER

您可以禁用此行为,允许后台进程继续运行。其他答案涵盖的机制仍然适用,因此您还应该保护您的进程免受它们的影响。

enable-linger是永久性的,直到被重新禁用。您可以使用以下命令进行检查:

ls /var/lib/systemd/linger

这可能有文件,每个用户名一个,用于启用了linger的用户。目录中列出的任何用户都可以在注销时保留后台进程运行的能力。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接