现象
经常在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)。
图片:
- 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信号给session leader(对应bash进程)。
- bash进程在收到SIGHUP后,将信号发给session下的所有进程,导致用户启动的进程退出。
下面通过strace命令来验证以上结论:
跟踪hw进程(命令意为跟踪pid为6367的进程上与signal有关的系统调用):
strace -e trace=signal -p 6367
跟踪bash进程(命令意为跟踪pid为5933的进程上与signal有关的系统调用):
strace -e trace=signal -p 5933
关闭启动hw程序的终端,观察strace输出.
hwd的strace如下,si_pid=5933说明是5933这个进程发了SIGHUP给它,也就是bash进程:
bash的strace略微复杂:
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信号:
signal(SIGHUP, SIG_IGN);
执行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);
}
}
调用setsid前先fork,因为若不fork,hw作为进程组的leader,是不允许重建session的,原因留给读者自己思考。
编译并执行hw,查看进程:
可以看到,相比之前,有几个不同的地方:
- 程序启动完,返回终端,hw切换到后台运行。
- hw进程的父进程不再是bash,而是init进程。
- hw没有关联的终端设备(pts/2)。
关闭终端,看到bash已经消失,但对hw进程没有任何影响:
更简单的方法
setsid命令,用setsid来启动程序,这样就不用修改任何代码也可以做到使启动的进程在新的session中,并且终端关闭时,进程不退出。
setsid ./hw
nohup命令,被nohup启动的程序会忽略SIGHUP信号。
nohup ./hw
其他
命令行中&的作用:
./hw &
&的作用是使程序在后台运行,输入fg命令又可以使程序切换到前台。虽然在后台运行,但并不能保证进程在终端关闭时不退出。
总结
简而言之,终端在关闭时会发送SIGHUP给对应的bash进程,bash进程收到这个信号后首先将它发给session下面的进程,如果你的程序没有对SIGHUP信号做特殊处理,那么进程就会随着终端关闭而退出。