
进程组(process group)
进程组顾名思义是指一个或多个进程的集合。他们通常与同一个job(可以从同一个终端接收信号)相关联。每个进程组拥有一个唯一的Process Group Id。可以使用getpgrp或getpgid获取进程的Process Group Id:
#include <unistd.h> /*
返回调用进程的进程组Id
*/
pid_t getpgrp(void); /*
若pid为0,作用与getpgrp相同
*/
pid_t getpgid(pid_t pid);
每个进程组可以有一个进程组leader,leader的进程Id与其进程组id相同。无论进程组leader是否终止,只要进程组中存在未终止的进程这个进程组就不会消失。进程组leader终止后进程组中便不存在进程组leader,它不会自动推选新的进程组leader。参见:Must a process group have a running leader process?
可以通过setpgid函数设置进程组Id:
#include <unistd.h> /*
设置pid所在进程组Id为pgid
*/
int setpgid(pid_t pid, pid_t pgid);
调用进程只能设置他自己和他的子进程的Process Group Id,并且如果他的子进程调用了exec类函数,那么调用进程也无法更改它的这个子进程的Process Group Id。在 job-control shells中,这个方法常用于fork调用后父进程来设置子进程的Process Group Id,或者子进程设置它自己的Process Group Id。当然,fork调用后子进程是会继承父进程的Process Group Id的,fork后setpgid目的是为了确保子进程的Process Group Id而避免因为并发竟态导致的意外(这个地方不太懂,没构建出作者设想的竟态环境)。
会话(session)
会话(session)指一个或多个进程组的集合。
上图所示的session结构可以使用shell中的pipeline来表示:
proc1 | proc2 &
proc3 | proc4 | proc5
在上面示例中,login shell所在的进程组和proc1、proc2所在的进程组为后台进程组, proc3,proc4,proc5所在的进程组为前台进程组。一个会话中只能有一个前台进程组,可以有一个或多个后台进程组。关于前台进程组与后台进程组我们在后面介绍。
进程可以通过setsid来创建新的session。
#include <unistd.h> /*
成功,返回进程组ID,否则返回 -1
*/
pid_t setsid(void);
setsid函数成功时返回的是调用进程的Process Group Id,也是调用进程的Process Id, 因为session leader 永远是它所在的进程组leader。其实UNIX中是没有类似 Process Id 或 Process Group Id 的 "Session Id" 这个东西的,有的仅仅是"Session leader"。 我们可以认为 session leader 的 Process Group Id 或 Process Id为其所在 session 的 Session Id。
- 调用进程成为新session的session leader(A session leader is the process that creates a session)。此时此进程是新session中的唯一进程。
- 调用进程成为新进程组的leader。新进程组的Process Group ID 等于 调用进程的 Process ID。
- 调用进程不会再有控制终端。如果调用进程在调用setsid前拥有控制终端的话,那么调用setsid后他将断开与其控制终端的联系。
#include <unistd.h> /*
获取pid所在session的session leader
的Process Group ID
*/
pid_t getsid(pid_t pid);
控制终端(controlling terminal)
- 一个session可以拥有一个控制终端(当然也可以没有)。他通常是我们登录时的终端设备或伪终端。
- 创建与控制终端的链接的session leader被称为控制进程 (Controlling Process)。
- 一个会话中的进程组可以划分为一个前台进程组和一个或多个后台进程组。
- 如果一个会话拥有控制终端,那么它有一个前台进进程组,这个会话中的其他进程组都是后台进程组。
- 无论何时我们按下终端的中止键(通常是 DELETE或Crtl-C),就会有一个中止信号发送给前台进程组中的所有进程。
- 无论何时我们按下终端的退出键(通常是Crtl-Backslash),就会有一个退出信号发送给前台进程组中的所有进程。
- 如果终端接口检测到网络断开,那么hang-up信号就会发送给控制进程,即the session leader。

一般我们不必关心控制终端,它在我们登录时自动被创建。有事程序需要与控制终端通信,无论是使用标准输出还是标准输入重定向。程序保证与控制终端进行通信的方法时读写 /dev/tty文件。这个特殊文件在内核中是控制终端的代名词。如果程序没有控制终端,那么它打开/dev/tty文件将会失败。
可以使用tcgetpgrp函数获取前台进程组Id,使用tcsetpgrp设置前台进程组:
#include <unistd.h> /*
通过打开终端的文件描述符获取前台进程组Id
*/
pid_t tcgetpgrp(int fd); /*
pgrpid 必须是相同session中的一个进程组Id
*/
int tcsetpgrp(int fd, pid_t pgrpid);
关于session、process group、controlling terminal的更多信息 , 参见:The controlling-terminal and process-groups.
Job Control
- shell需要支持job control
- 内核中的终端驱动必须支持job control
- 内核必须支持确定的job-control信号
# 在前台启动包含一个进程的job
vi main.c
# 两个后台jobs调用的所有进程都是后台进程
pr *.c | lpr &
make all & # 当我们启动一个后台job时,shell会给这个job分配一个job id,
# 并打印这个job中的一个或多个进程IDs
$ make all > Make.out &
[]
$ pr *.c | lpr &
[]
- 中止字符(通常是DELETE或Crtl-C)产生SIGINT
- 退出字符(通常是Crtl-Backslash)产生SIGQUIT
- 暂停字符(通常是Crtl-Z)产生SIGSTP
如前所述,只有前台job会接收到终端输入,但是后台job尝试读取终端并不是错误的,终端驱动会检测后台进程的这种举动并向后台job发送一个特殊的信号:SIGTTIN。SIGTTIN通常会中止后台job。
总结