1. 终端
在Linux系统中,用户通过终端登录系统后得到一个Shell进程,这个终端成为Shell进程的控制终端(Controlling Terminal),Shell进程启动的其他进程的控制终端也是这个终端。默认情况下(没有重定向),每个进程的标准输入、标准输出和标准错误输出都指向控制终端,进程从标准输入读也就是读用户的键盘输入,进程往标准输出或标准错误输出写也就是输出到显示器上。此外在控制终端输入一些特殊的控制键可以给前台进程发信号,例如Ctrl+C表示SIGINT,Ctrl+\表示SIGQUIT。
2. 会话与进程组
每次用户登录终端时会产生一个会话(session)。从用户登录开始到用户退出为止,这段时间内在该终端执行的进程都属于这一个会话。
每个进程除了有一进程ID之外,还属于一个进程组(Process Group)。进程组是一个或多个进程的集合,每个进程组有一个唯一的进程组ID。多个进程属于进程组的情况是多个进程用管道“|”号连接进行执行。如果在命令行执行单个进程时这个进程组只有这一个进程。
3. 控制终端
在终端(包括telnet等伪终端)登录就会产生一个会话,此会话拥有这一个单独的控制终端。
建立与控制终端连接的会话首进程,被称之为控制进程,也就是Shell进程。
一个会话中的几个进程组可被分成一个前台进程组以及一个或几个后台进程组。
4. 作业控制
前后台运行的进程组又称为作业(Job),一个前台作业可以由多个进程组成,一个后台作业也可以由多个进程组成,Shell进程可以同时运行一个前台作业和任意多个后台作业,这称为作业控制(Job Control)。
作业控制允许在一个终端上起动多个作业(进程组),控制哪一个作业可以存取该终端,以及哪些作业在后台运行。
从Shell进程使用作业控制功能角度观察,可以在前台启动一个作业或后台启动多个作业。一个作业只是几个进程的集合,通常用管道连接各进程。
例如下面的命令在后台(&表示在后台运行)启动了两个作业,这两个后台作业所调用的进程都在后台运行。
cat *.c | pg &
make all &
实际上有三个特殊字符可使终端驱动程序产生信号,信号将送至前台进程组的所有进程,而后台进程组作业则不受影响,它们是:
① 中断字符(DELETE或Ctrl-C)产生SIGINT信号。
② 退出字符(Ctrl-\)产生SIGQUIT信号。
③ 挂起字符(Ctrl-Z)产生SIGTSTP信号。
5. 会话与进程组的关系
例如用以下命令启动5个进程。
$ proc1 | proc2 &
$ proc3 | proc4 | proc5
其中proc1和proc2属于同一个后台进程组,proc3、proc4、proc5属于同一个前台进程组,Shell进程本身属于一个单独的进程组。这些进程组的控制终端相同,它们属于同一个session。其进程、进程组、session的关系如下图12-12所示。
图12-12 会话与进程组关系图
现在从session和进程组的角度重新来看登录和执行命令的过程。在上面的例子中,proc3、proc4、proc5被Shell放到同一个前台进程组,其中proc3进程是该进程组的组长,Shell进程调用wait等待它们运行结束,一旦它们全部运行结束,Shell进程就调用tcsetpgrp函数将自己提到前台继续接收命令。但是注意,如果proc3、proc4、proc5中的某个进程又fork出子进程,子进程也属于同一进程组,但是Shell进程并不知道子进程的存在,也不会调用wait等待它结束。换句话说,proc3 | proc4 | proc5是Shell进程的作业,而这个子进程不是,这是作业和进程组在概念上的区别。一旦作业运行结束,Shell进程就把自己提到前台。
6. 进程组和会话实例说明
(1)进程组与会话实例一
$ ps -o pid,ppid,pgrp,session,tpgid,comm | cat
PID PPID PGRP SESS TPGID COMMAND
6994 6989 6994 6994 8762 bash
8762 6994 8762 6994 8762 ps
8763 6994 8762 6994 8762 cat
这个作业由ps和cat两个进程组成,在前台运行。从PPID列可以看出这两个进程的父进程是bash;从PGRP列可以看出,bash在进程组ID为6994的进程组中,这个进程组ID等于bash的进程ID,所以它是进程组的组长;而两个子进程在8762的进程组中,ps是这个进程组的组长;从SESS列可以看出这三个进程都在同一session中,会话ID为6994,bash是会话首进程;从TPGID列可以看出,前台进程组ID是8762,也就是ps和cat这两个进程所在的进程组,其中ps进程为进程组的组长。
(2)进程组与会话实例二
$ ps -o pid,ppid,pgrp,session,tpgid,comm | cat &
[1] 8835
$ PID PPID PGRP SESS TPGID COMMAND
6994 6989 6994 6994 6994 bash
8834 6994 8834 6994 6994 ps
8835 6994 8834 6994 6994 cat
这个作业由ps和cat两个进程组成,在后台运行。bash不等作业结束就打印提示信息“[1] 8835”,然后给出提示符接受新的命令,“1”是作业的编号,如果同时运行多个作业可以用这个编号区分,“8835”是该作业中某个进程的进程ID。
7. 进程组函数
(1)进程组函数说明
每个进程组有一个组长进程,组长进程ID等于其进程组ID。只要在某个进程组中有一个进程存在,则该进程组就存在,这与其组长进程是否终止无关。从进程组创建开始到其中最后一个进程离开为止的时间区间称为进程组的生命期。
(2)取进程组ID函数原型
getpgrp(取得进程组识别码)
所需头文件 |
#include <unistd.h> |
函数说明 |
getpgrp()用来取得目前进程所属的组识别码。此函数相当于调用getpgid(0) |
函数原型 |
pid_t getpgrp(void) |
函数返回值 |
返回目前进程所属的组识别码 |
(3)设置进程组ID函数原型
setpgrp(设置进程组识别码)
所需头文件 |
#include <unistd.h> |
函数说明 |
setpgid()将参数pid 指定进程所属的组识别码设为参数pgid 指定的组识别码。如果参数pid 为0,则会用来设置目前进程的组识别码,如果参数pgid为0,则会以目前进程的进程识别码来取代 |
函数原型 |
int setpgid(pid_t pid,pid_t pgid) |
函数返回值 |
执行成功则返回组识别码,如果有错误则返回-1,错误原因存于errno中 |
错误代码 |
EINVAL:参数pgid小于0 EPERM:进程权限不足,无法完成调用 ESRCH:找不到符合参数pid指定的进程 |
进程调用setpgid(pid, pgid)可以参加一个现存的组或者创建一个新进程组,这时pid进程的进程组ID设置为pgid。如果这两个参数相等,则pid指定的进程变成进程组组长。
一个进程只能为它自己或它的子进程设置进程组ID。在它的子进程调用了 exec后,它就不再能改变该子进程的进程组ID。如果设置的pid等于0,则使用调用者的进程ID;如果设置pgid等于0,则使用pid进程的进程组ID。
在大多数作业控制中,在fork之后调用此函数,使父进程设置其子进程的进程组ID,然后使子进程设置其自己的进程组ID。这些调用中有一个是冗余的,但这样做可以保证父、子进程在进一步操作之前,子进程都进入了该进程组。如果不这样做的话,那么就产生一个竞态条件,因为它依赖于哪一个进程先执行。
在发送一个信号时,信号可以发送给一个进程或送给一个进程组。
8. 会话函数
(1)会话函数原型
setsid(创建一个新的会话)
所需头文件 |
#include <sys/types.h> #include <unistd.h> |
函数说明 |
创建一个新的会话 |
函数原型 |
pid_t setsid(void) |
函数返回值 |
若成功则为进程组 ID,若出错则为-1 |
(2)会话说明
用户每次用户登录会产生一个会话。如果调用setsid此函数的进程不是一个进程组的组长,则此函数会创建一个新会话,所产生的结果如下:
此进程变成该新会话的会话首进程(会话首进程是创建该会话的进程),此进程是该新会话中的唯一进程。
此进程成为一个新进程组的组长进程,新进程组ID是此进程的进程ID。
此进程没有控制终端,如果在调用setsid之前,此进程有一个控制终端,那么这种联系也被解除。
如果此调用进程已经是一个进程组的组长,则此函数返回出错。为了保证不处于这种情况,通常先调用fork,然后使其父进程终止,而子进程则继续。因为子进程继承了父进程的进程组ID,而其进程ID则是新分配的,两者不可能相等,所以这就保证了子进程不是一个进程组的组长。
(3)setsid函数的作用如下
① 让进程摆脱原会话的控制。
② 让进程摆脱原进程组的控制。
③ 让进程摆脱原控制终端的控制。
9. 孤儿进程
一个父进程已终止的进程称为孤儿进程 (orphan process),这种进程由1号进程init收养。
10. 僵尸进程
(1)僵尸进程的产生
一个进程在调用exit函数结束自己生命的时候,其实它并没有真正的被完全销毁,而是留下一个称为僵尸进程(Zombie)的数据结构。
在Linux进程的状态中,僵尸进程是非常特殊的一种,它已经释放了几乎所有内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个表项,记载该进程的退出状态等信息供其他进程收集。除此之外,僵尸进程不再占有任何内存空间。它需要它的父进程来为它收尸,如果它的父进程没有设置SIGCHLD信号处理函数、或者没有设置SIGCHLD信号为忽略(SIG_IGN)、或者没有调用wait(或waitpid)等待子进程结束,那么它就一直保持僵尸状态。如果这时父进程结束了,那么init进程会自动接手这个子进程,僵尸进程消失。但是如果如果父进程是一个循环,不会结束,那么子进程就会一直保持僵尸状态,这就是为什么系统中有时会有很多的僵尸进程。
(2)怎样来清除僵尸进程
清除僵尸进程有三种处理方法,具体说明如下:
改写父进程,在子进程死后为它收尸。具体做法是接管SIGCHLD信号,子进程死后,会发送SIGCHLD信号给父进程,父进程调用wait(或waitpid)函数为子进程收尸。
在父进程中设置SIGCHLD信号处理函数或者设置SIGCHLD信号为忽略(SIG_IGN)。
把父进程杀掉,父进程死后,僵尸进程成为“孤儿进程”,过继给1号进程init,init始终会负责清理僵尸进程,它产生的所有僵尸进程也跟着消失。