第九章〓进程关系
91〓引言
在上一章我们已了解到进程之间具有关系。首先,每个进程有一个父进程。当子进
程终止时
,父进程会得到通知并能取得子进程退出状态。在86节说明waitpicl函数时,我
们也提到
了进程组,以及如何等待进程组中的任一个进程终止。
本章的更详细地说明进程组以及POSIX1引进的对话期新概念。我们也将介绍log
in shell(
是我们登录时为我们调用的)和所有从login shell起动的进程之间的关系。
在说明这些关系时不可能不谈及信号,而谈论信号又需要很多本章介绍的概念。如
果你不熟
悉Unix信号,则可能先要浏览一下第十章。
92〓终端登录
先看一看登录到Unix系统时所执行的各个程序。在早期的Unix系统中,例如Versi
on7,用户
用哑终端(通过RS-232连到主机)进行登录。终端或者是在地的(直接连接)或者是远
程的(通
过调制解调器连接)。在这两种情况下,login都经由系统核中的终端设备驱动程序
。例如,
在PDP-11上常用的设备是DH-11和DZ-11。因为连到主机上的终端设备数已经确定,
所以同时
的
login数也就有了已知的上限。下面说明的登录过程适用于使用一个RS-232终端登
录到Unix
系统中。
43+BSD终端登录
登录过程在过去十五年中并没有多少改变。系统管理者创建一个通常名为/etc/
thys的文
件
,其中,每个终端设备有一行。每一行说明设备名和传到getty程序的参数。这些
参数说明
了终端的波特率等。当系统自举时,系统核创建进程ID1,也就是init进程。init
进程供系
统进入多用户状态。init读文件/etc/ttys,对每一个允许登录的终端设备,ini
t拥有一次
fork,它所生成的子进程则执行(exec)程序getty。这种情况示于图91中。
图91 init生成进程使终端可用于login
图91中,各个进程的实际用户ID和有效用户ID都是0(也就是它们都具有超级用户
特权)。i
nit以空环境exec getty程序。
getty对终端设备调用open画数。以读、写方式将终端打开。如果设备是调制解调
器,则Ope
n可能会在设备驱动程序中滞留,直到用户拨号调制解调器,并且线路被接通。一
旦设备被
打开,则文件描述符0,1,2就被设置到该设备。然后getty输出login之类的信息
,并等待
用户键入用户名。如果终端支持多种速度,则getty可以测试特殊字符以便适当地
更改终端
速度(波特率)。关于getty程序以及有关数据文件的细节,请参改Unix手册。
当用户键入了用户名后,getty就完成了。然后它以类似于下列的方式调用login程
序:
execle("/usr/bin/login","login""-p",username,(char*)O,enup);(在getty
tab文件
中可能会有一
些选择项使其调用其它程序,但系统默认是login程序。)init以一个空环境调用g
etty。get
ty以终端名(例如TERM=foo,其中终端foo的类型取自gettytab文件)和在gettytab中
的环境字
符串为login创建一个环境(enup参数)。-p标志通知login保留传给它的环境,也可
将其它环
境字符串加到该环境中,但是不要代换它。图92显示了在login刚被调用后这些
进程的状
态。
图92 login刚被调用后各进程的状态
因为init进程具有超级用户优先权,所以图92中的所有进程都有超级用户优先权
。图92
中底部三椎是一个进程,它们的进程ID和父进程ID都不会因执行exec而改变。
login的工作主要是:因为它得到了用户名,所以就能调用getpwnam取得相应用户
的口令字
文
件登记项。然后调用getpass(3)以显示提示Password;接着读用户键入的口令字(
自然,禁
止回适用户键入的口令字)。它调用crypt(3)将用户键入的口令字转换成密码,并
与该用户
口令字文件中的登记项的pw 迹茫模*常病絧asswd字段相比较。如果用户几次键入
的口令字
都无效,则lo
gin以参数1调用exit表示登录过程失败。父进程(init)了解到子进程的终止情况后
,将再次
调用fork,其后又跟随着exec getty,对此终端重复上述过程。
如果用户正确登录,login就将当前工作目录更改为该用户的起始目录(chdir)。它
也调用ch
own改变该终端的属主关系,使该用户成为属主和组属主。将对该终端设备的存取
许可权改
变成:用户读、写和组写。调用setgid及initgroups设置进程的组ID。然后用leg
in所得到
的所有信息初始化环境:起始目录(HOME)、shell(SHELL)、用户名(USER和LOGNAM
E)、以及
一个系统默认路径(PATH)。最后,login进程改变为登录用户的用户ID(setuid)并
调用该用
户的登录shell,其方式类似于:
execl("/bin/sh","-sh",(char *)o);
argv[o]的第一个字符'-'是一个标志,表示该shell被调用为登录shell。shel
l可以查
看此字符,并相应地修改其起动过程。
login所做的比上面说的要多。它可选地打印message-of-the-day文件,检查新邮
件以及其
它一些功能。但是考虑到本市的内容,我们主要关心上面所说的功能。
回忆在810节中对setuid函数的讨论,因为setuid是由超级用户调用的,它更改
所有三个
用户ID:实际、有效和保存的用户ID。login在较早时间调用的setgid对所有三个
组ID也有
同样效果。
到此为止,登录用户的login shell开始运行。其父进程ID是init进程ID(进程ID1
),所以当
此login shell终止时,init会得到通知(接到SIGCHLD信号),它会对该终端重复全
部上述过
程。登录shell的文件描述符0,1和2设置为终端设备。图93显示了这种安排。
图93终端登录结束后的有关进位
现在,login shell读其启动文件(Bourne shell和Korn shell是profile,C shell
是Cshrc和login)。这些启动文件通常改变某些环境变量,加上一些环境变量。例
如,很多用户设置他们自己的PATH,常常提示实际终端类型(TERM)。当执行完启
动文件后,用户最后得到shell的
提示符,并能键入命令。
SVR4终端登录
SVR4支持两种形成的终端登录:(a)getty方式,这与上面对43+BSD所说明的一
样,(b)tt
ymon登录,这是SVR4的一种新功能。通常,getty用于控制台,ttymon则用于其它
终端的登
录。
ttymon是名为SAF(Service Access Facility,服务存取设施)的一部分。按照本市
的目的,
我们只简单说明从init到login shell之间工作过程,最后结果与图93中所示相
似。init
是sac(服务存取控制器)的文进程,sac调用fork,然后其子进程exec ttymon程序
,此时系
统
进入多用户状态。ttymon监视列于配置文件中的所有终端端口,当用户键入login
名时,它
调用一次fork。在此之后该子进程又exec登录用户的login shell,于是到达了图
93中所
示的位置。一个区别是login shell的父进程现在是ttymon,而getty login中,l
ogin shel
l的父进程是init。
93〓网终登录
43+BSD网络登录
在上节所述的终端登录中,init知道哪些终端设备可用来进行登录,并为每个设备
生成一个g
etty进程。但是,对网络登录则情况有所不同,所有登录都绎由系统核的网络界面
驱动程序
(例如:以太网驱动程序),事先并不知道将会有多少这样的登录。不是使一个进程
等待每一
个可能的登录,而是必须等待一个网络连接请求的到达。在43+BSD中,有一
个称为in
etd的进程(有时称为Internet superserver),它等待大多数网络连接。在本市中
,我们将
说明43+BSD的网络登录中所涉及的进程序列)。关于这些进程的网络程序设计方
面的细节
请参阅Sterens[1990]。
作为系统起动的一部分,init调用一个shell执行shell脚本etc/rc。由此shell脚
本起动一
个精灵进程inetd。一旦此shell脚本终止,inetd的父进程就变成init。inetd等待
TCP/IP
连
接请求到达主机,而当一个连接请求到达时,它fork-子进程,然后该子进程exec
适当的程
序。
我们假定到达了一个对于TELNET服务器的一个TCP连接请求。TELNET是使用TCP协议
的远程登
录应用程序。在另一个主机(它通过某种形成的网络,连接到服务器主机上)上的用
户,或在
同一个主机上的一个用户籍起动TELNET客户进程(client)起动登录过程:
telnet hostnaml
该客户打开一个到名为hostname的主机的TCP连接,在hostname主机上起动的程序被
称为TELN
ET服务器。然后,客户进程和服务器进程之间使用TELNET应用协议通过TCP连接交
换数据。
所发生的是起动的用户现在登录到了服务器进程的主机(自然,用户需要在服务器
进程主机
上有一个有效的账号。)图94显示了在执行TELNET服务器进程(称为telnetd)中所
涉及的进
程序列。
然后,telnetd进程打开一个伪终端设备,并用fork生成一个子进程(在十九章中将
详细说明
伪终端。)父进程处理通过网络连接的通信,子进程则exec login程序。父子进程
通过伪终
端
相连接。在调用exec之前,子进程使其文件描述符0,1,2与伪终端相连。如果登
录正确,l
ogin就执行在92节中所述的同样步骤-更改当前工作目录为起始目录,设置登录
用户的组I
D和用户ID,以及登录用户的初始环境。然后login用exec将其自身代换为登录用户
的。图9
5显示了到达这一点时进程安装。
图94 执行TELNET服务者中涉及的进程序列
图95 为网络login设置了fd0,1,2后的进程安排
很明显,在伪终端设备驱动程序和终端实际用户之间有很多事情在进行看。在第十
九章中较
详细地说明伪终端之前,我们会介绍与这种安排相关的所有进程。
需要理解的重要之点是:当通过终端(图93)或网络(图95)登录时,我们得到一
个login
shell,其标准输入、输出和标准出错连接到或者一个终端设备上或者一个伪终端
设备上。
在下一节中我们会了解到这一login shell是一个POSIX1对话期的开始,而此终
端或伪终
端则是对话期的控制终端。
SVR4网络登录
SVR4中网络登录的情况与43+BSD中的几乎一样。同样使用了inetd服务器进程,
但是在SVR
4
中inetd是作为一种服务由服务存取控制器调用的,其父进程不是init。最后得到
的与图9
5中一样。
94〓进程组
每个进程除了有一进程ID之外,还属于一个进程组,在第十章讨论信号时还会涉及
进程组
。
一个进程组是一个或多个进程的集合。每个进程组有一个唯一的进程组ID。进程组
ID类似于
进程ID-它是一个正整数,并可存放在pid-t数据类型中。函数getpgrp返回调用进
程的进程
组ID。
#include <sys/typesh>
#include <unistdh>
pid 迹茫模*常病絫 getpgrp(void);
返回:调用进程的进程组ID
在很多贝壳莱类的系统中,包括43+BSD,这一函数的参数是pid,返回该进程的
进程组。
上面所示的原型号POSIX1版本。
每个进程组有一个组长进程。组长进程的标识是,其进程组ID等于其进程ID。
进程组组长可以创建一个进程组,创建该组中的进程,然后终止。只要在某个进程
组中有一
个进程存在,则该进程组就存在,这与其组长进程是否终止无关。从进程组创建开
始到其中
最后一个进程离开为止的时间区间称为进程组的生命期。在某个进程组中的最后一
个进程可
以终止,或者参加另一个进程组。
一个进程调用setpgid可以参加一个现存的组或者创建一个新进程组。(下一节中将
说明用se
tsid也可以创建一个新的进程组。)
#include<sys/typesh>
#include<unistdh>
int setpgid(pid 迹茫模*常病絫 pid,pid 迹茫模*常病絫 pgid);
返回:若成功为0,出错为-1
这将pid进程的进程组ID设置为pgid。如果这两个参数相等,则由pid指定的进程变
成进程组
组长。
一个进程只能为它自己或它的子进程设置进程组ID。在它的子进程调用了exec后,
它就不再
能改变该子进程的进程组ID。
如果pid是0,则使用调用者的进程ID。另外,如果pgid是0,则由pid指定的进程I
D被用作为
进程组ID。
如果系统不支持作业控制(在98节说明作业控制),那么就不定义-POSIX-JOB-CO
NTROL,在
这种情况下,此函数出错返回,errno设置为ENOSYS。
在大多数作业控制shell中,在fork之后调用此函数,使父进程设置其子进程的进
程组ID,
使子进程设置其自己的进程组ID。这些调用中有一个是冗余的,但这样做可以保证
父、
子进程在进一步操作之前,子进程都进入了该进程组。如果不这样做的话,那么就
产生一个
竞态条件,因为它依赖于那一个进程先执行。
在讨论信号时,我们将说明如何将一个信号送给一个进程(由其进程ID标识)或送给
一个进程
组(由进程组ID标识)。相类似,Waitpid则可被用来等待或者一个进程或者指定进
程组中的
一个进程。
95〓对话期
一个对话期是一个或多个进程组的集合。例如,可以有图96中所示的安排。其中
,在一个
对话期中有三个进程组。通常是由shell的管道线将几个进程编成一组的。例如,
图96中
的安排可能是由下列形式的shell命令形成的:
procl|proc2 &
proc3|proc4|proc5
图96 在进程组和对话期中的进程安排
一个进程调用setsicl函数就可建立一个新对话期。
#include<sys/typesh>
#include<unistdh>
pid 迹茫模*常病絫 setsid(void);
返回:若成功为进程组ID,出错为-1
如果调用此函数的进程不是一个进程组的组长,则此函数创建一个新对话期,它所
造成的结果是:
1此进程变成该新对话期的对话期首进程(对话期首进程是创建该对话期的进程)
。此进程
是该新对话期中的唯一一个进程。
2此进程成为一个新进程组的组长进程。新进程组ID是此调用进程的进程ID。
3此进程没有控制终端。(下一节讨论控制终端)。如果在调用setsid之前此进程
有一个控
制终端,那么这种联系也被解除。
如果此调用进程已经是一个进程组的组长,则此函数出错返回。为了保证不处于这
种情况,
通常的实践是先调用fork,然后使其父进程终止,而子进程则继续。因为子进程继
承了父进
程的进程组ID,而其进程ID则是新发现的,两者不可能相等,所以这就保证了子进
程不是一个进程组的组长。
POSIX1只说到对话期首进程。与进程ID和进程组ID不同,及有对话期ID。显然,
对话期首
进程是具有唯一进程ID的单个进程,所以我们可以将对话期首进程的进程ID视为对
话期ID。
SVR4就是这样处理的。SVID和SVR4的setsid(2)手册页谈到了以此种方式定义的对
话期ID。
这是一种实现细节,它不是POSIX1中定义的,43+BSD也不支持它。
SVR4有一个getsid函数,它返回一个进程的对话期ID。此函数不是POSIX1的所属
部分,4
3+BSD也不支持此函数。
96〓控制终端
对话期和进程组有一些其它特性:
·一个对话期可以有一个单独的控制终端。这通常是我们在其上登录的终端设备(
在终端登录情况)或伪终端设备(在网络登录情况)。
·建立与控制终端连接的对话期首进程,被称之为控制进程。
·一个对话期中的几个进程组可被分成一个前台进程组以及一个或几个后出进程组。
·如果一个对话期有一个控制终端,则它有一个前台进程组,其它进程组则为后台
进程组。
·无论何时键入中断键(常常是DELETE或Control-C)或退出键(常常是tonforl-\
),就会造
成将中断信号或退出信号送前台进程组的所有进程。
·如果终端界面检测到调制解调器已经脱开连接,则将挂断信号送至控制这些特性
示于图9
7中。
图97进程组、对话期和控制终端
通常,我们不必担心控制终端--当我们登录时,将自动为我们建立控制终端。
一个系统如何发现一个控制终端依赖于实现。194节中将说明实际步骤。
当对话期首进程打开第一个尚未与一个对话期相关联的终端设备时,SVR4将此作为
控制终端
分配给此对话期。这假定对话期首进程在调用open时及有指定O-NOCTTY标志(33
节)。
当对话期首进程以request参数为TIOCSCTTY调用ioctl时(第三个参数是空指针),
43+B
SD为对话期分配控制终端。为使此调用成功执行,此对话期不能已经有一个控制
终端。(
通常ioctl调用紧跟在setsid调用之后,setsid保证此进程是一个没有控制终端的
对话期首
进程。)43+BSD
不使用POSIX1中对open函数所说明的O 迹茫模*常病絅OCTTY标志。
有时不管标准输入,标准输出是否重新定向,程序都要与控制终端交互作用。保证
程序读写
控制终端的方法是打开文件/dev/tly,在系统核中,此特殊文件是控制终端的同
义语。自
然,如果程序没有控制终端,则打开此设备将失败。
典型的例子是读一个口令字的getpass(3)函数(自然,终端回送被关闭)。这一函数
由cryp
t(1)程序调用,而此程序则可用于管通线中。例如:
crypt <salaries | lpr
它将文件salaries解密,然后经由管道将输出送至打印假脱机程序。因为crypt从
其标准输
入读输入文件,所以标准输入不能用于输入口令字。但是,crypt的一个设计特征
是每次运
行此程序时,我们都应输入加密口令字,这样也就不需要将口令字存放在文件中。
已经知道有一些方法可以破译crypt程序使用的密码。关于密码文件的详细情况请
参见Garfi
nkel和Spafford [1991]。
97〓tcgetpgrp和tcsetpgrp函数
需要有一种方法用其可以通知系统核那一个进程组是前台进程组,这样,终端设备
驱动程序
就能了解将终端输入和终端产生的信号送到何处(图97)。
#include<sys/typesh>
#include<unistdh>
pid 迹茫模*常病絫 tcgetpgrp(int filedes);
Returns;process group ID 返回:若成功为前台进程组ID,出错为-1
返回:若成功为0,出错为-1
函数tcgetpgrp返回前台进程组ID,它与在filedes上打开的终端相关。
如果进程有一个控制终端,则该进程可以调用tcsetpgrp将前台进程组ID设置为pg
rpid。pgr
pid值应当是在同一回对话期中的一个进程组的ID。filedes必须引用该对话期的控
制终端。
大多数应用程序并不直接调用这两个函数。它们通常由作业控制shell调用。只有
定义了-PO
SIX-JOB-CONTROL,这两个函数才是被定义了的。否则它们出错返回。
98〓作业控制
作业控制是贝克莱在1980年左右加到UNIX的一个新特性。它允许我们在一个终端上
起动多个
作业(进程组),控制哪一个作业可以存取该终端,以及哪些作业在后台运行。作业
控制要求
三种形式的支持:
1支持作业控制的shell。
2系统核中的终端驱动程序必须支持作业控制。
3必须提供对某些作业控制信号的支持。
SVR3提供了一种不同形式的作业控制,称为shell层。但是POSIX1选择了贝克莱
形成的作
业控制,这也是我们在这里所说明的。回忆图27,如果系统支持作业控制,则定
义常数-P
OSIX-JOB-CONTROL。
FIPS151-要求POSIX1作业控制。
SVR4和43+BSD支持POSIX1作业控制。
从shell使用作业控制功能角度观察,我们可以在前台或后台启动一个作业。一个
作业只是
几个进程的集合,通常是一个进程管道。例如:
vi mainC
它在前台起动了只有一个进程组成的一个作业。下面的命令:
pr *C/lpr &
make all &
在后台起动了两个作业。这两个后台作业所调用的进程都在后台。
正如前述,我们需要一个支持作业控制的shell以使用由作业控制提供的功能。对
于较老的
系统,它们是否支持作业控制比较多于说明。C shell支持作业控制,Bourne she
ll则不支
持
,而Korn shell则是个选择项,它取决于宿主系统是否支持作业控制。但是现在C
shell已
被
移植到并不支持作业控制的系统上(例如系统V的早期版本),而SVR4 Bourne shel
l当用名字
jsh而不是sh调用时则支持作业控制。如果宿主系统支持作业控制,则Korn shell
继续支持
作业控制。在各种shell之间的差别并不显著时,我们将只是一般地读及支持作业
控制的she
ll和不支持作控制的shell。
当起动一个后台作业时,shell赋与它一个作业标识,并打印一个或几个进程ID。
下面的操
作过程显示了Korn shell如何处理这一点。
$ make all>Makeout &
[1] 1475
$ pr *c | lpr &
[2] 1490
$ 键入回车
[2]+Done pr *c | lpr &
[1]+Done make all>Makeout &
make是作业号1,所起动的进程ID是1475。下一个管道线是作业号2,其第一个进程
的进程ID
是1490。当作业已完成而且我们键入回车时,shell通知我们作业已经完成。我们
需要键入
回车的理由是:使shell打印其提示符。shell并不在任何随意的时间打印后台作业
的状态改
变-它只在打印其提示符之前这样做。如果不这样处理,则在我们正输入一行时,
它也可能
输出。
我们可以键入一个影响前台作业的特殊字符-挂起键(典型的是Control-Z)与终端进
行交互
作用。键入此字符使终端驱动程序将信号SIGTSTP送至前台进程组中的所有进程,
后台进程
组作业则不受影响。实际上有三个特殊字符可使终端驱动程序产生信号,并将它们
送至前台
进程组,它们是:
·中断字符(典型的是DELETE或Control-C)产生SIGINT。
·退出字符(典型的是Control-\)产生SIGQUIT。
·挂起字符(典型的是Control-Z)产生SIGTSTP。
在第十一章中将说明可将这三个字符更改为任意其它所选择的字符,以及如何使终
端驱动程序不处理这些特殊字符。
终端驱动程序必须处理与作业控制有关的另一种情况。我们可以有一个前台作业,
若干个后台作业,这些作中哪一个接收我们在终端上键入的字符呢?只有前台作业
接收终端输入。如果后台作业试图读终端,那么这并不是一个错误,但是终端驱动
程序检测这种情况,并且发送一个特定信号SIGTTIN给后台作业。这通常会停止此后
台作业,而有关用户则会得到这种情况的通知,然后我们就可将此作业转为前台
作业运行,于是它就可读终端。下列操作过程
显示了这一点:
$ cat>tempfoo & 在后台启动,但将从标准输入读
[1] 1681
$ 键入回车
[1]+Stopped (tty input) cat>tempfoo &
$ fg %1 使1号作业成为前台作业
cat>tempfoo shell告诉我们现在哪一个作业在前台
hello,world 输入1行
^D 键入我们的文件结束符
$ cat tempfoo 键入我们的文件结束符
hello,world 检查该行已送入文件
shell在后台起动cat进程,但是当cat试图读其标准输入(控制终端)时,终端驱动
程序知道
它是个后台作业,于是将SIGTTIN信号送至该后台作业。shell检测到其子进程的状
态改变(
回乙86节中对wait和waitpid的讨论),并通知我们该作业已被停止。然后,我们
用shell
的fg命令将此停止的作业送入前台运行。(关于作业控制命令,例如fg和bg的详细
情况,以
及标识不同作业的各种方法请参阅有关shell的手册页。)这样做使shell将此作业
转为前台
进程组(tcsetpgrp),并将继续信号(SIGCONT)送给该进程组。因为该作业现在前台
进程组中
,所以它可以读控制终端。
如果后台作业输出到控制终端又将发生什么呢?这是一个我们可以允许或禁止的选
择项。通
常,我们可以用stly(1)命令改变这一选择项。(第十一章将说明在程序中如何改变
这一选择
项)。下面显示了这种操作过程:
$ cat tempfoo & 在后台执行
[1] 1719 在提示符后出现后台作业的输出
[1]+Done cat tempfoo &
$ stty tostop 使后台作业可能向控制终端输出
$ cat tempfoo & 在后台再次执行
[1] 1721
$ 键入回车,发现作业已停止
[1]+Stopped(tty output) cat tempfoo&
$ fg %1 将停止的作业恢复为前台作业
cat tempfoo shell告诉我们现在哪一个作业在前台
hello,world 该作业的输出
图98摘录了我们已说明的作业控制的某些功能。
图98 对于前台、后台作业以及终端驱动程序的作业控制功能摘要
穿过终端驱动程序框的实线表示:终端I/O和终端产生的信号总是从前台进程组
连接到实
际终端。对应于SIGTTOU信号的虚线表示,后台进程组进程的输出出现在终端上是
个选择项
。
是否需要作业控制:这是一个有很多争论的问题。作业控制是在窗口终端广泛得到
应用之前
设计和实现的。很多人认为设计得好的窗口系统已经免除了对作业控制的需要。某
些人抱怨
作业控制的实现要求得到系统核、终端驱动程序、shell以及某些应用程序的支持
,是吃力
不讨好的事情。某些人在窗口系统中使用作业控制,他们认为两者都需要。不管你
的意见如
何,作业控制是POSIX1以及FIPS151-1的组成部分,它还将继续存在。
99〓shell执行程序
让我们检验一上shell是如何执行程序的,以及这与进程组、控制终端和对话期等
概念的关
系。为此,我们要再次使用ps命令。
首先我们使用不支持作业控制的经典的Bourne shell。如果执行:
ps -xj
则其输出为:
PPID PID PGID SID TPGID COMMAND
1 163 163 163 163 -sh
163 168 163 163 163 ps
(其中,删除了一些我们现在不感兴趣的列一终端名、用户ID、CPU时间等)。shel
l和ps命令
两者在同一对话期和前台进程组(163)。因为163是在TPGID列中显示的进程组,所
以我们称
其为前台进程组。ps的父进程是shell,这正是我们所期望的。注意,login shel
l是由logi
n以"一"作为其第一个字符调用的。
不幸的是,ps(1)命令的输出在各个Unix版本中都有所不同。在SVR4之下,使用命
令ps-j1得
到类似的输出,但SVR4不打印TPGID字段。在43+BSD之下,使用命令ps-xj -otp
gid。
注意,将一个进程与一个终端进程组ID(TPGID列)相关联是名字使用不当。一个进
程并没有
一个终端进程控制组属性。一个进程属于一个进程组。而一个进程组属于一个对话
期。对话
期可
能有,也可能及有控制终端。如果它确有一个控制终端,则此终端设备知道其前台
进程的进
程组ID。这一值可以用tcsetpgrp函数在终端驱动程序中设置。(如图98中所示。
)前台进
程组ID是终端的一个属性,不是进程的属性。取自终端设备驱动程序的该值是ps在
TPGID列
中打印的值。如果ps发现此对话期没有控制终端,则它在该列打印-1。
如果在后台执行该命令:
ps -xj &
则唯一改变的值是命令的进程ID。
PPID PID PGID SID TPGID COMMAND
1 163 163 163 163 -sh
163 169 163 163 163 ps
因为这种shell不知道作业控制,所以后台作业没有构成另一个进程组,也没有从
后台作业
处取走控制终端。
让我们现在看一看Bourne shell如何处理一个管道线。执行下列命令:
ps -xj | cat1
其输出是:
PPID PID PGID SID TPGID COMMAND
1 163 163 163 163 -sh
163 200 163 163 163 cat1
200 201 163 163 163 ps
(程序cat1只是标准cat程序的一个副本,但名字不同。我们还将在本节使用cat的
另一个名
为cat2的副本。在一个管道线中使用二个cat时,不同的名字可使我们将宅区分开
来。)注意
,在管道中的最后一个进程是shell的子进程,在该管道中的第一个进程则是最后
一个进程
的子进程。从中可以看出,shell fork一个它的副本,然后此副本再为管道线中的
每条命令
各fork一个进程。
如果在后台执行此管道线:
ps -xj | cat1 &
则只有进程ID改变了。因为shell并不处理作业控制,后台进程的进程组ID仍是16
3 ,如同
终端进程组ID一样。
如果一个后台进程试图读其控制终端,则会发生什么呢?例如,若执行:
cat>tempfoo &
在有作业控制时,后台作业被放在后台进程组,如果后台作业试图读控制终端,则
会产生信号SIGTTIN。在没有作业控制时,其处理方法是:如果该进程自己不重新定
向标准输入,则shell自动将后台进程的标准输入重新定向到/dev/null。读/dev
/null则产生一个文件结束。这就意味着后台cat进程立即读到文件尾,并正常结束。
上面说明了对后台进程通过其标准输入存取控制终端的适当处理方法,但是,如果
一个后台进程打开/dev/tty并且读该控制终端,又将怎样呢?对此问题的回答是"它
依赖于"。但是这很可能不是我们所要的。例如:
crypt<salaries | lpr &
就是这样的一条管道线。我们在后台运行它,但是crypt程序打开/dev/tty,更
改终端的特性(禁止回送),然后从该设备读,最后复置该终端特性。当执行这条后
台管道时,crypt在终端上打印,提示符Password:但是shell读取了我们所输入的(密码
口令字),并企图执行那种名字的一条命令。我们输给shell的下一行,则被Crype进程
取为口令字行,于是Salaries也不能正确地被译码,将一堆没有用的信息送到了打印机
。在这里,我们有了二个进程,它们试图在同时读同一设备,其结果则依赖于系
统。我们前面说明的作业控制以较好的方式处
理一个终端在多个进程间的转接。
返回到我们的Bourne shell实例,在一条管道中执行三个进程:
ps -xj|cat1|cat2
下面看一看shell所用的进程控制:
PPID PID PGID SID TPGID COMMAND
1 163 163 163 163 -sh
163 202 163 163 163 cat2
202 203 163 163 163 ps
202 204 163 163 163 cat1
再一次,该管道中的最后一个进程是shell的子进程,而执行在管道中其它命令的
进程则是
该最后进程的子进程。图99显示了所发生的情况。
图99 Bourne shell执行管道线ps-xj|cat1|cat2时的进程
依赖于"。但是这很可能不是我们所要的。例如:
crypt<salaries | lpr &
就是这样的一条管道线。我们在后台运行它,但是crypt程序打开/dev/tty,更
改终端的特性(禁止回送),然后从该设备读,最后复置该终端特性。当执行这条后
台管道时,crypt在终端上打印,提示符Password:但是shell读取了我们所输入的(密码
口令字),并企图执行那种名字的一条命令。我们输给shell的下一行,则被Crype进程
取为口令字行,于是Salaries也不能正确地被译码,将一堆没有用的信息送到了打印机
。在这里,我们有了二个进程,它们试图在同时读同一设备,其结果则依赖于系
统。我们前面说明的作业控制以较好的方式处
理一个终端在多个进程间的转接。
返回到我们的Bourne shell实例,在一条管道中执行三个进程:
ps -xj|cat1|cat2
下面看一看shell所用的进程控制:
PPID PID PGID SID TPGID COMMAND
1 163 163 163 163 -sh
163 202 163 163 163 cat2
202 203 163 163 163 ps
202 204 163 163 163 cat1
再一次,该管道中的最后一个进程是shell的子进程,而执行在管道中其它命令的
进程则是
该最后进程的子进程。图99显示了所发生的情况。
图99 Bourne shell执行管道线ps-xj|cat1|cat2时的进程
ps -xj &
其输出为:
PPID PID PGID SID TPGID COMMAND
1 700 700 700 700 -ksh
700 709 709 700 700 ps
再一次,ps命令被放入它自己的进程组,但是此时进程组(709)不再是前台进程组
。这是一
个后台进程组。TPGID 700指示前台进程组是login shell。
按下列方式在一个管道中执行两个进程:
ps -xj|cat1
其输出为:
PPID PID PGID SID TPGID COMMAND
1 700 700 700 710 -ksh
700 710 710 700 710 ps
700 711 710 700 710 cat1
两个进程ps和cat1都在一个新进程组(T10)中,这是一个前台进程组。在本例和类
似的Bourn
e
shell实例之间我们也能看到另一个区别。Bourne shell首先创建将执行管道线中
的最后一
条命令的进程,而因此进程是第一个进程的父进程。在这里,Korn shell是两个进
程的父进
程。但是,如果在后台执行此管道线。
ps -xj|cat1 &
其结果显示现在Kornshell以与Bourne shell相同的方式产生进程。
PPID PID PGID SID TPGID COMMAND
1 700 700 700 700 -ksh
700 712 712 700 700 cat1
712 713 712 700 700 ps
两个进程712和713都处在后台进程组712中。
910〓孤儿进程组
一个父进程已终止的进程称为弧儿进程(orphan process),这种进程由init进程收
领。现
在我们要说明整个进程组也可成为孤儿,以及POSIX1如何处理它。
实例
考虑一个进程,它fork了一个子进程然后终止。这在系统中是经常发生的,并无异
常之处,
但是在父进程终止时,如果该子进程停止(用作业控制)又将如何呢?子进程如何继
续,以及
子进程是否知道它已经是孤儿进程?程序91是这种情况的一个例子。下面要说明
该程序的
某些新特征。图910显示了程序91已经起动,父进程已经fork了子进程后的情
况。
图910 将成为孤儿的进程组的实例
这里,我们假定使用了一个作业-控制shell。回忆前面所述,shell将前台进程放
在一个进
程组中(本例中是512),shell则留在自己的组内(442)。子进程继承其父进程(512
)的进程组
。在fork之后:
·父进程睡眠5秒钟,这是一种让子进程在父进程终止之前运行的一种权宜之计。
·子进程为挂断信号(SIGHUP)建立信号处理程序。这样我们就能观察到SIGHUP信号
是否已送
到子进程。(在第十章讨论信号处理程序)。
·子进程用kill函数向其自身发送停止信号。这停止了要进程,这类似于用终端挂
起字符停
止一个前台作业。
·当父进程终止时,该子进程成为孤儿进程,共父进程ID成为1,也就是init进程
ID。
·到此点时,子进程成为一个孤儿进程组的成员。POSIX1将孤儿进程组定义为:
该组中每
个成员的父进程或者是该组的一个成员,或者不是该组所属对话期的成员。对孤儿
进程组的
另一种描述可以是:一个进程组不是孤儿进程组的条件是:该组中有一个进程,其
父进程在
属于同一对话期的另一个组中。如果进程组不是孤儿进程组,那么在属于同一对话
期的另一
个组中的父进程就有机会重新起动该组中停止的进程。
在我们这里,进程组中进程(513)的父进程(1)属于另一个对话期。所以此进程组是
弧儿进程
组。
·因为在父进程(512)终止后,进程组成为弧儿进程组,POSIX1要求向新成为孤
儿进程组
中
处于停止状态的每一个进程发送挂断信号(SIGHUG),接着又向其发送继续信号(SI
GCONT)。
·在处理了挂断信号后,子进程继续。对挂断信号的系统默认动作是终止该进程,
为此我们
必须提供一个信号处理程序以捕捉该信号。因此,我们可以期望sig-hup函数中的
printf会
在pr-ids函数中的printf之前执行。
程序91 创建一个孤儿进程组
下面是程序91的输出
$ aout
parent:pid=512,ppid=442,pgrp=512
child:pid=513,ppid=512,pgrp=512
$ SIGHUP received,pid=513
child:pid=513,ppid=1,pgrp=512
read error from control terminal,errno=5
注意,因为两个进程,login shell和子进程都写向终端,所以shell提示符和子进
程的输出
一起出现。如我们所期望的那样,子进程的父进程ID变成1。
注意,在子进程中调用pr-ids后,程序位阅读标准输入。正如前述,当后台进程组
试图读控
制终端时,则对该后台进程组产生SIGTTIN。但在这里,这是一个弧儿进程组,如
果系统核
用此信号停止它,则在此进程组中的进程就再也不会继续。POSIX1规定,read出
错返回,
其errno设置为E10(在作者所用的系统中其值是5)。
最后,要注意的是在父进程终止时,子进程变成后台进程组,因为父进程是由she
ll作为前
台作业执行的。
在195的pty程序中我们将会看到孤儿进程组的另一个例子。
911〓43+BSD实现
上面说明了进程、进程组、对话期和控制终端的各种属性,值得观察一下所有这些
是如何
实现
的。下面简要说明43+BSD的实现。SVR4实现的某些详细情况则参见Williams〔1
989〕
。图911显示了43+BSD的各种数据结构。
图911 对话期和进程组的43+BSD实现
让我们说明图中示出的各种字段。从对话期结构开始。为每个对话期分配一个这种
结构(
例如,每次调用setsid时)。
·scount是该对话期中的进程组数。当此计数器减至0时,则可释放此结构。
·SLeader是指向对话期首进程proc结构的指针。如前面提及的,43+BSD不保
持对话期I
D字段,而SVR4则保持此字段。
·Sttyvp是指向控制终端unode结构的指针。
·Sttyp是指向控制终端tly结构的指针。
在调用setsid时,在系统核中分配一个新的对话期结构。SCount设置为1,Sl
eader设置
为
调用进程的proc结构的指针,因为新对话期没有控制终端,所以sttyvp和SHy
p设置为
定指针。
接看说明tly结构。对于每个终端设备和每个伪终端设备在系统核中分配一个这样
的结构。(
在第十九章对伪终端作更多说明)
·tsession指向将此终端作为控制终端的对话期结构。(注意,tly结构指向对话
期结构,
对话期结构则也指向tly结构。)终端在失去载波信号(图97)时使用此指针将挂起
信号送给
对话期首进程。
·tpgrp指向前台进程组的pgrp结构。终端驱动程序用此字段将信号送在前台进
程组。由
输入特殊字符(中断、退出和挂起)而产生的三个信号被送至前台进程组。
·ttermios是包含所有这些特殊字符和与该终端有关信息的结构。(例如,波特
率,回送
打开或关闭等)。在第十一章中将再说明此结构。
·twinsize是包含终端窗口当前尺寸的winsize结构。当终端窗口尺寸改变时,
信号SIGWI
NCH被送至前台进程组。在11、12节中将说明如何设置和存取终端当前窗口尺寸。
注意,为了找到特定对话期的前台进程组,系统核从对话期结构开始,然后按s-t
typ得到控制终端的tty结构,然后按t-pgrp得到前台进程组的pgrp结构。
pgrp结构包含一个进程的信息。
·pgid是进程组ID。
·pgsession指向此进程组所属的对话期结构。
·pgmem是指向此进程组第一个进程的proc结构的指针。在proc结构中的ppgr
pnxt指向此组中的下一个进程,进程组中最后一个进程proc中的ppgrpnxt则为定
指针。
proc结构包含一个进程的所有信息。
·ppid包含进程ID。
·ppptr是指向父进程proc结构的指针。
·ppgrp指向本进程所属的进程组的pgrp结构。
·ppgrpnxt如上面已说明的,这是指向进程组中下一个进程的指针。
最后还有一个vnode结构。在打开控制终端设备时分配此结构。进程对/dev/tty
的所有访问都通过vnode结构。在图911中,实际inode是vnode的一部分。
在310中我们曾提及这是43+BSD的实现方法,而SVR4则将Vnode存在inode中。
912〓摘要
本章说明了进程组之间的关系--对话期,它由若干个进程组组成。作业控制是当今
很多Unix系统所支持的功能,我们说明了它是如何由支持作业控制的shell实现的。在
这些进程关系涉及到了/dev/tty。在所有这些进程关系中,都用了很多信号方面的
功能。下一章详细地讨论Unix中的信号机制
。〖LM〗