第9章 进程关系


下载 下载 第9章 进 程 关 系 9.1 引言 在上一章我们已了解到进程之间具有关系。首先,每个进程有一个父进程。当子进程终止 时,父进程会得到通知并能取得子进程的退出状态。在 8 . 6节说明w a i t p i d函数时,我们也提到 了进程组,以及如何等待进程组中的任意一个进程终止。 本章将更详细地说明进程组以及 P O S I X . 1引进的对话期新概念。还将介绍登录 s h e l l(登录 时所调用的)和所有从登录s h e l l起动的进程之间的关系。 在说明这些关系时不可能不谈及信号,而谈论信号又需要很多本章介绍的概念。如果你不 熟悉U N I X信号,则可能先要浏览一下第1 0章。 9.2 终端登录 先看一看登录到U N I X系统时所执行的各个程序。在早期的 U N I X系统中,例如V 7,用户 用哑终端(通过R S - 2 3 2连到主机)进行登录。终端或者是本地的(直接连接)或者是远程的(通 过调制解调器连接)。在这两种情况下,登录都经由内核中的终端设备驱动程序。例如,在 P D P - 11上常用的设备是D H - 11和D Z - 11。因为连到主机上的终端设备数已经确定,所以同时的 登录数也就有了已知的上限。下面说明的登录过程适用于使用一个 R S - 2 3 2终端登录到U N I X系 统中。 9.2.1 4.3+BSD终端登录 登录过程在过去 1 5年中并没有多少改变。系统管理者创建一个通常名为 / e t c / t t y s的文件, 其中,每个终端设备有一行,每一行说明设备名和传到 g e t t y程序的参数,这些参数说明了终端 的波特率等。当系统自举时,内核创建进程 ID 1,也就是i n i t进程。i n i t进程使系统进入多用户 状态。i n i t读文件/ e t c / t t y s ,对每一个允许登录的终端设备, i n i t调用一次f o r k,它所生成的子进程 则执行程序g e t t y。这种情况示于图9 - 1中。 图9 - 1中各个进程的实际用户 I D和有效用户I D都是 0 (也就是它们都具有超级用户特权 )。i n i t以空环境执行 g e t t y程序。 g e t t y对终端设备调用o p e n函数,以读、写方式将终 端打开。如果设备是调制解调器,则o p e n可能会在设备 驱动程序中滞留,直到用户拨号调制解调器,并且线 路被接通。一旦设备被打开,则文件描述符 0、1、2就 被设置到该设备。然后g e t t y输出“l o g i n:”之类的信息, 并等待用户键入用户名。如果终端支持多种速度,则 g e t t y可以测试特殊字符以便适当地更改终端速度 (波特 率)。关于g e t t y程序以及有关数据文件的细节,请参阅 图9-1 init生成进程使终端可用于登录 进程 ID 1 对每个终端 f o r k一次 每个子进程 exec getty U N I X手册。 当用户键入了用户名后,g e t t y就完成了。然后它以类似于下列的方式调用 l o g i n程序: execle("/usr/bin/login", "login", "-p", username, (char *) 0, envp); (在g e t t y t a b文件中可能会有一些选择项使其调用其他程序,但系统默认是 l o g i n程序)。i n i t以一 个空环境调用g e t t y。g e t t y以终端名(例如TERM=foo, 其中终端f o o的类型取自g e t t y t a b文件)和 在g e t t y t a b中的环境字符串为l o g i n创建一个环境(e n v p参数)。-p标志通知l o g i n保留传给它的环 境,也可将其他环境字符串加到该环境中,但是不要替换它。图 9 - 2显示了l o g i n刚被调用后这 些进程的状态。 因为最初的i n i t进程具有超级用户优先权,所以图 9 - 2中的所有进程都有超级用户优先权。 图9 - 2中底部三个进程的进程 I D相同,因为进程I D不会因执行e x e c而改变。并且,除了最初的 i n i t进程,所有的进程均有一个父进程I D。 login 能处理多项工作。因为它得到了用户名,所以能调用 getpwnam 取得相应用户的 口令文件登录项。然后调用 g e t p a ss(3)以显示提示“ P a s s w o r d:”接着读用户键入的口令 (自然,禁止回送用户键入的口令)。它调用 c r y pt(3)将用户键入的口令加密,并与该用户 口令文件中登录项的 p w _ p a s s w d字段相比较。如果用户几次键入的口令都无效,则 login 以 参数1调用exit 表示登录过程失败。父进程( i n i t)了解到子进程的终止情况后,将再次调 用f o r k,其后又跟随着执行 g e t t y,对此终端重复上述过程。 如果用户正确登录, l o g i n就将当前工作目录更改为该用户的起始目录 ( c h d i r )。它也调用 c h o w n改变该终端的所有权,使该用户成为所有者和组所有者。将对该终端设备的存取许可权 改变成:用户读、写和组写。调用 s e t g i d及i n i t g r o u p s设置进程的组I D。然后用l o g i n所得到的所 有信息初始化环境:起始目录 ( H O M E )、s h e l l ( S H E L L )、用户名( U S E R和L O G N A M E ),以及一 个系统默认路径( PAT H )。最后,l o g i n进程改变为登录用户的用户 I D ( s e t u i d )并调用该用户的登 录s h e l l,其方式类似于: execl("/bin/sh", "-sh", (char *) 0); a rg v [ 0 ]的第一个字符-是一个标志,表示该 s h e l l被调用为登录s h e l l。s h e l l可以查看此字符,并 相应地修改其起动过程。 第 9章 进 程 关 系 1 8 1下载 进程 ID 1 通过g e t t y和l o g i n R S - 2 3 2连接 终端设备 驱动程序 使用终端 的用户 登录s h e l l 图9-3 终端登录结束后的有关进位图9-2 login刚被调用后各进程的状态 进程 ID 1 读/ e t c / t t y s ;对每个终端 f o r k一次创建空环境 打开终端设备(文 件描述符0 , 1 , 2 )读 用户名;初始化环 境设置 l o g i n所做的比上面说的要多。它可选地打印m e s s a g e - o f - t h e - d a y文件,检查新邮件以及其他 一些功能。但是考虑到本书的内容,我们主要关心上面所说的功能。 回忆在8 . 1 0节中对s e t u i d函数的讨论,因为s e t u i d是由超级用户调用的,它更改所有三个用户 ID:实际、有效和保存的用户ID。login在较早时间调用的setgid对所有三个组ID也有同样效果。 到此为止,登录用户的登录 s h e l l开始运行。其父进程I D是i n i t进程ID (进程ID 1),所以当 此登录s h e l l终止时,i n i t会得到通知(接到S I G C H L D信号),它会对该终端重复全部上述过程。 登录s h e l l的文件描述符0,1和2设置为终端设备。图9 - 3显示了这种安排。 现在,登录s h e l l读其起动文件(Bourne shell和K o r n S h e l l是.profile, C shell是. c s h r c和. l o g i n )。 这些起动文件通常改变某些环境变量,加上一些环境变量。例如,很多用户设置他们自己的 PAT H,常常提示实际终端类型 ( T E R M )。当执行完起动文件后,用户最后得到 s h e l l的提示符, 并能键入命令。 9.2.2 SVR4终端登录 S V R 4支持两种形成的终端登录: ( a ) g e t t y方式,这与上面对 4 . 3 + B S D所说明的一样, ( b ) t t y m o n登录,这是S V R 4的一种新功能。通常,g e t t y用于控制台,t t y m o n则用于其他终端的 登录。 t t y m o n是名为服务存取设施(Service Access Facility, SAF)的一部分。按照本书的目的, 我们只简单说明从i n i t到登录s h e l l之间工作过程,最后结果与图 9 - 3中所示相似。i n i t是s a c(服 务存取控制器)的父进程,s a c调用f o r k,然后其子进程执行t t y m o n程序,此时系统进入多用户 状态。t t y m o n监视列于配置文件中的所有终端端口,当用户键入登录名时,它调用一次 f o r k。 在此之后该子进程又执行登录用户的登录 s h e l l,于是到达了图9 - 3中所示的位置。一个区别是 登录s h e l l的父进程现在是t t y m o n,而在getty 登录中,登录s h e l l的父进程是i n i t。 9.3 网络登录 9.3.1 4.3+B S D网络登录 在上节所述的终端登录中 , i n i t知道哪些终端设备可用来进行登录,并为每个设备生成一个 g e t t y进程。但是,对网络登录则情况有所不同,所有登录都经由内核的网络界面驱动程序(例 如:以太网驱动程序),事先并不知道将会有多少这样的登录。不是使一个进程等待每一个可 能的登录,而是必须等待一个网络连接请求的到达。在 4 . 3 + B S D中,有一个称为 i n e t d的进程 (有时称为Internet superserver),它等待大多数网络连接。本书将说明4 . 3 + B S D的网络登录中所 涉及的进程序列。关于这些进程的网络程序设计方面的细节请参阅 S t e v e n s〔1 9 9 0〕。 作为系统起动的一部分, i n i t调用一个s h e l l,使其执行s h e l l脚本e t c / r c。由此s h e l l脚本起动 一个精灵进程i n e t d。一旦此s h e l l脚本终止,i n e t d的父进程就变成i n i t。i n e t d等待T C P / I P连接请 求到达主机,而当一个连接请求到达时,它执行一次 f o r k,然后该子进程执行适当的程序。 我们假定到达了一个对于 T E L N E T服务器的T C P连接请求。T E L N E T是使用T C P协议的远 程登录应用程序。在另一个主机 (它通过某种形式的网络,连接到服务器主机上 )上的用户,或 在同一个主机上的一个用户籍起动T E L N E T客户进程( c l i e n t )起动登录过程: telnet h o s t n a m e 该客户进程打开一个到名为 h o s t n a m e的主机的T C P连接,在h o s t n a m e主机上起动的程序被称为 1 8 2 U N I X环境高级编程 下载 T E L N E T服务器。然后,客户进程和服务器进程之间使用 T E L N E T应用协议通过T C P连接交换 数据。所发生的是起动客户进程的用户现在登录到了服务器进程所在的主机。(自然,用户需 要在服务器进程主机上有一个有效的账号)。图9 - 4显示了在执行 T E L N E T服务器进程 (称为 t e l n e t d )中所涉及的进程序列。 然后,t e l n e t d进程打开一个伪终端设备,并用 f o r k生成一个子进程(第 1 9章将详细说明伪 终端)。父进程处理通过网络连接的通信,子进程则执行 l o g i n程序。父、子进程通过伪终端相 连接。在调用e x e c之前,子进程使其文件描述符0 , 1 , 2与伪终端相连。如果登录正确,l o g i n就执 行9 . 2节中所述的同样步骤— 更改当前工作目录为起始目录,设置登录用户的组 I D和用户I D, 以及登录用户的初始环境。然后 l o g i n用e x e c将其自身替换为登录用户的登录 s h e l l。图9 - 5显示 了到达这一点时的进程安排。 很明显,在伪终端设备驱动程序和终端实际用户之间有很多事情在进行着。第 1 9章详细说 明伪终端时,我们会介绍与这种安排相关的所有进程。 需要理解的重点是:当通过终端(见图 9 - 3)或网络(见图9 - 5)登录时,我们得到一个登 录s h e l l,其标准输入、输出和标准出错连接到一个终端设备或者伪终端设备上。在下一节中 我们会了解到这一登录s h e l l是一个P O S I X . 1对话期的开始,而此终端或伪终端则是会话期的控 制终端。 9.3.2 SVR4网络登录 S V R 4中网络登录的情况与 4 . 3 + B S D中的几乎一样。同样使用了 i n e t d服务器进程,但是在 S V R 4中i n e t d是作为一种服务由服务存取控制器 s a c调用的,其父进程不是 i n i t。最后得到的结 果与图9 - 5中一样。 9.4 进程组 每个进程除了有一进程I D之外,还属于一个进程组,第1 0章讨论信号时还会涉及进程组。 进程组是一个或多个进程的集合。每个进程组有一个唯一的进程组 I D。进程组I D类似于 进程I D——它是一个正整数,并可存放在 p i d _ t数据类型中。函数 g e t p g r p返回调用进程的进程 第 9章 进 程 关 系 1 8 3下载 图9-4 执行T E L N E T服务进程中涉及的进程序列 进程 ID 1 f o r k并执( e x e c ) / b i n / s h , 在系统进入多用户方 式时它解释执/ e t c / r c 来自T E L N E T客户 的T C P连接请求 来自T E L N E T客户的 连接请求到达时 图9-5 为网络登录设置了fd 0, 1, 2后的进程安排 进程 ID 1 登录s h e l l 伪终端设备 驱动程序 使用终端 的用户 通过i n e t d , t e l n e t d , 和l o g i n t e l n e t d 服务器和 t e l n e t客户之间的网 络连接 组I D。 #include #include pid_t getpgrp(void); 返回:调用进程的进程组 I D 在很多伯克利的系统中,包括 4 . 3 + B S D,这一函数的参数是p i d,返回该进程 的进程组。上面所示的原型是P O S I X . 1版本。 每个进程组有一个组长进程。组长进程的标识是,其进程组 I D等于其进程I D。 进程组组长可以创建一个进程组,创建该组中的进程,然后终止。只要在某个进程组中有 一个进程存在,则该进程组就存在,这与其组长进程是否终止无关。从进程组创建开始到其中 最后一个进程离开为止的时间区间称为进程组的生命期。某个进程组中的最后一个进程可以终 止,也可以参加另一个进程组。 进程调用s e t p g i d可以参加一个现存的组或者创建一个新进程组 (下一节中将说明用s e t s i d也 可以创建一个新的进程组)。 #include #include int setpgid(pid_t p i d, pid_t p g i d); 返回:若成功则为0,出错为-1 这将pid 进程的进程组I D设置为p g i d。如果这两个参数相等,则由 pid 指定的进程变成进程组 组长。 一个进程只能为它自己或它的子进程设置进程组 I D。在它的子进程调用了e x e c后,它就不 再能改变该子进程的进程组I D。 如果p i d是0,则使用调用者的进程I D。另外,如果p g i d是0,则由p i d指定的进程I D被用作 为进程组I D。 如果系统不支持作业控制( 9 . 8节将说明作业控制),那么就不定义_ P O S I X _ J O B _ C O N T R O L, 在这种情况下,此函数返回出错,e r r n o设置为E N O S Y S。 在大多数作业控制 s h e l l中,在f o r k之后调用此函数,使父进程设置其子进程的进程组 I D, 然后使子进程设置其自己的进程组 I D。这些调用中有一个是冗余的,但这样做可以保证父、子 进程在进一步操作之前,子进程都进入了该进程组。如果不这样做的话,那么就产生一个竞态 条件,因为它依赖于哪一个进程先执行。 在讨论信号时,将说明如何将一个信号送给一个进程 (由其进程I D标识)或送给一个进程组 (由进程组I D标识)。同样,w a i t p i d则可被用来等待一个进程或者指定进程组中的一个进程。 9.5 对话期 对话期(s e s s i o n)是一个或多个进程组的集合。例如,可以有图 9 - 6中所示的安排。其中, 1 8 4 U N I X环境高级编程 下载 在一个对话期中有三个进程组。通常是由 s h e l l的管道线将几个进程编成一组的。例如,图 9 - 6 中的安排可能是由下列形式的s h e l l命令形成的: procl | proc2 & proc3 | proc4 | proc5 图9-6 进程组和对话期中的进程安排 进程调用s e t s i d函数就可建立一个新对话期。 #include #include pid_t setsid(void); 返回:若成功则为进程组 I D,若出错则为-1 如果调用此函数的进程不是一个进程组的组长,则此函数创建一个新对话期,结果为: (1) 此进程变成该新对话期的对话期首进程(session leader,对话期首进程是创建该对话期 的进程)。此进程是该新对话期中的唯一进程。 (2) 此进程成为一个新进程组的组长进程。新进程组 I D是此调用进程的进程I D。 (3) 此进程没有控制终端(下一节讨论控制终端)。如果在调用s e t s i d之前此进程有一个控 制终端,那么这种联系也被解除。 如果此调用进程已经是一个进程组的组长,则此函数返回出错。为了保证不处于这种情况, 通常先调用fork,然后使其父进程终止,而子进程则继续。因为子进程继承了父进程的进程组ID, 而其进程ID则是新分配的,两者不可能相等,所以这就保证了子进程不是一个进程组的组长。 P O S I X . 1只包括对话期首进程,而没有类似与进程 I D和进程组I D的对话期I D。 显然,对话期首进程是具有唯一进程 I D的单个进程,所以可以将对话期首进程的 进程I D视为对话期I D。S V R 4就是这样处理的。S V I D和S V R 4的s e t s i d ( 2 )手册页谈 到了以此种方式定义的对话期 I D。这是一种实现细节,它不是 P O S I X . 1中定义的, 4 . 3 + B S D也不支持它。 S V R 4有一个g e t s i d函数,它返回一个进程的对话期 I D。此函数不是P O S I X . 1 的所属部分,4 . 3 + B S D也不支持此函数。 9.6 控制终端 对话期和进程组有一些其他特性: 第 9章 进 程 关 系 1 8 5下载 登录s h e l l 进程组 进程组 进程组 对话期 • 一个对话期可以有一个单独的控制终端( controlling terminal)。这通常是我们在其上登 录的终端设备(终端登录情况)或伪终端设备(网络登录情况)。 • 建立与控制终端连接的对话期首进程,被称之为控制进程( controlling process)。 • 一个对话期中的几个进程组可被分成一个前台进程组( foreground process group)以及一 个或几个后台进程组(background process group)。 • 如果一个对话期有一个控制终端,则它有一个前台进程组,其他进程组则为后台进程组。 • 无论何时键入中断键(常常是D E L E T E或C t r l - C)或退出键(常常是C t r l - \),就会造成将 中断信号或退出信号送至前台进程组的所有进程。 • 如果终端界面检测到调制解调器已经脱开连接,则将挂断信号送至控制进程(对话期首 进程。)这些特性示于图9 - 7中。 图9-7 进程组、对话期和控制终端 通常,我们不必担心控制终端——登录时,将自动建立控制终端。 系统如何分配一个控制终端依赖于实现。 1 9 . 4节中将说明实际步骤。 当对话期首进程打开第一个尚未与一个对话期相关联的终端设备时, S V R 4将 此作为控制终端分配给此对话期。这假定对话期首进程在调用 o p e n时没有指定 O _ N O C T T Y标志(见3 . 3节)。 当对话期首进程用 T I O C S C T T Y(第三个参数是空指针)的 r e q u e s t参数调用 i o c t l时,4 . 3 + B S D为对话期分配控制终端。为使此调用成功执行,此对话期不能 已经有一个控制终端(通常i o c t l调用紧跟在s e t s i d调用之后,s e t s i d保证此进程是一 个没有控制终端的对话期首进程)。4.3+BSD 不使用P O S I X . 1中对o p e n函数所说明 的O _ N O C T T Y标志。 有时不管标准输入、标准输出是否重新定向,程序都要与控制终端交互作用。保证程序读 写控制终端的方法是打开文件/ d e v / t t y,在内核中,此特殊文件是控制终端的同义语。自然,如 果程序没有控制终端,则打开此设备将失败。 1 8 6 U N I X环境高级编程 下载 登录s h e l l 后台进程组 对话期首进程= 控制进程 控制器终端 后台进程组 前台进程组 对话期 典型的例子是用于读口令的 g e t p a s s ( 3 )函数(终端回送被关闭)。这一函数由crypt(1) 程序 调用,而此程序则可用于管通线中。例如: crypt < salaries | lpr 它将文件s a l a r i e s解密,然后经由管道将输出送至打印假脱机程序。因为 c r y p t从其标准输入读 输入文件,所以标准输入不能用于输入口令。但是,c r y p t的一个设计特征是每次运行此程序时, 都应输入加密口令,这样也就不需要将口令存放在文件中。 已经知道有一些方法可以破译 c r y p t程序使用的密码。关于加密文件的详细情况请参见 G a r f i n k e l和S p a fford [1991]。 9.7 tcgetpgrp和t c s e t p g r p函数 需要有一种方法来通知内核哪一个进程组是前台进程组,这样,终端设备驱动程序就能了 解将终端输入和终端产生的信号送到何处(见图 9 - 7)。 #include #include pid_t tcgetpgrp(int f i l e d e s) ; 返回:若成功则为前台进程组 I D,若出错则为-1 int tcsetpgrp(int f i l e d e s, pid_t p g r p i d) ; 返回:若成功则为0,若出错则为-1 函数t c g e t p g r p返回前台进程组I D,它与在f i l e d e s上打开的终端相关。 如果进程有一个控制终端,则该进程可以调用 t c s e t p g r p将前台进程组 I D设置为p g r p i d。 p g r p i d值应当是在同一对话期中的一个进程组的I D。f i l e d e s必须引用该对话期的控制终端。 大多数应用程序并不直接调用这两个函数。它们通常由作业控制 s h e l l调用。只有定义了 _ P O S I X _ J O B _ C O N T R O L,这两个函数才被定义了。否则它们返回出错。 9.8 作业控制 作业控制是伯克利在1 9 8 0年左右加到U N I X的一个新特性。它允许在一个终端上起动多个 作业(进程组),控制哪一个作业可以存取该终端,以及哪些作业在后台运行。作业控制要求三 种形式的支持: (1) 支持作业控制的s h e l l。 (2) 内核中的终端驱动程序必须支持作业控制。 (3) 必须提供对某些作业控制信号的支持。 S V R 3提供了一种不同形式的作业控制,称为 s h e l l层。但是P O S I X . 1选择了伯 克利形式的作业控制,这也是我们在这里所说明的。回忆表 2 - 7,如果系统支持作 业控制,则定义常数_ P O S I X _ J O B _ C O N T R O L。 FIPS 151-1要求P O S I X . 1作业控制。 S V R 4和4 . 3 + B S D支持P O S I X . 1作业控制。 第 9章 进 程 关 系 1 8 7下载 从s h e l l使用作业控制功能角度观察,可以在前台或后台起动一个作业。一个作业只是几个 进程的集合,通常是一个进程管道。例如: vi main.c 在前台起动了只有一个进程的一个作业。下面的命令: pr *.c | lpr & make all & 在后台起动了两个作业。这两个后台作业所调用的进程都在后台运行。 正如前述,我们需要一个支持作业控制的 s h e l l以使用由作业控制提供的功能。对于早期的 系统,s h e l l是否支持作业控制比较易于说明。C shell支持作业控制,Bourne shell则不支持,而 K o r n S h e l l能否支持作业控制取决于主机是否支持作业控制。但是现在 C shell已被移植到并不支 持作业控制的系统上(例如系统V的早期版本),而SVR4 Bourne shell当用名字j s h而不是s h调用 时则支持作业控制。如果主机支持作业控制,则 K o r n S h e l l继续支持作业控制。各种s h e l l之间的 差别并不显著时,我们将只是一般地说明支持作业控制的 s h e l l和不支持作控制的s h e l l。 当起动一个后台作业时,s h e l l赋与它一个作业标识,并打印一个或几个进程 I D。下面的操 作过程显示了K o r n S h e l l是如何处理这一点的。 $ make all > Make.out & [1] 1475 $ pr *.c | lpr & [2] 1490 $ 键入回车 [2] + Done pr *.c | lpr & [1] + Done make all > Make.out & m a k e是作业号1,所起动的进程I D是1 4 7 5。下一个管道线是作业号 2,其第一个进程的进程 I D 是1 4 9 0。当作业已完成而且键入回车时, s h e l l通知我们作业已经完成。键入回车是为了让 s h e l l 打印其提示符。s h e l l并不在任何随意的时间打印后台作业的状态改变——它只在打印其提示符 之前这样做。如果不这样处理,则当我们正输入一行时,它也可能输出。 我们可以键入一个影响前台作业的特殊字符——挂起键(一般采用 C t r l - Z)与终端进行交 互作用。键入此字符使终端驱动程序将信号 S I G T S T P送至前台进程组中的所有进程,后台进程 组作业则不受影响。实际上有三个特殊字符可使终端驱动程序产生信号,并将它们送至前台进 程组,它们是: • 中断字符(一般采用D E L E T E或C t r l - C)产生S I G I N T。 • 退出字符(一般采用C t r l - \)产生S I G Q U I T。 • 挂起字符(一般采用C t r l - Z)产生S I G T S T P。 第11章中将说明可将这三个字符更改为任一其他字符,以及如何使终端驱动程序不处理这 些特殊字符。 终端驱动程序必须处理与作业控制有关的另一种情况。我们可以有一个前台作业,若干个 后台作业,这些作业中哪一个接收我们在终端上键入的字符呢 ?只有前台作业接收终端输入。 如果后台作业试图读终端,那么这并不是一个错误,但是终端驱动程序检测这种情况,并且发 送一个特定信号S I G T T I N给后台作业。这通常会停止此后台作业,而有关用户则会得到这种情 况的通知,然后就可将此作业转为前台作业运行,于是它就可读终端。下列操作过程显示了这 一点: 1 8 8 U N I X环境高级编程 下载 $ cat > temp.foo & 在后台启动,但将从标准输入读 [1] 1681 $ 键入回车 [1] + Stopped (tty input) cat > temp.foo & $ fg %1 使1号作业成为前台作业 cat > temp.foo s h e l l告诉我们现在哪一个作业在前台 hello, world 输入1行 ˆ D 键入文件结束符 $ cat temp.foo 检查该行已送入文件 hello, world s h e l l在后台起动c a t进程,但是当c a t试图读其标准输入(控制终端)时,终端驱动程序知道它 是个后台作业,于是将S I G T T I N信号送至该后台作业。s h e l l检测到其子进程的状态改变(回忆 8 . 6节中对w a i t和w a i t p i d的讨论),并通知我们该作业已被停止。然后,用 s h e l l的f g命令将此停 止的作业送入前台运行(关于作业控制命令,例如 f g和b g的详细情况,以及标识不同作业的各 种方法请参阅有关s h e l l的手册页)。这样做使s h e l l将此作业转为前台进程组( t c s e t p g r p),并将 继续信号( S I G C O N T )送给该进程组。因为该作业现在前台进程组中,所以它可以读控制终端。 如果后台作业输出到控制终端又将发生什么呢 ? 这是一个我们可以允许或禁止的选择项。 通常,可以用s t t y ( 1 )命令改变这一选择项(第 11章将说明在程序中如何改变这一选择项)。下 面显示了这种操作过程: $ cat temp.foo & 在后台执行 [1] 1719 $ hello, world 在提示符后出现后台作业的输出 键入回车 [1] + Done cat temp.foo & $ stty tostop 禁止后台作业向控制终端输出 $ cat temp.foo & 在后台再次执行 [1] 1721 $ 键入回车,发现作业已停止 [1] + Stopped(tty output) cat temp.foo & $ fg %1 将停止的作业恢复为前台作业 cat temp.foo s h e l l告诉我们现在哪一个作业在前台 hello, world 该作业的输出 图9 - 8摘录了我们已说明的作业控制的某些功能。穿过终端驱动程序框的实线表示:终端 I / O和终端产生的信号总是从前台进程组连接到实际终端。对应于 S I G T TO U信号的虚线表示, 后台进程组进程的输出是否出现在终端是可选择的。 是否需要作业控制是一个有很多争论的问题。作业控制是在窗口终端广泛得到应用之前设 计和实现的。很多人认为设计得好的窗口系统已经免除了对作业控制的需要。某些人抱怨作业 控制的实现要求得到内核、终端驱动程序、 s h e l l以及某些应用程序的支持,是吃力不讨好的事 情。某些人在窗口系统中使用作业控制,他们认为两者都需要。不管你的意见如何,作业控制 是P O S I X . 1以及FIPS 151-1的组成部分,它还将继续存在。 9.9 shell执行程序 让我们检验一下s h e l l是如何执行程序的,以及这与进程组、控制终端和对话期等概念的关 系。为此,要再次使用p s命令。 第 9章 进 程 关 系 1 8 9下载 首先使用不支持作业控制的经典的Bourne shell。如果执行: ps -xj 则其输出为: PPID PID PGID SID TPGID COMMAND 1 163 163 163 163 -sh 163 168 163 163 163 ps 其中,删除了一些我们现在不感兴趣的列— 终端名、用户I D、C P U时间等。s h e l l和p s命令两 者位于同一对话期和前台进程组 ( 1 6 3 )中。因为1 6 3是在T P G I D列中显示的进程组,所以称其为 前台进程组。p s的父进程是s h e l l,这正是我们所期望的。注意,登录 s h e l l是由l o g i n以—作为其 第一个字符调用的。 1 9 0 U N I X环境高级编程 下载 对话期 使用终端 的用户 终端驱 动程序 后台进程组 前台进程组 登录s h e l l 在s e t s i d后执行(e x e c), 然后建立控制终端 或 或 图9-8 对于前台、后台作业以及终端驱动程序的作业控制功能摘要 不幸的是,p s ( 1 )命令的输出在各个 U N I X版本中都有所不同。在 S V R 4之下, 使用命令ps -j1得到类似的输出,但S V R 4不打印T P G I D字段。在4 . 3 + B S D之下,使 用命令ps -xj -otpgid。 注意,说进程与终端进程组I D ( T P G I D列)相关联是用词不当。进程并没有终端进程控制组。 进程属于一个进程组,而进程组属于一个对话期。对话期可能有,也可能没有控制终端。如果 它确有一个控制终端,则此终端设备知道其前台进程的进程组 I D。这一值可以用t c s e t p g r p函数 在终端驱动程序中设置(见图 9 - 8)。前台进程组I D是终端的一个属性,而不是进程的属性。取 自终端设备驱动程序的该值是 p s在T P G I D列中打印的值。如果 p s发现此对话期没有控制终端, 则它在该列打印-1。 如果在后台执行该命令: ps -xj & 则唯一改变的值是命令的进程I D。 PPID PID PGID SID TPGID COMMAND 1 163 163 163 163 -sh 163 169 163 163 163 ps 因为这种s h e l l不知道作业控制,所以后台作业没有构成另一个进程组,也没有从后台作业处取 走控制终端。 现在看一看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 (程序c a t 1只是标准c a t程序的一个副本,但名字不同。本节还将使用 c a t的另一个名为c a t 2的副 本。在一个管道线中使用两个 c a t时,不同的名字可使我们将它们区分开来。)注意,管道中的 最后一个进程是s h e l l的子进程,该管道中的第一个进程则是最后一个进程的子进程。从中可以 看出,shell fork一个它的副本,然后此副本再为管道线中的每条命令各 f o r k一个进程。 如果在后台执行此管道线: ps -xj | cat1 & 则只有进程I D改变了。因为s h e l l并不处理作业控制,后台进程的进程组 I D仍是1 6 3,如同终端 进程组I D一样。 如果一个后台进程试图读其控制终端,则会发生什么呢 ?例如,若执行: cat > temp.foo & 在有作业控制时,后台作业被放在后台进程组,如果后台作业试图读控制终端,则会产生信号 S I G T T I N。在没有作业控制时,其处理方法是:如果该进程自己不重新定向标准输入,则 s h e l l 自动将后台进程的标准输入重新定向到 / d e v / n u l l。读/ d e v / n u l l则产生一个文件结束。这就意味 着后台c a t进程立即读到文件尾,并正常结束。 第 9章 进 程 关 系 1 9 1下载 上面说明了对后台进程通过其标准输入存取控制终端的适当的处理方法,但是,如果一个 后台进程打开/ d e v / t t y并且读该控制终端,又将怎样呢?对此问题的回答是“看情况”。但是这很 可能不是我们所要的。例如: crypt < salaries | lpr & 就是这样的一条管道线。我们在后台运行它,但是 c r y p t程序打开/ d e v / t t y,更改终端的特性 (禁止回送),然后从该设备读,最后复置该终端特性。当执行这条后台管道时, c r y p t在终端上 打印提示符“Password: ”,但是s h e l l读取了我们所输入的加密码口令,并企图执行其中一条命 令。我们输送给s h e l l的下一行,则被c r y p t进程取为口令行,于是s a l a r i e s也就不能正确地被译码, 结果将一堆没有用的信息送到了打印机。在这里,我们有了两个进程,它们试图同时读同一设 备,其结果则依赖于系统。前面说明的作业控制以较好的方式处理一个终端在多个进程间的转 接。 返回到Bourne shell实例,在一条管道中执行三个进程: ps -xj | cat1 | cat2 下面看一看s h e l l所用的进程控制: 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 再重申一遍,该管道中的最后一个进程是 s h e l l的子进程,而执行管道中其他命令的进程则是该 最后进程的子进程。图9 - 9显示了所发生的情况。 图9-9 Bourne shell执行管道线ps -xj | cat1 | cat2 时的进程 因为该道通线中的最后一个进程是登录 s h e l l的子进程,当该进程 ( c a t 2 )终止时,s h e l l得到 通知。 现在让我们用作业控制s h e l l来检验一下同一个例子。这将显示这些 s h e l l处理后台作业的方 法。在本例中将使用K o r n S h e l l——用 C shell得到的结果几乎是一样的。 ps -xj 其输出为: PPID PID PGID SID TPGID COMMAND 1 9 2 U N I X环境高级编程 下载 管道线 1 700 700 700 708 -ksh 700 708 708 700 708 ps (从本例开始,以粗体显示前台进程组。)我们立即看到了与 Bourne shell例子的区别。 K o r n S h e l l将前台作业( p s )放入了它自己的进程组( 7 0 8 )。p s命令是进程组组长进程,并是该进程 组的唯一进程。进一步而言,此进程组具有控制终端,所以它是前台进程。我们的登录 s h e l l在 执行p s命令时是后台进程组。但需要注意的是,这两个进程组7 0 0和7 0 8都是同一对话期的成员。 事实上,在本书的实例中对话期决不会改变。 在后台执行此进程: ps -xj & 其输出为: PPID PID PGID SID TPGID COMMAND 1 700 700 700 700 -ksh 700 709 709 700 700 ps 再一次,p s命令被放入它自己的进程组,但是此时进程组( 7 0 9 )不再是前台进程组。这是一个后 台进程组。TPGID 700指示前台进程组是登录s h e l l。 按下列方式在一个管道中执行两个进程: 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 两个进程p s和c a t 1都在一个新进程组 ( 7 1 0 )中,这是一个前台进程组。在本例和类似的 B o u r n e s h e l l实例之间能看到另一个区别。 Bourne shell首先创建将执行管道线中最后一条命令的进程, 而此进程是第一个进程的父进程。在这里, K o r n S h e l l是两个进程的父进程。但是,如果在后 台执行此管道线: ps -xj | cat1 & 其结果显示现在K o r n S h e l l以与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 两个进程7 1 2和7 1 3都处在后台进程组7 1 2中。 9.10 孤儿进程组 一个父进程已终止的进程称为孤儿进程 (orphan process),这种进程由i n i t进程收养。现在 我们要说明整个进程组也可成为孤儿,以及 P O S I X . 1如何处理它。 实例 考虑一个进程,它 f o r k了一个子进程然后终止。这在系统中是经常发生的,并无异常之 第 9章 进 程 关 系 1 9 3下载 处,但是在父进程终止时,如果该子进程停止(用作业控制)又将如何呢?子进程如何继续, 以及子进程是否知道它已经是孤儿进程?程序 9 - 1是 这种情况的一个例子。下面要说明该程序的某些新 特征。图9 - 1 0显示了程序 9 - 1已经起动,父进程已经 f o r k了子进程后的情况。 这里,假定使用了一个作业控制 s h e l l。回忆前面 所述,s h e l l将前台进程放在一个进程组中 (本例中是 5 1 2 ),s h e l l则留在自己的组内( 4 4 2 )。子进程继承其父 进程( 5 1 2 )的进程组。在f o r k之后: • 父进程睡眠5秒钟,这是一种让子进程在父进程 终止之前运行的一种权宜之计。 • 子进程为挂断信号(S I G H U P)建立信号处理程 序。这样就能观察到S I G H U P信号是否已送到子进程。 (第1 0章将讨论信号处理程序。) • 子进程 用 k i l l函数向其 自身发送 停止信号 (S I G T S T P)。这停止了子进程,类似于用终端挂起字符( C t r l - Z)停止一个前台作业。 • 当父进程终止时,该子进程成为孤儿进程,共父进程 I D成为1,也就是i n i t进程I D。 • 现在,子进程成为一个孤儿进程组的成员。 P O S I X . 1将孤儿进程组( orphaned process g r o u p)定义为:该组中每个成员的父进程或者是该组的一个成员,或者不是该组所属对话期 的成员。对孤儿进程组的另一种描述可以是:一个进程组不是孤儿进程组的条件是:该组中有 一个进程,其父进程在属于同一对话期的另一个组中。如果进程组不是孤儿进程组,那么在属 于同一对话期的另一个组中的父进程就有机会重新起动该组中停止的进程。 在这里,进程组中所有进程的进程(如进程 5 1 3的父进程1 )属于另一个对话期。所以此进 程组是孤儿进程组。 • 因为在父进程终止后,进程组成为孤儿进程组, P O S I X . 1要求向新孤儿进程组中处于停 止状态的每一个进程发送挂断信号(S I G H U P),接着又向其发送继续信号(S I G C O N T)。 • 在处理了挂断信号后,子进程继续。对挂断信号的系统默认动作是终止该进程,为此必 须提供一个信号处理程序以捕捉该信号。因此,我们期望 s i g _ h u p函数中的p r i n t f会在p r _ i d s函数 中的p r i n t f之前执行。 程序9-1 创建一个孤儿进程组 1 9 4 U N I X环境高级编程 下载 图9-10 将成为孤儿的进程组的实例 对话期 父进程 (PID 512) 登录s h e l l (PID 442) 子进程 (PID 513) 进程组5 1 2 进程组4 4 2 下面是程序9 - 1的输出: $ a . o u t 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 注意,因为两个进程,登录s h e l l和子进程都写向终端,所以s h e l l提示符和子进程的输出一起出 现。正如我们所期望的那样,子进程的父进程I D变成1。 注意,在子进程中调用 p r _ i d s后,程序企图读标准输入。正如前述,当后台进程组试图读 控制终端时,则对该后台进程组产生 S I G T T I N。但在这里,这是一个孤儿进程组,如果内核用 此信号停止它,则此进程组中的进程就再也不会继续。 P O S I X . 1规定,r e a d返回出错,其e r r n o 设置为E I O(在作者所用的系统中其值是5)。 最后,要注意的是父进程终止时,子进程变成后台进程组,因为父进程是由 s h e l l作为前台 作业执行的。 在1 9 . 5节的p t y程序中将会看到孤儿进程组的另一个例子。 9 . 11 4.3+BSD实现 上面说明了进程、进程组、对话期和控制终端的各种属性,值得观察一下所有这些是如何 实现的。下面简要说明 4 . 3 + B S D的实现。S V R 4实现的某些详细情况则参见 Wi l l i a m s〔1 9 8 9〕。 第 9章 进 程 关 系 1 9 5下载 图9 - 11显示了4 . 3 + B S D的各种数据结构。 图9 - 11 对话期和进程组的4 . 3 + B S D实现 下面说明图中的各个字段。从s e s s i o n结构开始。每个对话期都分配了这样一种结构(例如, 每次调用s e t s i d时)。 • s_count是该对话期中的进程组数。当此计数器减至0时,则可释放此结构。 • s_leader是指向对话期首进程p r o c结构的指针。如上所述,4 . 3 + B S D不保持对话期I D字段, 而S V R 4则保持此字段。 • s_ttyvp是指向控制终端v n o d e结构的指针。 • s_ttyp是指向控制终端t t y结构的指针。 在调用s e t s i d时,在内核中分配一个新的对话期结构。s _ c o u n t设置为1,s _ l e a d e r设置为调用 进程的p r o c结构的指针,因为新对话期没有控制终端,所以s _ t t y v p和s _ t t y p设置为空指针。 接看说明t t y结构。每个终端设备和每个伪终端设备均在内核中分配这样一种结构(第 1 9章 将对伪终端作更多说明。) • t_session指向将此终端作为控制终端的s e s s i o n结构(注意,t t y结构指向s e s s i o n结构,结构 也指向tty结构)。终端在失去载波信号(见图9-7)时使用此指针将挂起信号送给对话期首进程。 • t_pgrp指向前台进程组的p g r p结构。终端驱动程序用此字段将信号送向前台进程组。由输 入特殊字符(中断、退出和挂起)而产生的三个信号被送至前台进程组。 • t_termios是包含所有这些特殊字符和与该终端有关信息(例如,波特率、回送打开或关 闭等)的结构。第11章将再说明此结构。 1 9 6 U N I X环境高级编程 下载 t t y结构 s e s s i o n结构 v n o d e结构 p r o c结构p r o c结构p r o c结构 p g r p结构 进程组成员链接表 设备的 实际i节点 • t_winsize是包含终端窗口当前尺寸的 w i n s i z e结构。当终端窗口尺寸改变时,信号 S I G W I N C H被送至前台进程组。11 . 1 2节将说明如何设置和存取终端当前窗口尺寸。 注意,为了找到特定对话期的前台进程组,内核从 s e s s i o n结构开始,然后用s _ t t y p得到控 制终端的t t y结构,然后用t _ p g r p得到前台进程组的p g r p结构。 p g r p结构包含一个进程组的信息。 • pg_id是进程组I D。 • pg_session指向此进程组所属的s e s s i o n结构。 • pg_mem是指向此进程组第一个进程p r o c结构的指针。p r o c结构中的p _ p g r p n x t指向此组中 的下一个进程,进程组中最后一个进程p r o c中的p _ p g r p n x t则为空指针。 p r o c结构包含一个进程的所有信息。 • p_pid包含进程I D。 • p_pptr是指向父进程p r o c结构的指针。 • p_pgrp指向本进程所属的进程组的p g r p结构。 • p_pgrpnxt是指向进程组中下一个进程的指针。 最后还有一个v n o d e结构。在打开控制终端设备时分配此结构。进程对 / d e v / t t y的所有访问 都通过v n o d e结构。在图9 - 11中,实际i节点是v节点的一部分。3 . 1 0节曾提及这是4 . 3 + B S D的实 现方法,而S V R 4则将v节点存在i节点中。 9.12 小结 本章说明了进程组之间的关系——对话期,它由若干个进程组组成。作业控制是当今很多 U N I X系统所支持的功能,本章说明了它是如何由支持作业控制的 s h e l l实现的。在这些进程关 系中也涉及到了/ d e v / t t y。 所有这些进程的关系都使用了很多信号方面的功能。下一章将详细讨论U N I X中的信号机制。 习题 9.1 考虑6 . 7节中说明的u t m p和w t m p文件,为什么l o g o u t记录是由4 . 3 + B S D的init 进程写 的?对于网络登录的处理与此相同吗? 9.2 编写一段程序,要求调用f o r k并在子进程中建立一个新的对话期。验证子进程变成了 进程组组长且不再有控制终端。 第 9章 进 程 关 系 1 9 7下载
还剩18页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 3 金币 [ 分享pdf获得金币 ] 0 人已下载

下载pdf

pdf贡献者

xnchall

贡献于2015-09-02

下载需要 3 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf