进程组和会话 是为支持shell 作业控制而定义的抽象概念。
进程组:由一个或多个 进程组成,它们有同样的进程组标识符(PGID)。进程组ID 是一个数字,其类型与进程ID 一样(pid_t)。一个进程组 拥有 一个 process group leader 进程 , 该进程是创建该组的进程,其进程ID为 该进程组的ID,新进程会继承其父进程 所属的进程组ID。
进程组拥有一个 生命周期(lifetime), 其开始时间为 leader进程 创建组的时刻, 结束时间为 最后一个成员退出组的时刻。一个进程可能会因为 加入了另外一个进程组而退出进程组 或因为终止而退出进程组。进程组leader进程 不需要是 最后一个离开进程组的。
会话是一组进程组的集合。会话中的所有进程 有相同的 会话标识符(SID), 会话标识符 是 pid_t 类型 的数字。会话leader进程是创建该新会话的进程,其进程ID会成为会话ID。新进程会继承父进程的会话ID。
一个会话中的所有进程共享单个控制终端。 控制终端在 会话leader进程首次打开一个终端设备时被建立。 一个终端 最多 可能会成为 一个会话的控制终端。
在任一时刻,会话中的其中一个进程组会成为终端的前台进程组,其它进程组会成为后台进程组。只有前台进程组中的进程才能从控制终端中读取输入。当用户在控制终端中输入其中一个信号生成终端字符之后,该信号会被发送到前台进程组中的所有成员。这些字符包括生成SIGINT的中断字符(通常是Control-C), 生成SIGQUIT 的退出字符(通常是Control-\), 生成SIGSTP的挂起字符(通常是(Control-Z)。
图34.1 给出了执行下面的命令之后各个进程之间的进程组和会话关系。
每个进程 都有一个进程组ID. 一个进程可以获取 它的进程组ID,通过使用getpgrp()
如果getpgrp() 返回的值和 调用者自己的 进程ID 相等, 那么这个进程 就是 它的进程组的 leader.
setpgid() 系统调用 可以改变 某个进程的 进程组ID
说明 如果pid 被设置为 0, 则调用者的进程组ID 被改变。 如果pgid 被指定为0,则 由pid参数指定的进程 的进程组ID 会变的 和它的进程ID 一样。因此 如下的 setpgid() 是等效的。
调用setpgid() 时有几个约束
pid 参数 可以 指定为 调用进程的 或者 它的子进程的 pid. 违反这条规则 会导致 ESRCH
调用进程 ,和 pid 指定的进程(可能时同一个),和 目标的进程组 必须是 处于相同的会话 (session)。违反这条规则会导致 EPERM
pid 的 值 不能是 会话的leader 进程的pid值。违反这条规则 导致 EPERM
一个进程在其子进程已经执行exec()后就无法修改子进程的进程组ID了。违反这条规则会导致 EACCESS错误。之所以会有这条约束是因为:在一个进程开始执行之后,再修改其进程组ID的话 会使程序变的混乱。
由于一个进程在其子进程已经执行exec()之后就无法修改该子进程的进程组ID 的约束条件。所以基于shell 的作业控制程序设计,要满足如下条件
1. 一个任务(一个命令或一组以管道符连接的命令)中的所有进程必须放置在一个进程组中。
2. 每个子进程在执行程序之前必须要被分配到进程组中,因为程序本身是不清楚如何操作进程组ID的。
pid_t childPid;
pid_t pipelinePgid; /* PGID to which processes in a pipeline
are to be assigned */
/* Other code */
childPid = fork();
switch (childPid) {
case -1: /* fork() failed */
/* Handle error */
case 0: /* Child */
if (setpgid(0, pipelinePgid) == -1)
/* Handle error */
/* Child carries on to exec the required program */
default: /* Parent (shell) */
if (setpgid(childPid, pipelinePgid) == -1 && errno != EACCES)
/* Handle error */
/* Parent carries on to do other things */
}
获取和修改 进程组ID 的其他(过时的)接口。
setpgrp(pid, pgid); //它能将将进程组ID 设置为任意值。这会引起安全问题。
getpgid(pid); //
34.3 会话(Sessions)
The getsid() 系统调用 返回 由pid 指定的 的进程的 会话ID。
如果 pid 被指定为0, getsid() 返回调用进程 的 会话ID。
如果调用进程不是 进程组首进程(leader),那么setsid() 会创建一个新会话。
setsid()系统调用 创建一个 新的会话 如下:
调用者进程 变成 新会话的 leader进程, 也是 新进程组的 leader进程。 调用进程的的 进程组ID 和 会话ID 被设置为 和它的进程ID相同。
调用者进程 没有 控制终端。 所有之前到控制终端的连接都会被断开。
如果调用进程 是一个进程组的leader进程,那么setsid()调用就会报出EPERM错误。避免这个错误发生的最简单的方式是执行一个fork() 并让父进程终止以及 让子进程调用setsid()。由于子进程会继承父进程的进程组ID 并接收属于自己的唯一进程ID.
约束进程组首进程对setsid()的调用是有必要的。因为如果没有这个约束的话,进程组组长就能将其自身迁移到另一个新的会话中了, 这会破坏会话和进程组之间严格的两级层级,因此一个进程组的所有成员必须属于同一个会话。
Listing 34-2 演示了setsid() 的使用 ,为了检查 它不再有 控制终端, 这个程序尝试打开 file /dev/tty .当运行此程序时,我们看到如下:
––––––––––––––––––––––––––––––––––––––––––––––––––––––––– pgsjc/t_setsid.c
#define _XOPEN_SOURCE 500
#include
#include
#include "tlpi_hdr.h"
int
main(int argc, char *argv[])
{
if (fork() != 0) /* Exit if parent, or on error */
_exit(EXIT_SUCCESS);
if (setsid() == -1)
errExit("setsid");
printf("PID=%ld, PGID=%ld, SID=%ld\n", (long) getpid(),
(long) getpgrp(), (long) getsid(0));
if (open("/dev/tty", O_RDWR) == -1)
errExit("open /dev/tty");
exit(EXIT_SUCCESS);
}
–––––––––––––––––––––––––––––––––––––––––––––––––––––––––