现象
常常在linux下开发的人应当都有这样的经验,就是在终端上启动的程序,在关掉终端时,这个程序的进程也被一起关掉了。看下边这个程序,为了使进程永远运行,在输出helloworld后,循环调用sleep:
#1:4:b:f:b:2:3:9:3:4:6:b:a:3:2:0:c:9:2:c:e:1:a:a:f:1:5:6:5:a:4:2#
直接关掉这个终端,在另一个终端上查找该进程,已然找不到了:
#6:2:8:e:6:b:e:f:7:c:d:e:7:0:3:0:f:b:2:4:0:a:8:e:e:1:2:4:4:1:3:7#
这个行为看上去仿佛是理所其实的,也符合人的第一觉得:”在终端上启动的程序是属于终端的,所以当关掉终端时,这个终端里的一包裹进程都一起被解决掉了”。但这些说法是不能使一个会思索且饱含好奇心的人信服的。
下边我们就从linux进程管理的细节来分析其根本诱因。
终端进程
linux系统是基于进程的,几乎每位命令都可以在相应的目录下找到它们的程序,执行一个命令相当于启动一个或多个程序,终端也不例外,在我centos下边终端对应一个bash程序(不同操作系统终端的bash程序可能不一样),它坐落/usr/bin/下边:
#9:1:f:e:c:6:5:a:9:7:c:9:7:5:1:5:1:a:d:a:4:a:f:3:f:c:8:2:7:0:8:a#
每每打开一个终端就会启动一个bash进程,我这儿启动了两个终端,可以看见有两个bash进程:
#e:5:b:b:a:e:4:4:1:3:5:3:3:8:b:e:1:d:8:a:6:9:b:f:b:4:4:b:4:7:c:e#
终端进程与启动进程的关系
linux系统上面所有的进程的关系可以看做一个树状结构,系统持续运行,进程的不断启动就是不断fork的过程(fork是linux系统api,作用是复制自己来世成子进程),从系统启动、初始化、登录终端、到执行命令都是生成子进程的过程:
#0:8:b:b:2:b:0:e:0:2:8:6:d:f:7:e:0:9:6:0:9:6:1:9:7:3:a:7:d:0:4:2#
init进程是所有进程的先祖,它的pid(进程id)为1,ppid(父进程id)也为1,由于它没有父进程,系统内的其他进程都是由它或则它的子进程fork而至。
我们在linux上作业的终端对应了一个bash进程,在其上运行的命令和程序都是bash的子进程,或由bash的子进程衍生。
用hw程序验证一下,可以看见hw进程的父进程恰好是bash进程:
#a:2:2:a:1:e:5:a:1:f:4:e:6:0:a:4:4:d:d:c:0:3:c:1:b:9:d:0:a:b:a:0#
但这并不能解释为何终端关掉了在前面运行的程序也跟随退出,由于在linux下,进程之间的关系并不像线程那样,当主线程退出时,子线程一起被强制退出。进程之间没有主次的区别,但有兄妹关系,而母女进程的运行是相对独立的,一方的退出不会造成另一方退出。
进程session-揭露真相
在linux下,一个session是由一组进程组构成的,每位进程组又由多个进程构成。
在一个bash上运行的程序都归属于一个session(除非特别处理),而这个bash就是这个session的leader。每位session又可以关联一个控制终端(ControllingTerminal)。
#8:e:f:3:e:8:5:7:3:a:a:f:e:0:3:0:4:a:c:f:1:2:d:a:4:0:4:4:e:e:7:8#
图片:
hw进程的ppid=5933,说明父进程为第一个bash,这个bash的父进程为gnome-ternimal进程,gnome-ternimal是centos可视化界面的终端管理进程,每打开一个终端,它就会启动一个bash进程,而用户的命令也是直接由bash进程执行的。hw程序和第一个bash同属于一个session(sid=5933),这个sid等于bash的pid,所以第一个bash是这个session的leader。图片中还显示了bash和hw进程拥有共同的终端设备pts/2,它是一种字符设备,不同于前面提及的gnome-ternimal进程。当控制终端(对应gnome-ternimal)测量到终端设备断(对应pts/2)开联接时,会通知设备的控制进程,即发送SIGHUP讯号给sessionleader(对应bash进程)。bash进程在收到SIGHUP后,将讯号发给session下的所有进程,造成用户启动的进程退出。
下边通过strace命令来验证以上推论:
跟踪hw进程(命令意为跟踪pid为6367的进程上与signal有关的系统调用):
strace-etrace=signal-p6367
跟踪bash进程(命令意为跟踪pid为5933的进程上与signal有关的系统调用):
strace-etrace=signal-p5933
关掉启动hw程序的终端,观察strace输出.
hwd的strace如下,si_pid=5933说明是5933这个进程发了SIGHUP给它,也就是bash进程:
#e:3:6:4:a:a:2:6:9:8:b:f:c:8:0:0:6:4:7:1:2:0:7:f:3:d:0:7:1:c:e:6#
bash的strace略微复杂:
#b:3:3:2:2:0:d:8:2:c:4:b:8:f:1:e:b:8:9:f:3:5:3:b:8:0:b:3:b:9:3:9#
kill(4294960929,SIGHUP)
kill第一个参数是32位有符号整数,转换成int就是-6367,当参数为负时表示发送给这个数绝对值的进程组,即pgrp=6367的所有进程,在里面的图片中可以见到hw进程刚好属于该进程组。
kill(5933,SIGHUP)
5933是自己的pid,bash在第一次收到SIGHUP时先把讯号发给session内其他进程,之后再度发送SIGHUP命令给自己,将自己杀害,前面的si_pid=5933也否认了这一点。
怎么让终端关掉时进程不退出
按照前面的推论,要使终端关掉时进程不退出,有以下几种情况:
用户进程拦截SIGHUP讯号。用户进程和bash进程不在一个session。
下边依次验证这两种情况
拦截SIGHUP
更改hw程序,忽视SIGHUP讯号:
#9:8:b:6:5:2:b:f:8:3:a:a:5:4:a:4:6:2:4:3:d:1:4:2:7:6:f:4:f:c:a:6#
执行hw程序,并查看进程,可以看见hw进程和父进程bash:
#a:5:9:e:a:c:b:b:a:8:b:6:9:c:e:3:5:2:4:b:5:e:c:3:9:5:f:8:2:2:b:4#
关掉终端,在另一个终端查看进程:
#f:c:5:2:c:e:8:e:d:3:f:f:1:0:a:4:3:f:0:4:0:d:3:b:9:b:7:a:3:0:f:8#
bash进程早已退出,但hw进程还在,符合预期!!并且hw进程的ppid弄成了1,说明hw在父进程bash退出后弄成孤儿进程被init进程收留。
新建session&setsid
为了使用户进程和bash不在同一个session,须要调用setsid方式,该方式的作用是新建一个新的session,并使自己成为leader。
#3:1:6:d:e:6:d:c:b:0:a:6:c:6:5:2:8:1:2:f:9:4:2:a:b:c:d:3:4:a:1:6#
调用setsid前先fork,由于若不fork,hw作为进程组的leader,是不容许重建session的,缘由留给读者自己思索。
编译并执行hw,查看进程:
#7:7:3:f:e:7:5:3:d:6:f:f:f:0:b:0:a:b:8:e:1:8:d:d:c:6:0:e:4:b:8:d#
可以看见,相比之前,有几个不同的地方:
程序启动完,返回终端,hw切换到后台运行。hw进程的父进程不再是bash,而是init进程。hw没有关联的终端设备(pts/2)。
关掉终端,见到bash早已消失,但对hw进程没有任何影响:
#1:1:e:e:d:1:3:8:9:0:6:c:1:b:9:b:a:a:8:1:1:4:9:7:b:0:6:7:1:f:b:6#
更简单的方式
setsid命令,用setsid来启动程序,这样就不用更改任何代码也可以做到使启动的进程在新的session中,而且终端关掉时,进程不退出。
setsid./hw
nohup命令,被nohup启动的程序会忽视SIGHUP讯号。
nohup./hw
其他
命令行中&的作用:
#9:f:3:9:d:8:d:5:2:a:1:7:2:6:4:8:b:8:c:f:8:d:e:9:0:4:5:1:6:8:c:6#
&的作用是使程序在后台运行,输入fg命令又可以使程序切换到前台。似乎在后台运行,但并不能保证进程在终端关掉时不退出。
总结
简而言之,终端在关掉时会发送SIGHUP给对应的bash进程,bash进程收到这个讯号后首先将它发给session下边的进程,假如你的程序没有对SIGHUP讯号做特殊处理,这么进程都会随着终端关掉而退出。