先放总结给没空看全文的人:
- session(会话)是用户登录系统以后所需的context(上下文)
- process group(进程组)是一组相关联的进程,用来方便信号量的分发
- session退出以后所有隶属于该session的进程组都会收到hup信号而挂起,这样就有了控制进程生命周期的作用
- tty可以作为输入输出设备被绑定到一个session上,bash就是这么干的
- 子进程会继承父进程的session和process group,可以通过
setsid
建立新的session来脱离继承的sid,从而逃离旧session的生命周期 - 当使用killpg将信号传递给进程组的时候,这个信号会被分发至进程组的每个进程。所以进程组可以方便对进程的控制
前述模型-父子进程
在日常的学习生活中,普通人接触较多的主要是parent-children进程模型。这是一个简单明了的编程模型:
- 父进程
fork()
以后产生一个子进程,子进程继承父进程的资源 - 子进程
exec()
点什么 - 假如子进程退出,那么父进程就得
wait()
/signal(SIGCHLD,SIG_IGN)
,否则子进程会变僵尸进程 - 假如父进程退出,那么子进程就会变成孤儿进程,然后被
init
收养
一个典型的父子进程模型的代码如下:
#include <stdio.h>
#include <sys/types.h>
void ChildProcess(void); /* child process prototype */
void ParentProcess(void); /* parent process prototype */
void main(void)
{
pid_t pid;
signal(SIGCHLD,SIG_IGN);
pid = fork();
if (pid == 0)
ChildProcess();
else
ParentProcess();
}
但是在多用户多终端下,简单的进程的父子进程模型难以描述复杂的进程关系,于是Linux在原有模型的基础上,引入了session(会话)和process group(进程组)的概念。
Linux进程模型
session
因为Linux是多用户系统,每个用户都需要和系统相对独立的进行交互,于是session的概念就被引入进来。当用户log in,然后打开bash的时候,会通过系统调用setsid
建立一个session。session就是用户此次登录的context,可以方便的用来存储信息、控制会话、分配资源。比如,session就是tty(控制终端)绑定的对象。下面是ssh登录远端服务器时,关于session创建的例子:
- 首先列出参与的进程信息
UID PID PPID PGID SID C STIME TTY TIME CMD
root 1488 1 1488 1488 0 2017 ? 00:02:22 /usr/sbin/sshd -D
ubuntu 17132 17092 17092 17092 0 14:59 ? 00:00:00 sshd: ubuntu@pts/0
ubuntu 17135 17132 17135 17135 0 14:59 pts/0 00:00:00 -bash
ubuntu 17171 17135 17171 17135 0 15:00 pts/0 00:00:00 ps -ejf
- 服务端上守护进程
sshd 1488
监听到了client连接请求 - 守护进程
fork
一个子进程sshd: ubuntu 17132
,来处理请求。可以看到这个子进程17132
的SID与父进程1488
不同,这里是通过系统调用setsid()
建立了一个新的session - 进程
sshd 17132
fork
并exec
了子进程bash
作为交互界面,可以看到bash 17135
又创建了一个新的session,并且绑定了一个虚拟控制终端(controlling terminal / tty)pts/0
。至此我们终于可以通过sshd 17132
向pts/0
输入交互信息了,这些输入最终会作为session 17135
的stdin被bash接受并处理。 - 在bash中我们执行了
ps -ejf
查看进程信息,于是bash创建了一个子进程ps -ejf
执行请求。可以看到子进程ps 17171
拥有和bash
相同的sid
和tty
- 当
session 17135
的session leader也就是bash 17135
退出的时候,在此session下的所有进程也会收到hup(挂断信号)而终止退出
因此,偷懒的守护进程代码是这样:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
void ChildProcess(void); /* child process prototype */
void ParentProcess(void); /* parent process prototype */
void main(void)
{
pid_t pid;
signal(SIGCHLD,SIG_IGN);
pid = fork();
if (pid == 0)
{
setsid(); #守护进程建立新的session
ChildProcess();
}
else
exit();
}
首先可以发现本段代码相比父子进程模型多了setsid()
,它的作用是建立新的session,sid就是当前进程的pid。当守护进程属于一个新的session以后,就会脱离父进程session的生命周期和tty,也就是说,daemon将不会因为父session关闭而被挂断,也不会将父tty作为标准输入输出。
然后,为什么这是偷懒的代码呢,因为严谨的daemon需要两次fork来产生。下面描述完进程组的以后会给出标准daemon创建例程。
Process Group
进程组的存在是为了方便对一组进程进行统一的管理。比如我们可以通过killpg
向进程组传递信号量。或者方便利用job controll来进行前后台进程组的切换。
现在进程组的概念仍然比较抽象,以后有机会学到容器的虚拟化机理,应该能够深刻体会进程组的作用。
下面是double fork的示例代码:
fork();
if (pid == 0) {
setsid();
fork();
if (pid == 0) {
childProcess();
}
}
exit();
double fork的原因主要有:
- 如果只有一次fork,假如父进程并没有退出,而是需要继续运行其他代码,那么子进程daemon就不会被过继给
init
。在两次fork的情况下,第一个child process的作用只是用来产生daemon进程,fork
完称以后可以直接exit
,这样就能显式保证daemon在创建完成后就会被过继给init
,而祖父进程仍然可以继续执行其他逻辑 - 如果只有一次fork,那么daemon因为
setsid
,所以会是session leader。这意味着daemon将有权限将这个新创建的session绑定到tty上。而两次fork产生的daemon,因为第二个child process并没有setsid,所以并不是session leader,从而杜绝了这种情况发生 (这种说法来自stackoverflow,我目前并没有验证过)