进程关系
- 一、进程组
- 二、会话
- 三、控制终端
- 四、函数tcgetpgrp、tcsetpgrp和tcgetsid
- 五、作业控制
- 五、孤儿进程组
一、进程组
每个进程除了有一进程ID之外,还属于一个进程组,进程组是一个或多个进程的集合。同一进程组中的各进程接收来自同一终端的各 种信号。每个进程组有一个唯一的进程组ID。类似进程ID是个整数,存放pid_t数据类型。
#include <unistd.h>pid_t getpgrp(void); //返回值:调用进程的进程组ID
//带参数形式:
#include<unistd.h>
pid_t getgid(pid_t pid);
//pid为0,返回调用进程的进程组ID
getpgid(0);
//等价于
getpgrp();
-
每个进程组有一个组长进程。组长进程的进程组ID等于其进程ID。
-
进程组组长可以创建一个进程组、创建该组中的进程,然后终止。
-
只要在某个进程组中有一个进程存在,则该进程组就存在,这与其组长进程是否终止无关。
-
从进程组创建开始到其中最后一个进程离开为止的时间区间称为进程组 的生命期。
-
某个进程组中的最后一个进程可以终止,也可以转移到另一个进程 组。
//加入一个现有的进程组或者创建一个新进程组
int setpgid(pid_t pid, pid_t pgid);
//setpgid函数将pid进程的进程组ID设置为pgid。
//如果这两个参数相等,则由pid指定的进程变成进程组组长。
//pid是0,则使用调用者的进程ID。
//果pgid是0,则由pid指定的进程ID用作进程组ID。
//返回值:若成功,返回0;若出错,返回−1
-
一个进程只能为它自己或它的子进程设置进程组ID。在它的子进程调用了
exec
后,它就不再更改该子进程的进程组ID。 -
在fork之后调用此函数,使父进程设置其子进程的进程组ID,并且也使子进程设置其自己的进程组ID。这两个调用中有个是 冗余的,但让父进程和子进程都这样做可以保证,在父进程和子进程认为子进 程已进入了该进程组之前,这确实已经发生了。
-
不这样做,在fork之后,由 于父进程和子进程运行的先后次序不确定,会因为子进程的组员身份取决于哪 个进程首先执行而产生竞争条件。
二、会话
会话是一个或多个进程组的集合,如下图所示一个会话中有3个进程组。
shell的管道将几个进程编程一组,上图可以有shell命令形成的:
procl | proc2 &
proc3 | proc4 | proc5
//建立一个新会话。
#include <unistd.h>
pid_t setsid(void);
//返回值:若成功,返回进程组ID;若出错,返回-1
调用此函数的进程不是一个进程组的组长,则此函数创建一个新会 话。具体会发生以下3件事。
- 该进程变成新会话的会话首进程(session leader,会话首进程是创建 该会话的进程)。此时,该进程是新会话中的唯一进程。
- 该进程成为一个新进程组的组长进程。新进程组ID是该调用进程的进 程ID。
- 该进程没有控制终端。如果在调用 setsid 之 前该进程有一个控制终端,那么这种联系也被切断。
-
如果该调用进程已经是一个进程组的组长,则此函数返回出错。
-
通常先调用fork,然后使其父进程终止,而子进程则继续。
- 因为子进程继承了父进程的进程组ID,而其进程ID则是新分配的,两者不可能相等,这就保证了子进程不是一个进程组的组长。
-
//getsid函数返回会话首进程的进程组ID
#include <unistd.h>
pid_t getsid(pid_t pid);
//若pid是0,getsid返回调用进程的会话首进程的进程组ID。
//若pid并不属于调用者所在的会话,那么调用进程就不能得到该会话首进程的进程组ID。
//返回值:若成功,返回会话首进程的进程组ID;若出错,返回-1
三、控制终端
会话和进程组还有一些其他特性
-
一个会话可以有一个控制终端。这通常是终端设备(在终端登录情况下)或伪终端设备(在网络登录情况下)。
-
建立与控制终端连接的会话首进程被称为控制进程。
-
一个会话中的几个进程组可被分成一个前台进程组以及一个或多个后台进程组。
-
如果一个会话有一个控制终端,则它有一个前台进程组,其他进程组为后 台进程组。
-
无论何时键入终端的中断键(常常是Delete或Ctrl+C),都会将中断信号 发送至前台进程组的所有进程。
-
无论何时键入终端的退出键(常常是Ctrl+\),都会将退出信号发送至前 台进程组的所有进程。
-
如果终端接口检测到调制解调器(或网络)已经断开连接,则将挂断信号 发送至控制进程(会话首进程)。
通常,我们不必担心控制终端,登录时,将自动建立控制终端,4个平台分配控制终端的方式:
-
不管标准输入、标准输出是否重定向,程序都要与控制终端交互作 用。
-
保证程序能与控制终端对话的方法是 open 文件/dev/tty。
-
程序没有控制终端,则对于此设备的 open将失败。
四、函数tcgetpgrp、tcsetpgrp和tcgetsid
//通知内核哪一个进程组是前台进程组
#include <unistd.h>
pid_t tcgetpgrp(int fd);
//fd必须引用该会话的控制终端。
//函数tcgetpgrp返回前台进程组ID,它与在fd上打开的终端相关联。
//返回值:若成功,返回前台进程组ID;若出错,返回−1int tcsetpgrp(int fd, pid_t pgrpid); //进程有控制终端,则该进程可以调用tcsetpgrp将前台进程组ID设 置为pgrpid。//pgrpid值应当是在同一会话中的一个进程组的ID。// 返回值:若成功,返回0;若出错,返回−1
//给出控制TTY的文件描述符,通过tcgetsid函数,应用程序就能获得会话首进程的进程组ID。
#include <termios.h>pid_t tcgetsid(int fd);//返回值:若成功,返回会话首进程的进程组ID;若出错,返回−1//要管理控制终端的应用程序可以调用 tcgetsid 函数识别出控制终端的会话 首进程的会话ID(它等价于会话首进程的进程组ID)。
五、作业控制
作业控制,允许在一个终端上启动 多个作业(进程组),它控制哪一个作业可以访问该终端以及哪些作业在后台 运行。作业控制要求以下3种形式的支持。
-
支持作业控制的shell。
-
内核中的终端驱动程序必须支持作业控制。
-
内核必须提供对某些作业控制信号的支持。
如下图所示作业控制的功能,穿过终端驱动程序框的实线表明终端 I/O 和终端产生的信号总是从前台进程组连接到实际终端。对 应于 SIGTTOU
信号的虚线表明后台进程组进程的输出是否出现在终端是可选 择的。
五、孤儿进程组
-
父进程已终止的进程称为孤儿进程,这种进程由init进程“收养”。整个进程组也可成为“孤儿”。
-
进程fork了一个子进程然后终止,但是在父进程终止时,该子进程停止(用作业控制)又将如何呢?子进程如何继续,以及子进程是否知道它已经是孤儿进程?
下图显示:父进程已经fork了子进程,该子进程停止,父进程则将退出。
程序如下:
#include "apue.h"
#include <errno.h>static void sig_hup(int signo)
{printf("SIGHUP received, pid = %ld\n", (long)getpid());
}static void pr_ids(char *name)
{printf("%s: pid = %ld, ppid = %ld, pgrp = %ld, tpgrp = %ld\n",name, (long)getpid(), (long)getppid(), (long)getpgrp(),(long)tcgetpgrp(STDIN_FILENO));fflush(stdout);
}int main(void)
{char c;pid_t pid;pr_ids("parent");if ((pid = fork()) < 0) {err_sys("fork error");} else if (pid > 0) { /* parent */sleep(5); // 父进程睡眠5秒,这是一种让子进程在父进程终止之前运行的一种权宜之计 } else { /* child */pr_ids("child");//子进程为挂断信号(SIGHUP)建立信号处理程序。//观察到 SIGHUP信号是否已发送给子进程signal(SIGHUP, sig_hup); //建立信号处理程序
//子进程用kill函数向其自身发送停止信号(SIGTSTP)。kill(getpid(), SIGTSTP); //停止pr_ids("child"); //只有当我们继续时才打印if (read(STDIN_FILENO, &c, 1) != 1)printf("read error %d on controlling TTY\n", errno);}//当父进程终止时,该子进程成为孤儿进程,所以其父进程ID成为1,也就是init进程ID。//现在,子进程成为一个孤儿进程组的成员exit(0);
}
假定使用了一个作业控制 shell,shell 将前台进程放在它 (指前台进程)自已的进程组中(本例中是6099),shell则留在自己的进程组内(2837)。子进程继承其父进程(6099)的进程组。在fork之后:
父进程终止后,进程组包含一个停止的进程,进程组成为孤儿进程组,POSIX.1要求向新孤儿进程组中处于停止状态的每一个进程发送挂断信号 (SIGHUP),接着又向其发送继续信号(SIGCONT)。在处理了挂断信号后,子进程继续。
对挂断信号的系统默认动作是终止该进程,为此必须提供一个信号处理程序以捕捉该信号。因此,我们期望sig_hup 函数中的printf会在pr_ids函数中的printf之前执行。
POSIX.1将孤儿进程组定义为:
-
该组中每个成员的父进程要么是该组的一个 成员,要么不是该组所属会话的成员。
-
一 个进程组不是孤儿进程组的条件是——该组中有一个进程,其父进程在属于同 一会话的另一个组中。如果进程组不是孤儿进程组,那么在属于同一会话的另 一个组中的父进程就有机会重新启动该组中停止的进程。
-
进程组中每 一个进程的父进程(例如,进程6100的父进程是进程1)都属于另一个会话,所以此进程组是孤儿进程组。