terminal
linux引导完成后,可用Ctl+Alt+Fx切换的terminal,正是/dev/ttyx,这算是某种硬件的terminal(当然如果真的有硬件的terminal,也会是一个/dev/ttyx)。
而konsole之类的虚拟termianl软件(sshd流程类似),例如在konsole中新建一个标签页(在自动运行bash之前),就会申请一个/dev/pts/x。其中p就是pseudo,即虚拟的terminal。
运行`ps aux`就可以看到每个进程属于哪个terminal,这跟下文的session概念是关联的,一个进程启动后就隶属于一个session,也就会受控于一个tty,因而一般情况下就是这个进程的0 1 2三个fd使用的那个tty。
可以通过进程在/proc/pid/fd中的实际映射来确认是哪个tty。
其中的0,即stdin,显然不能被进程共用,所以就有了前台和后台的概念。
进程组PGRP
每个进程都属于一个PGRP,组内有个group leader,也就是getpgrp() == pid()的那个进程。
例如我们运行`cat list | sort`,那么就会创建一个新的进程组,cat进程会是leader。
当谈论前台和后台的概念的时候,单位是进程组,例如`cat list|sort`就会被认为是某个terminal的前台进程组;按下^Z,或者发送SIGSTOP,这个进程组就会suspend,并且切换到后台;再运行bg,或者发送SIGCONT,这个进程组仍保持后台,只是会继续运行。在bash中,可以直接通过运行`cat list|sort &`把进程组在后台启动(或者仍是经历了前台到后台的切换过程?待展开)。
按^Z的时候整个进程组suspend,所以进程组中所有的信号都接收到了信号,这对于^C同样有效,这是shell自动完成的,不过似乎发送信号本来就可以整组发送,可以参看kill的这种用法。
一般的shell创建子进程的时候,都是新起的进程组;不过自己写的程序调用fork()/system()之类,子进程的group leader会跟父进程相同。
进程session
session包含了很多进程组,其中有个session leader,也就是getsid() == pid()的那个进程。
session leader的所有后代进程,都隶属于这个session,除非其中有主动调用setsid()独立门户开创一个新的session的。
session的概念也是为了使用tty,一个session必须对应一个tty,这样当这个tty断开的时候,就能够批量解决所有使用这个tty的进程的后续问题(否则就会留下一堆进程,永无止境地等待一个断开的stdin)。
当一个tty断开(ssh连接因为网络故障中断大概是这种情况)
kernel向tty对应的session leader发送SIGHUP。一般session leader是一个shell,这个shell一般会继续给它的子进程们发送SIGHUP,所以整个session就都终结了,也就不会有什么进程继续使用这个断开的tty了。
根据bash的文档,bash在收到SIGHUP后,会给它所有的job发送SIGHUP,如果其中有suspend状态的,则先SIGCONT再SIGHUP。
当session leader退出
那么leader的第一级子进程马上就会被init收养(即PPID为1),但是它的session leader和group leader都不变。
这种session leader不存在的情况据说叫做孤儿进程组,据说孤儿进程组会收到kernel的SIGHUP信号,然后如果不处理都会停止掉。
经本人测试,跑得很欢,虽然tty已经不可用(ps命令显示问号)。
细节:bash的shopt中的huponexit默认是on,也就是exit的时候不会给jobs发送SIGHUP,所以估计只有在开启了这个选项的leader bash exit的时候才会给下属的进程组发送SIGHUP。
如何成为daemon
理想的daemon,session独立,不使用任何tty。
对于别人写的程序,`setsid program > /dev/null 2>&1 < /dev/null`启动,setsid会先fork,然后setsid(),最后exec() program。
这样setsid进程fork后马上退出,shell认为前台命令组结束,stdin回到shell;program处于一个新的session,并且是leader;program也没有对应的tty,这是setsid在exec()之前干的大概;ll /dev/pid/fd就可以发现,0 1 2都是/dev/null,目的就达到了。
如果是自己写程序,那么就自己fork();setsid();close(0);close(1);close(2);,这样比上一种方式更干净。
总而言之,fork()的目的有二,一是让父进程(一般是shell)的waitpid结束,继续下去;一个是fork()后新的子进程才能保证不是一个group leader,这样setsid()才能执行成功。
而nohup命令,实际用起来,感觉用处不大,特别是setsid之后基本就不会被SIGHUP了。
另外传统上,daemon一般在SIGHUP的响应代码中实现reload的功能。
此文有待知识点完善后修正。