Loading...

当前位置:资讯中心主页 >Linux >文章内容

  • 第九章 进程关系
  • 来源:Blog.ChinaUnix.net作者:Blog.ChinaUnix.net 发布时间:2008-04-07 11:54:11
    • 域名注册

    • 域名惊喜价格 cn域名1元注册
    • com域名39.9

      虚拟主机

    • 主机按月支付,低至19元/月
    • 超大流量,可开子站点

      VPS主机

    • 特惠VPS168元/月,4-8M独享带宽保证
    • 独立操作系统,无限开站点
          

    ?  
    9?1〓引言?  
    在上一章我们已了解到进程之间具有关系。首先,每个进程有一个父进程。当子进  
    程终止时  
    ,父进程会得到通知并能取得子进程退出状态。在8?6节说明waitpicl函数时,我  
    们也提到  
    了进程组,及怎么等待进程组中的任一个进程终止。?  
    本章的更周详地说明进程组及POSIX?1引进的对话期新概念。我们也将介绍log  
    in shell(  
    是我们登录时为我们调用的)和所有从login shell起动的进程之间的关系。?  
    在说明这些关系时不可能不谈及信号,而谈论信号又需要非常多本章介绍的概念。如  
    果你不熟  
    悉Unix信号,则可能先要浏览一下第十章。?  
    9?2〓终端登录?  
    先看一看登录到Unix系统时所执行的各个程式。在早期的Unix系统中,例如Versi  
    on7,用户  
    用哑终端(通过RS-232连到主机)进行登录。终端或是在地的(直接连接)或是远  
    程的(通  
    过调制解调器连接)。在这两种情况下,login都经由系统核中的终端设备驱动程式  
    。例如,  
    在PDP-11上常用的设备是DH-11和DZ-11。因为连到主机上的终端设备数已确定,  
    所以同时  
    的  
    login数也就有了已知的上限。下面说明的登录过程适用于使用一个RS-232终端登  
    录到Unix  
    系统中。?  
    4?3+BSD终端登录?  
    登录过程在过去十五年中并没有多少改动。系统管理者创建一个通常名为/etc/  
    thys的文  
    件  
    ,其中,每个终端设备有一行。每一行说明设备名和传到getty程式的参数。这些  
    参数说明  
    了终端的波特率等。当系统自举时,系统核创建进程ID1,也就是init进程。init  
    进程供系  
    统进入多用户状态。init读文件/etc/ttys,对每一个允许登录的终端设备,ini  
    t拥有一次  
    fork,他所生成的子进程则执行(exec)程式getty。这种情况示于图9?1中。?  
    图9?1 init生成进程使终端可用于login?  
    图9?1中,各个进程的实际用户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保留传给他的环境,也可  
    将其他环  
    境字符串加到该环境中,不过不要代换他。图9?2显示了在login刚被调用后这些  
    进程的状  
    态。??  
    图9?2 login刚被调用后各进程的状态?  
    因为init进程具有终极用户优先权,所以图9?2中的所有进程都有终极用户优先权  
    。图9?2  
    中底部三椎是个进程,他们的进程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文件,检查新邮  
    件及其  
    他一些功能。不过考虑到本市的内容,我们主要关心上面所说的功能。?  
    回忆在8?10节中对setuid函数的讨论,因为setuid是由终极用户调用的,他更改  
    所有三个  
    用户ID:实际、有效和保存的用户ID。login在较早时间调用的setgid对所有三个  
    组ID也有  
    同样效果。?  
    到此为止,登录用户的login shell开始运行。其父进程ID是init进程ID(进程ID1  
    ),所以当  
    此login shell终止时,init会得到通知(接到SIGCHLD信号),他会对该终端重复全  
    部上述过  
    程。登录shell的文件描述符0,1和2设置为终端设备。图9?3显示了这种安排。?  
       
    图9?3终端登录结束后的有关进位?  
    目前,login shell读其启动文件(Bourne shell和Korn shell是profile,C shell  
    是Cshrc和login)。这些启动文件通常改动某些环境变量,加上一些环境变量。例  
    如,非常多用户设置他们自己的PATH,常常提示实际终端类型(TERM)。当执行完启  
    动文件后,用户最后得到shell的  
    提示符,并能键入命令。?  
    SVR4终端登录?  
    SVR4支持两种形成的终端登录:(a)getty方式,这和上面对4?3+BSD所说明的一  
    样,(b)tt  
    ymon登录,这是SVR4的一种新功能。通常,getty用于控制台,ttymon则用于其他  
    终端的登  
    录。?  
    ttymon是名为SAF(Service Access Facility,服务存取设施)的一部分。按照本市  
    的目的,  
    我们只简单说明从init到login shell之间工作过程,最后结果和图9?3中所示相  
    似。init  
    是sac(服务存取控制器)的文进程,sac调用fork,然后其子进程exec ttymon程式  
    ,此时系  
    统  
    进入多用户状态。ttymon监视列于设置文件中的所有终端端口,当用户键入login  
    名时,他  
    调用一次fork。在此之后该子进程又exec登录用户的login shell,于是到达了图  
    9?3中所  
    示的位置。一个差别是login shell的父进程目前是ttymon,而getty login中,l  
    ogin shel  
    l的父进程是init。?  
    9?3〓网终登录?  
    4?3+BSD网络登录?  
    在上节所述的终端登录中,init知道哪些终端设备可用来进行登录,并为每个设备  
    生成一个g  
    etty进程。不过,对网络登录则情况有所不同,所有登录都绎由系统核的网络界面  
    驱动程式  
    (例如:以太网驱动程式),事先并不知道将会有多少这样的登录。不是使一个进程  
    等待每一  
    个可能的登录,而是必须等待一个网络连接请求的到达。在4?3+BSD中,有一  
    个称为in  
    etd的进程(有时称为Internet superserver),他等待大多数网络连接。在本市中  
    ,我们将  
    说明4?3+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连接交  
    换数据。  
    所发生的是起动的用户目前登录到了服务器进程的主机(自然,用户需要在服务器  
    进程主机  
    上有一个有效的账号。)图9?4显示了在执行TELNET服务器进程(称为telnetd)中所  
    涉及的进  
    程式列。?  
    然后,telnetd进程打开一个伪终端设备,并用fork生成一个子进程(在十九章中将  
    周详说明  
    伪终端。)父进程处理通过网络连接的通信,子进程则exec login程式。父子进程  
    通过伪终  
    端  
    相连接。在调用exec之前,子进程使其文件描述符0,1,2和伪终端相连。如果登  
    录正确,l  
    ogin就执行在9?2节中所述的同样步骤-更改当前工作目录为起始目录,设置登录  
    用户的组I  
    D和用户ID,及登录用户的初始环境。然后login用exec将其自身代换为登录用户  
    的。图9  
    ?5显示了到达这一点时进程安装。?  
    图9?4 执行TELNET服务者中涉及的进程式列?  
    图9?5 为网络login设置了fd0,1,2后的进程安排?  
    非常明显,在伪终端设备驱动程式和终端实际用户之间有非常多事情在进行看。在第十  
    九章中较  
    周详地说明伪终端之前,我们会介绍和这种安排相关的所有进程。?  
    需要理解的重要之点是:当通过终端(图9?3)或网络(图9?5)登录时,我们得到一  
    个login  
    shell,其标准输入、输出和标准出错连接到或一个终端设备上或一个伪终端  
    设备上。  
    在下一节中我们会了解到这一login shell是个POSIX?1对话期的开始,而此终  
    端或伪终  
    端则是对话期的控制终端。?  
    SVR4网络登录?  
    SVR4中网络登录的情况和4?3+BSD中的几乎相同。同样使用了inetd服务器进程,  
    不过在SVR  
    4  
    中inetd是作为一种服务由服务存取控制器调用的,其父进程不是init。最后得到  
    的和图9?  
    5中相同。?  
    9?4〓进程组?  
    每个进程除了有一进程ID之外,还属于一个进程组,在第十章讨论信号时还会涉及  
    进程组  
    。?  
    一个进程组是个或多个进程的集合。每个进程组有一个唯一的进程组ID。进程组  
    ID类似于  
    进程ID-他是个正整数,并可存放在pid-t数据类型中。函数getpgrp返回调用进  
    程的进程  
    组ID。?  
    #include <sys/types?h>?  
    #include <unistd?h>??  
    pid 迹茫模*常病? getpgrp(void);?  
    返回:调用进程的进程组ID?  
    在非常多贝壳莱类的系统中,包括4?3+BSD,这一函数的参数是pid,返回该进程的  
    进程组。  
    上面所示的原型号POSIX?1版本。?  
    每个进程组有一个组长进程。组长进程的标识是,其进程组ID等于其进程ID。?  
    进程组组长能创建一个进程组,创建该组中的进程,然后终止。只要在某个进程  
    组中有一  
    个进程存在,则该进程组就存在,这和其组长进程是否终止无关。从进程组创建开  
    始到其中  
    最后一个进程离开为止的时间区间称为进程组的生命期。在某个进程组中的最后一  
    个进程可  
    以终止,或参加另一个进程组。?  
    一个进程调用setpgid能参加一个现存的组或创建一个新进程组。(下一节中将  
    说明用se  
    tsid也能创建一个新的进程组。)?  
    #include<sys/types?h>?  
    #include<unistd?h>?  
    int setpgid(pid 迹茫模*常病? pid,pid 迹茫模*常病? pgid);?  
    返回:若成功为0,出错为-1?  
    这将pid进程的进程组ID设置为pgid。如果这两个参数相等,则由pid指定的进程变  
    成进程组  
    组长。?  
    一个进程只能为他自己或他的子进程设置进程组ID。在他的子进程调用了exec后,  
    他就不再  
    能改动该子进程的进程组ID。?  
    如果pid是0,则使用调用者的进程ID。另外,如果pgid是0,则由pid指定的进程I  
    D被用作为  
    进程组ID。?  
    如果系统不支持作业控制(在9?8节说明作业控制),那么就不定义-POSIX-JOB-CO  
    NTROL,在  
    这种情况下,此函数出错返回,errno设置为ENOSYS。?  
    在大多数作业控制shell中,在fork之后调用此函数,使父进程设置其子进程的进  
    程组ID,  
    使子进程设置其自己的进程组ID。这些调用中有一个是冗余的,但这样做能确保  
    父、  
    子进程在进一步操作之前,子进程都进入了该进程组。如果不这样做的话,那么就  
    产生一个  
    竞态条件,因为他依赖于那一个进程先执行。?  
    在讨论信号时,我们将说明怎么将一个信号送给一个进程(由其进程ID标识)或送给  
    一个进程  
    组(由进程组ID标识)。相类似,Waitpid则可被用来等待或一个进程或指定进  
    程组中的  
    一个进程。?  
    9?5〓对话期?  
    一个对话期是个或多个进程组的集合。例如,能有图9?6中所示的安排。其中  
    ,在一个  
    对话期中有三个进程组。通常是由shell的管道线将几个进程编成一组的。例如,  
    图9?6中  
    的安排可能是由下列形式的shell命令形成的:?  
    procl|proc2 &?  
    proc3|proc4|proc5?  
    图9?6 在进程组和对话期中的进程安排?  
    一个进程调用setsicl函数就可建立一个新对话期。?  
    #include<sys/types?h>?  
    #include<unistd?h>?  
    pid 迹茫模*常病? setsid(void);?  
    返回:若成功为进程组ID,出错为-1?  
    如果调用此函数的进程不是个进程组的组长,则此函数创建一个新对话期,他所  
    造成的结果是:?  
    1?此进程变成该新对话期的对话期首进程(对话期首进程是创建该对话期的进程)  
    。此进程  
    是该新对话期中的唯一一个进程。?  
    2?此进程成为一个新进程组的组长进程。新进程组ID是此调用进程的进程ID。?  
       
    3?此进程没有控制终端。(下一节讨论控制终端)。如果在调用setsid之前此进程  
    有一个控  
    制终端,那么这种联系也被解除。?  
    如果此调用进程已是个进程组的组长,则此函数出错返回。为了确保不处于这  
    种情况,  
    通常的实践是先调用fork,然后使其父进程终止,而子进程则继续。因为子进程继  
    承了父进  
    程的进程组ID,而其进程ID则是新发现的,两者不可能相等,所以这就确保了子进  
    程不是个进程组的组长。?  
    POSIX?1只说到对话期首进程。和进程ID和进程组ID不同,及有对话期ID。显然,  
    对话期首  
    进程是具有唯一进程ID的单个进程,所以我们能将对话期首进程的进程ID视为对  
    话期ID。  
    SVR4就是这样处理的。SVID和SVR4的setsid(2)手册页谈到了以此种方式定义的对  
    话期ID。  
    这是一种实现细节,他不是POSIX?1中定义的,4?3+BSD也不支持他。?  
    SVR4有一个getsid函数,他返回一个进程的对话期ID。此函数不是POSIX?1的所属  
    部分,4  
    ?3+BSD也不支持此函数。?  
    9?6〓控制终端?  
    对话期和进程组有一些其他特性:?  
    ?一个对话期能有一个独立的控制终端。这通常是我们在其上登录的终端设备(  
    在终端登录情况)或伪终端设备(在网络登录情况)。?  
    ?建立和控制终端连接的对话期首进程,被称之为控制进程。?  
    ?一个对话期中的几个进程组可被分成一个前台进程组及一个或几个后出进程组。?  
    ?如果一个对话期有一个控制终端,则他有一个前台进程组,其他进程组则为后台  
    进程组。  
    ?无论何时键入中断键(常常是DELETE或Control-C)或退出键(常常是tonforl-\  
    ),就会造  
    成将中断信号或退出信号送前台进程组的所有进程。?  
    ?如果终端界面检测到调制解调器已脱开连接,则将挂断信号送至控制这些特性  
    示于图9  
    ?7中。?  
    图9?7进程组、对话期和控制终端?  
    通常,我们不必担心控制终端--当我们登录时,将自动为我们建立控制终端。?  
    一个系统怎么发现一个控制终端依赖于实现。19?4节中将说明实际步骤。?  
    当对话期首进程打开第一个尚未和一个对话期相关联的终端设备时,SVR4将此作为  
    控制终端  
    分配给此对话期。这假定对话期首进程在调用open时及有指定O-NOCTTY标志(3?3  
    节)。?  
    当对话期首进程以request参数为TIOCSCTTY调用ioctl时(第三个参数是空指针),  
    4?3+B  
    SD为对话期分配控制终端。为使此调用成功执行,此对话期不能已有一个控制  
    终端。(  
    通常ioctl调用紧跟在setsid调用之后,setsid确保此进程是个没有控制终端的  
    对话期首  
    进程。)4?3+BSD?  
    不使用POSIX?1中对open函数所说明的O 迹茫模*常病?OCTTY标志。?  
    有时不管标准输入,标准输出是否重新定向,程式都要和控制终端交互作用。确保  
    程式读写  
    控制终端的方法是打开文件/dev/tly,在系统核中,此特别文件是控制终端的同  
    义语。自  
    然,如果程式没有控制终端,则打开此设备将失败。?  
    典型的例子是读一个口令字的getpass(3)函数(自然,终端回送被关闭)。这一函数  
    由?cryp  
    t(1)?程式调用,而此程式则可用于管通线中。例如:?  
    crypt <salaries | lpr?  
    他将文件salaries解密,然后经由管道将输出送至打印假脱机程式。因为crypt从  
    其标准输  
    入读输入文件,所以标准输入不能用于输入口令字。不过,crypt的一个设计特征  
    是每次运  
    行此程式时,我们都应输入加密口令字,这样也就不必将口令字存放在文件中。  
    ?  
    已知道有一些方法能破译crypt程式使用的密码。关于密码文件的周详情况请  
    参见Garfi  
    nkel和Spafford [1991]。?  
    9?7〓tcgetpgrp和tcsetpgrp函数?  
    需要有一种方法用其能通知系统核那一个进程组是前台进程组,这样,终端设备  
    驱动程式  
    就能了解将终端输入和终端产生的信号送到何处(图9?7)。?  
    #include<sys/types?h>?  
    #include<unistd?h>?  
    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,这两个函数才是被定义了的。否则他们出错返回。?  
    9?8〓作业控制?  
    作业控制是贝克莱在1980年左右加到UNIX的一个新特性。他允许我们在一个终端上  
    起动多个  
    作业(进程组),控制哪一个作业能存取该终端,及哪些作业在后台运行。作业  
    控制需求  
    三种形式的支持:?  
    1?支持作业控制的shell。?  
    2?系统核中的终端驱动程式必须支持作业控制。?  
    3?必须提供对某些作业控制信号的支持。?  
    SVR3提供了一种不同形式的作业控制,称为shell层。不过POSIX?1选择了贝克莱  
    形成的作  
    业控制,这也是我们在这里所说明的。回忆图2?7,如果系统支持作业控制,则定  
    义常数-P  
    OSIX-JOB-CONTROL。?  
    FIPS151-需求POSIX?1作业控制。?  
    SVR4和4?3+BSD支持POSIX?1作业控制。?  
    从shell使用作业控制功能角度观察,我们能在前台或后台启动一个作业。一个  
    作业只是  
    几个进程的集合,通常是个进程管道。例如:?  
    vi main?C?  
    他在前台起动了只有一个进程组成的一个作业。下面的命令:?  
    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>Make?out &?  
    [1]  1475?  
    $ pr *?c | lpr &?  
    [2] 1490?  
    $   键入回车?  
    [2]+Done pr *?c | lpr &?  
    [1]+Done make all>Make?out &?  
    make是作业号1,所起动的进程ID是1475。下一个管道线是作业号2,其第一个进程  
    的进程ID  
    是1490。当作业已完成而且我们键入回车时,shell通知我们作业已完成。我们  
    需要键入  
    回车的理由是:使shell打印其提示符。shell并不在所有随意的时间打印后台作业  
    的状态改  
    变-他只在打印其提示符之前这样做。如果不这样处理,则在我们正输入一行时,  
    他也可能  
    输出。?  
    我们能键入一个影响前台作业的特别字符-挂起键(典型的是Control-Z)和终端进  
    行交互  
    作用。键入此字符使终端驱动程式将信号SIGTSTP送至前台进程组中的所有进程,  
    后台进程  
    组作业则不受影响。实际上有三个特别字符可使终端驱动程式产生信号,并将他们  
    送至前台  
    进程组,他们是:?  
    ?中断字符(典型的是DELETE或Control-C)产生SIGINT。?  
    ?退出字符(典型的是Control-\)产生SIGQUIT。?  
    ?挂起字符(典型的是Control-Z)产生SIGTSTP。?  
    在第十一章中将说明可将这三个字符更改为任意其他所选择的字符,及怎么使终  
    端驱动程式不处理这些特别字符。?  
    终端驱动程式必须处理和作业控制有关的另一种情况。我们能有一个前台作业,  
    若干个后台作业,这些作中哪一个接收我们在终端上键入的字符呢?只有前台作业  
    接收终端输入。如果后台作业试图读终端,那么这并不是个错误,不过终端驱动  
    程式检测这种情况,并且发送一个特定信号SIGTTIN给后台作业。这通常会停止此后  
    台作业,而有关用户则会得到这种情况的通知,然后我们就可将此作业转为前台  
    作业运行,于是他就可读终端。下列操作过程  
    显示了这一点:?  
    $ cat>temp?foo & 在后台启动,但将从标准输入读?  
    [1] 1681?  
    $ 键入回车?  
    [1]+Stopped (tty input) cat>temp?foo &?  
    $ fg %1 使1号作业成为前台作业?  
    cat>temp?foo shell告诉我们目前哪一个作业在前台?  
    hello,world 输入1行?  
    ^D 键入我们的文件结束符?  
    $ cat temp?foo 键入我们的文件结束符?  
    hello,world 检查该行已送入文件?  
    shell在后台起动cat进程,不过当cat试图读其标准输入(控制终端)时,终端驱动  
    程式知道  
    他是个后台作业,于是将SIGTTIN信号送至该后台作业。shell检测到其子进程的状  
    态改动(  
    回乙8?6节中对wait和waitpid的讨论),并通知我们该作业已被停止。然后,我们  
    用shell  
    的fg命令将此停止的作业送入前台运行。(关于作业控制命令,例如fg和bg的周详  
    情况,以  
    及标识不同作业的各种方法请参阅有关shell的手册页。)这样做使shell将此作业  
    转为前台  
    进程组(tcsetpgrp),并将继续信号(SIGCONT)送给该进程组。因为该作业目前前台  
    进程组中  
    ,所以他能读控制终端。?  
    如果后台作业输出到控制终端又将发生什么呢?这是个我们能允许或禁止的选  
    择项。通  
    常,我们能用stly(1)命令改动这一选择项。(第十一章将说明在程式中怎么改动  
    这一选择  
    项)。下面显示了这种操作过程:?  
    $ cat temp?foo & 在后台执行?  
    [1] 1719 在提示符后出现后台作业的输出?  
    [1]+Done cat temp?foo &?  
    $ stty tostop 使后台作业可能向控制终端输出?  
    $ cat temp?foo & 在后台再次执行?  
    [1] 1721?  
    $ 键入回车,发现作业已停止?  
    [1]+Stopped(tty output) cat temp?foo&?  
    $ fg %1 将停止的作业恢复为前台作业?  
    cat temp?foo shell告诉我们目前哪一个作业在前台?  
    hello,world 该作业的输出?  
    图9?8摘录了我们已说明的作业控制的某些功能。?  
    图9?8 对于前台、后台作业及终端驱动程式的作业控制功能摘要?  
    穿过终端驱动程式框的实线表示:终端I/O和终端产生的信号总是从前台进程组  
    连接到实  
    际终端。对应于SIGTTOU信号的虚线表示,后台进程组进程的输出出目前终端上是  
    个选择项  
    。?  
    是否需要作业控制:这是个有非常多争论的问题。作业控制是在窗口终端广泛得到  
    应用之前  
    设计和实现的。非常多人认为设计得好的窗口系统已免除了对作业控制的需要。某  
    些人抱怨  
    作业控制的实现需求得到系统核、终端驱动程式、shell及某些应用程式的支持  
    ,是吃力  
    不讨好的事情。某些人在窗口系统中使用作业控制,他们认为两者都需要。不管你  
    的意见如  
    何,作业控制是POSIX?1及FIPS151-1的组成部分,他还将继续存在。?  
    9?9〓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字段。在4?3+BSD之下,使用命令ps-xj -otp  
    gid。?  
    注意,将一个进程和一个终端进程组ID(TPGID列)相关联是名字使用不当。一个进  
    程并没有  
    一个终端进程控制组属性。一个进程属于一个进程组。而一个进程组属于一个对话  
    期。对话  
    期可  
    能有,也可能及有控制终端。如果他确有一个控制终端,则此终端设备知道其前台  
    进程的进  
    程组ID。这一值能用tcsetpgrp函数在终端驱动程式中设置。(如图9?8中所示。  
    )前台进  
    程组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>temp?foo &?  
    在有作业控制时,后台作业被放在后台进程组,如果后台作业试图读控制终端,则  
    会产生信号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的子进程,而执行在管道中其他命令的  
    进程则是  
    该最后进程的子进程。图9?9显示了所发生的情况。?  
    图9?9 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的子进程,而执行在管道中其他命令的  
    进程则是  
    该最后进程的子进程。图9?9显示了所发生的情况。?  
    图9?9 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中。?  
    9?10〓孤儿进程组?  
    一个父进程已终止的进程称为弧儿进程(orphan process),这种进程由init进程收  
    领。现  
    在我们要说明整个进程组也可成为孤儿,及POSIX?1怎么处理他。?  
    实例?  
    考虑一个进程,他fork了一个子进程然后终止。这在系统中是经常发生的,并无异  
    常之处,  
    不过在父进程终止时,如果该子进程停止(用作业控制)又将怎么呢?子进程怎么继  
    续,及  
    子进程是否知道他已是孤儿进程?程式9?1是这种情况的一个例子。下面要说明  
    该程式的  
    某些新特征。图9?10显示了程式9?1已起动,父进程已fork了子进程后的情  
    况。?  
    图9?10 将成为孤儿的进程组的实例?  
    这里,我们假定使用了一个作业-控制shell。回忆前面所述,shell将前台进程放  
    在一个进  
    程组中(本例中是512),shell则留在自己的组内(442)。子进程继承其父进程(512  
    )的进程组  
    。在fork之后:?  
    ?父进程睡眠5秒钟,这是一种让子进程在父进程终止之前运行的一种权宜之计。  
    ?  
    ?子进程为挂断信号(SIGHUP)建立信号处理程式。这样我们就能观察到SIGHUP信号  
    是否已送  
    到子进程。(在第十章讨论信号处理程式)。?  
    ?子进程用kill函数向其自身发送停止信号。这停止了要进程,这类似于用终端挂  
    起字符停  
    止一个前台作业。?  
    ?当父进程终止时,该子进程成为孤儿进程,共父进程ID成为1,也就是init进程  
    ID。?  
    ?到此点时,子进程成为一个孤儿进程组的成员。POSIX?1将孤儿进程组定义为:  
    该组中每  
    个成员的父进程或是该组的一个成员,或不是该组所属对话期的成员。对孤儿  
    进程组的  
    另一种描述能是:一个进程组不是孤儿进程组的条件是:该组中有一个进程,其  
    父进程在  
    属于同一对话期的另一个组中。如果进程组不是孤儿进程组,那么在属于同一对话  
    期的另一  
    个组中的父进程就有机会重新起动该组中停止的进程。?  
    在我们这里,进程组中进程(513)的父进程(1)属于另一个对话期。所以此进程组是  
    弧儿进程  
    组。?  
    ?因为在父进程(512)终止后,进程组成为弧儿进程组,POSIX?1需求向新成为孤  
    儿进程组  
    中  
    处于停止状态的每一个进程发送挂断信号(SIGHUG),接着又向其发送继续信号(SI  
    GCONT)。  
    ?  
    ?在处理了挂断信号后,子进程继续。对挂断信号的系统默认动作是终止该进程,  
    为此我们  
    必须提供一个信号处理程式以捕捉该信号。因此,我们能期望sig-hup函数中的  
    printf会  
    在pr-ids函数中的printf之前执行。?  
    程式9?1 创建一个孤儿进程组?  
    下面是程式9?1的输出?  
    $ a?out?  
    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。但在这里,这是个弧儿进程组,如  
    果系统核  
    用此信号停止他,则在此进程组中的进程就再也不会继续。POSIX?1规定,read出  
    错返回,  
    其errno设置为E10(在作者所用的系统中其值是5)。?  
    最后,要注意的是在父进程终止时,子进程变成后台进程组,因为父进程是由she  
    ll作为前  
    台作业执行的。?  
    在19?5的pty程式中我们将会看到孤儿进程组的另一个例子。?  
    9?11〓4?3+BSD实现?  
    上面说明了进程、进程组、对话期和控制终端的各种属性,值得观察一下所有这些  
    是怎么  
    实现  
    的。下面简要说明4?3+BSD的实现。SVR4实现的某些周详情况则参见Williams〔1  
    989〕  
    。图9?11显示了4?3+BSD的各种数据结构。?  
    图9?11 对话期和进程组的4?3+BSD实现?  
    让我们说明图中示出的各种字段。从对话期结构开始。为每个对话期分配一个这种  
    结构(  
    例如,每次调用setsid时)。?  
    ?s?count是该对话期中的进程组数。当此计数器减至0时,则可释放此结构。?  
       
    ?S?Leader是指向对话期首进程proc结构的指针。如前面提及的,4?3+BSD不保  
    持对话期I  
    D字段,而SVR4则保持此字段。?  
    ?S?ttyvp是指向控制终端unode结构的指针。?  
    ?S?ttyp是指向控制终端tly结构的指针。?  
    在调用setsid时,在系统核中分配一个新的对话期结构。S?Count设置为1,S?l  
    eader设置  
    为  
    调用进程的proc结构的指针,因为新对话期没有控制终端,所以s?ttyvp和S?Hy  
    p设置为  
    定指针。?  
    接看说明tly结构。对于每个终端设备和每个伪终端设备在系统核中分配一个这样  
    的结构。(  
    在第十九章对伪终端作更多说明)?  
    ?t?session指向将此终端作为控制终端的对话期结构。(注意,tly结构指向对话  
    期结构,  
    对话期结构则也指向tly结构。)终端在失去载波信号(图9?7)时使用此指针将挂起  
    信号送给  
    对话期首进程。?  
    ?t?pgrp指向前台进程组的pgrp结构。终端驱动程式用此字段将信号送在前台进  
    程组。由  
    输入特别字符(中断、退出和挂起)而产生的三个信号被送至前台进程组。?  
    ?t?termios是包含所有这些特别字符和和该终端有关信息的结构。(例如,波特  
    率,回送  
    打开或关闭等)。在第十一章中将再说明此结构。?  
    ?t?winsize是包含终端窗口当前尺寸的winsize结构。当终端窗口尺寸改动时,  
    信号SIGWI  
    NCH被送至前台进程组。在11、12节中将说明怎么设置和存取终端当前窗口尺寸。  
    ?  
    注意,为了找到特定对话期的前台进程组,系统核从对话期结构开始,然后按s-t  
    typ得到控制终端的tty结构,然后按t-pgrp得到前台进程组的pgrp结构。?  
    pgrp结构包含一个进程的信息。?  
    ?pg?id是进程组ID。?  
    ?pg?session指向此进程组所属的对话期结构。?  
    ?pg?mem是指向此进程组第一个进程的proc结构的指针。在proc结构中的p?pgr  
    pnxt指向此组中的下一个进程,进程组中最后一个进程proc中的p?pgrpnxt则为定  
    指针。?  
       
    proc结构包含一个进程的所有信息。?  
    ?p?pid包含进程ID。?  
    ?p?pptr是指向父进程proc结构的指针。?  
    ?p?pgrp指向本进程所属的进程组的pgrp结构。?  
    ?p?pgrpnxt如上面已说明的,这是指向进程组中下一个进程的指针。?  
    最后更有一个vnode结构。在打开控制终端设备时分配此结构。进程对/dev/tty  
    的所有访问都通过vnode结构。在图9?11中,实际i?node是v?node的一部分。  
    在3?10中我们曾提及这是4?3+BSD的实现方法,而SVR4则将V?node存在i?node中。  
    ?  
    9?12〓摘要?  
    本章说明了进程组之间的关系--对话期,他由若干个进程组组成。作业控制是当今  
    非常多Unix系统所支持的功能,我们说明了他是怎么由支持作业控制的shell实现的。在  
    这些进程关系涉及到了/dev/tty。在所有这些进程关系中,都用了非常多信号方面的  
    功能。下一章周详地讨论Unix中的信号机制  
    。〖LM〗


  • 以上内容由 华夏名网 搜集整理,如转载请注明原文出处,并保留这一部分内容。

      “华夏名网” http://www.sudu.cn 和 http://www.bigwww.com 是成都飞数科技有限公司的网络服务品牌,专业经营虚拟主机,域名注册,VPS,服务器租用业务。公司创建于2002年,经过6年的高速发展,“华夏名网”已经成为我国一家知名的互联网服务提供商,被国外权威机构webhosting.info评价为25大IDC服务商之一。

    华夏名网网址导航: 虚拟主机 双线主机 主机 域名注册 cn域名 域名 服务器租用 酷睿服务器 vps vps主机

  • (阅读次数:232)
  • 上一篇: 第七章 Unix进程的环境    下一篇: 第六章 系统数据文件和信息
  • [收藏] [推荐] [评论] [打印本页] [返回上一页][关闭窗口]
  • 昵称: (为空则显示guest)
  • 评论分数: ★ ★ ★★★ ★★★★ ★★★★★
  • 评论内容:(不能超过250字,需审核后才会公布,请自觉遵守互联网相关政策法规。