linux终端关闭时为什么会导致在其上启动的进程退出?

现象

经常在linux下开发的人应该都有这样的经验,就是在终端上启动的程序,在关闭终端时,这个程序的进程也被一起关闭了。看下面这个程序,为了使进程永远运行,在输出helloworld后,循环调用sleep:

这里写图片描述

直接关闭这个终端,在另一个终端上查找该进程,已经找不到了:

这个行为看起来似乎是理所当然的,也符合人的第一感觉:”在终端上启动的程序是属于终端的,所以当关闭终端时,这个终端里的一包裹进程都一起被解决掉了”。但这种说法是不能使一个会思考且充满好奇心的人信服的。

下面我们就从linux进程管理的细节来剖析其根本原因。

终端进程

linux系统是基于进程的,几乎每个命令都可以在相应的目录下找到它们的程序,执行一个命令相当于启动一个或多个程序,终端也不例外,在我centos下面终端对应一个bash程序(不同操作系统终端的bash程序可能不一样),它位于/usr/bin/下面:

这里写图片描述

每当打开一个终端都会启动一个bash进程,我这里启动了两个终端,可以看到有两个bash进程:

这里写图片描述

终端进程与启动进程的关系

linux系统里面所有的进程的关系可以看做一个树形结构,系统持续运行,进程的不断启动就是不断fork的过程(fork是linux系统api,作用是复制自己来生成子进程),从系统启动、初始化、登录终端、到执行命令都是生成子进程的过程:

这里写图片描述

init进程是所有进程的祖先,它的pid(进程id)为1,ppid(父进程id)也为1,因为它没有父进程,系统内的其他进程都是由它或者它的子进程fork而来。

我们在linux上作业的终端对应了一个bash进程,在其上运行的命令和程序都是bash的子进程,或由bash的子进程衍生。

用hw程序验证一下,可以看到hw进程的父进程正好是bash进程:

这里写图片描述

但这并不能解释为什么终端关闭了在上面运行的程序也跟着退出,因为在linux下,进程之间的关系并不像线程那样,当主线程退出时,子线程一起被强制退出。进程之间没有主次的区别,但有父子关系,而父子进程的运行是相对独立的,一方的退出不会导致另一方退出。

进程session-揭开真相

在linux下,一个session是由一组进程组构成的,每个进程组又由多个进程构成。

在一个bash上运行的程序都归属于一个session(除非特别处理),而这个bash就是这个session的leader。每个session又可以关联一个控制终端(Controlling Terminal)。

这里写图片描述

图片:

  1. hw进程的ppid=5933,说明父进程为第一个bash,这个bash的父进程为gnome-ternimal进程,gnome-ternimal是centos可视化界面的终端管理进程,每打开一个终端,它都会启动一个bash进程,而用户的命令也是直接由bash进程执行的。
  2. hw程序和第一个bash同属于一个session(sid=5933),这个sid等于bash的pid,所以第一个bash是这个session的leader。
  3. 图片中还显示了bash和hw进程拥有共同的终端设备pts/2,它是一种字符设备,不同于上面提到的gnome-ternimal进程。
  4. 当控制终端(对应gnome-ternimal)检测到终端设备断(对应pts/2)开连接时,会通知设备的控制进程,即发送SIGHUP信号给session leader(对应bash进程)。
  5. bash进程在收到SIGHUP后,将信号发给session下的所有进程,导致用户启动的进程退出。

下面通过strace命令来验证以上结论:

  1. 跟踪hw进程(命令意为跟踪pid为6367的进程上与signal有关的系统调用):

    strace -e trace=signal -p 6367

  2. 跟踪bash进程(命令意为跟踪pid为5933的进程上与signal有关的系统调用):

    strace -e trace=signal -p 5933

  3. 关闭启动hw程序的终端,观察strace输出.

hwd的strace如下,si_pid=5933说明是5933这个进程发了SIGHUP给它,也就是bash进程:

这里写图片描述

bash的strace略微复杂:

这里写图片描述

  1. kill(4294960929, SIGHUP)

    kill第一个参数是32位有符号整数,转换成int就是-6367,当参数为负时表示发送给这个数绝对值的进程组,即pgrp=6367的所有进程,在上面的图片中可以看到hw进程正好属于该进程组。

  2. kill(5933, SIGHUP)

    5933是自己的pid,bash在第一次收到SIGHUP时先把信号发给session内其他进程,然后再次发送SIGHUP命令给自己,将自己杀死,后面的si_pid=5933也证实了这一点。

如何让终端关闭时进程不退出

根据上面的结论,要使终端关闭时进程不退出,有以下几种情况:

  1. 用户进程拦截SIGHUP信号。
  2. 用户进程和bash进程不在一个session。

下面依次验证这两种情况

拦截SIGHUP

修改hw程序,忽略SIGHUP信号:

signal(SIGHUP, SIG_IGN);
  • 1
  • 2

执行hw程序,并查看进程,可以看到hw进程和父进程bash:

这里写图片描述

关闭终端,在另一个终端查看进程:

这里写图片描述

bash进程已经退出,但hw进程还在,符合预期!!而且hw进程的ppid变成了1,说明hw在父进程bash退出后变成孤儿进程被init进程收养。

新建session&setsid

为了使用户进程和bash不在同一个session,需要调用setsid方法,该方法的作用是新建一个新的session,并使自己成为leader。

// 先fork
int pid = fork();
if(pid > 0){
    // 父进程, 直接退出
    return 1;
}else if(pid == 0){
    // 子进程
    // 创建新的session
    setsid();
    //
    printf("Hello World!\n");
    printf("sleeping...\n");
    while(1){
        sleep(1);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

调用setsid前先fork,因为若不fork,hw作为进程组的leader,是不允许重建session的,原因留给读者自己思考。

编译并执行hw,查看进程:

这里写图片描述

可以看到,相比之前,有几个不同的地方:

  1. 程序启动完,返回终端,hw切换到后台运行。
  2. hw进程的父进程不再是bash,而是init进程。
  3. hw没有关联的终端设备(pts/2)。

关闭终端,看到bash已经消失,但对hw进程没有任何影响:

这里写图片描述

更简单的方法

  1. setsid命令,用setsid来启动程序,这样就不用修改任何代码也可以做到使启动的进程在新的session中,并且终端关闭时,进程不退出。

    setsid ./hw

  2. nohup命令,被nohup启动的程序会忽略SIGHUP信号。

    nohup ./hw

其他

命令行中&的作用:

./hw &
  • 1
  • 2

&的作用是使程序在后台运行,输入fg命令又可以使程序切换到前台。虽然在后台运行,但并不能保证进程在终端关闭时不退出。

总结

简而言之,终端在关闭时会发送SIGHUP给对应的bash进程,bash进程收到这个信号后首先将它发给session下面的进程,如果你的程序没有对SIGHUP信号做特殊处理,那么进程就会随着终端关闭而退出。

猜你喜欢

转载自blog.csdn.net/qfire/article/details/80112701