发信人:scircle(yuanyuan),信区:Security标题:unix环境高级编程--第8章进程控制(上)发信站:BBS水木清华站(MonMar2715:58:552000)第八章〓进程控制〓引言本章介绍Unix的进程控制,包括创建新进程、执行程序和进程终止。我们也说明进程的各种ID〖CD2〗实际、有效和保存的用户和组ID,以及它们如何受到进程控制原语的影响。本章也包括了解释器文件和system函数。本章以大多数Unix系统所提供的进程会计机制结束。这使我们从一个不同角度了解进程控制功能。〓进程标识每个进程都有一个非负整型的唯一进程ID。因为进程ID标识符总是唯一的,常将其用作为其它标识符的一部分以保证其唯一性。在节中的tmpnam函数将进程ID作为名字的一部分创建一个唯一的路径名。有某些专用的进程:进程ID0是调度进程,常常被称为交换进程(swapper)。该进程并不执行任何磁盘上的程序。〖CD2〗它是系统核的一部分,因此也被称为系统进程。进程ID1通常是init进程,在自举过程结束时由系统核调用。该进程的程序文件在Unix的较早版本中是/etc/init,在版新版本中是/sbin/init。此进程负责在系统核自举后起动一个Unix系统。init通常读与系统有关的初始化文件(/etc/rc*文件),并将系统引导到一个状态(例如多用户)。init进程决不会终止。它是一个普通的用户进程(与交换进程不同,它不是一个在系统核内的系统进程),但是它以超级用户特权运行。在本章稍后部分会说明init如何成为所有孤儿进程的父进程。在某些Unix的虚存实现中,进程ID2是页精灵进程(pagedaemon)。此进程负责支持虚存系统的请页操作。除了进程ID,每个进程还有一些其它标识符。下列函数返回这些标识符。#include#includepid迹茫模*常病絫getpid(voide;〓〓〖CD2〗调用进程的进程pid迹茫模*常病絫getppid(void);〓〓〖CD2〗调用进程的父进程uid迹茫模*常病絫getuid(void);〓〓〖CD2〗调用进程的实际用户uid迹茫模*常病絫geteuid(void);〓返回:〓〓〖CD2〗调用进程的有效用户Igid迹茫模*常病絫getgid(void);〓〓〖CD2〗调用进程的实际组gid迹茫模*常病絫getegid(void);〓〓〖CD2〗调用进程的有效组注意,这些函数都没有出错返回,在下一章中讨论fork函数时,将进一步讨论父进程ID。在节中已讨论了实际和有效用户及组ID。〓fork函数一个现存进程调用fork函数是Unix核创建一个新进程的唯一方法。(这并不适用于前节提及的交换进程、init进程和页精灵进程。这些进程是由系统核作为自举过程的一部分以特殊方式创建的。#include#includepid迹茫模*常病絫Returns:0inchild,processIDofchildinparent,-1onerror〓返回:子进程中为0,父进程中为子进程ID,出错为由fork创建的新进程被称为子进程。该函数被调用一次,但返回二次。两次返回的区别是子进程的返回值是0,而父进程的返回值则是新子进程的进程ID。将子进程ID返回给父进程的理由是:因为一个进程的子进程可以多于一个,所以没有一个函数使一个进程可以获得其所存子进程的进程ID。fork使子进程得到返回值0的理由是:一个进程只会有一个父进程,所以子进程总是可以调用getppid以获得其父进程的进程ID。(进程ID0总是由交换进程使用,所以一个子进程的进程ID不可能为0。子进程和父进程继续执行fork之后的指令。子进程是父进程的复制器。例如,子进程就得父进程数据空间、堆和栈的复制器。注意,这是子进程所拥用的拷贝。父、子进程并不共享这些存储空间部分。如果正文段是只读的,则父、子进程共享正文段节)。很多现在的实现并不做一个父进程数据的栈和堆的完全拷贝,因为在fork之后经常跟随着exec。作为替代,使用了在写时复制(COW)的技术。这些区域由父、子进程共享,而且系统核将它们的存取权改变为只读的。如果有进程试图修改这些区域,则系统核为有关部分,典型的是虚存系统中的页,作一个拷贝。Bach[1986]的节和Lefflen等[1989]的节对这种特征作了更详细的说明。实例程序例示了Fork函数。如果执行此程序则得到:$awritetobeforepid=430,glob=7,var=89〓〓子进程的变量值改变了pid=429,glob=6,var=88〓〓父进程的变量值没有改变$$catawritetobeforebefore一般,在fork之后里先进程先执行,还是子进程先执行是不确定的。这取决于系统核所使用的调度算法。如果要求文、字进程之间相互同步,则要求某种形式的进程间通信。在程序8中,父进程体自己腔眠2秒钟,以此该子进程先执行。但并不保证2秒钟已经足够,在8市说明竟学条件时,我们还够深及这一问题及其它类型的同步方法。在节口,在fork之后我们将用信号体、父、子进程同步。注意,程序中fork与I/O函数之间的关系。回忆第三章中所述,Wrik函数是不带缓存的。国灰在fork之间调用Wrir后,所以具数据写到标准输出上一次。但是,标准I/O库是带缓存的。回忆一下第节,如果标准输出连到终设备,则它是行缓冲的,否则它是冷缓冲的。当以交互方式运行该程序时,我们只课到printf输出的行一次,具原因是标准输出缓存收新行符刷新。但是当收标准输出重新定向到一个文件时,我们却得到printf输出行两次时,该行数据仍在缓冲中,然后在父进程数据空间复制到子进程中时该缓存数据也被复制到子进程中。于是那时父、子进程各自有了带该行内容的缓冲。在exit之前的第二个printf将其数据添加到现存的缓存中。当每个进程终止时,其缓存中的内容被写到相应文件中。程序〓fork函数的实例文件共享对程序需注意的另一点是:在重新定向父进程的标准输出时,子进程的标准输出也被重新定向。确定,fork的一个特性是所有由父进程打开的描述符都复制到子进程中。父、子进程每个相同的打开描述符共享一个文件表项。(回忆图。考虑下述情况,一个进程打开了三个不同文件,它们是:标准输入、标准输出和标准出错。在从fork返回时,我们有了如图中所示的安排。图〓fork之后父子、进程之间对打开文件的共享这种共享文件的方式使父、子进程对同一文件使用了一个文件位移量。考虑下述情况:一个进程fork了一个子进程,然后等待子进程终止。假定,作为普通处理的一部分,父、子进程都向标准输出执行写操作。如果父进程使其标准输出重新定向(很可能是由shell实现的),那么子进程写到该标准输出时,它将更新与父进程共享的该文件的位移量。在我们所考虑的例子中,当父进程等待子进程时,子进程写到标准输出;而在子进程终止后,父进程也写到标准输出上,并且知道其输出会添加在子进程所写数据之后。如果父、子进程不共享同一文件位移量,这种形式的交互作用就很难实现。如果父、子进程写到同一描述符文件,但又没有任何形式的同步(例如使父进程等待子进程),那么它们的输出就会相互混合(假定所用的描述符是在fork之前打开的)。虽然这种情况是可能发生的(见程序,但这并不是常用的操作方式。有两种常见的在fork之后处理文件描述符的情况:父进程等待子进程完成。在这种情况下,父进程无需对其描述符作任何处理。当子进程终止后,它曾进行过读、写操作的任一共享描述符的文件位移量也作了相应更新。父、子进程各自执行不同的程序段。在这种情况下,在fork之后,父、子进程各自关闭它们不需使用的文件描述符,并且不干扰对方使用的文件描述符。这种方法是网络服务进程常常使用的。除了打开文件之外,很多父进程的其它性质也由子进程继承:·实际用户ID、实际组ID、有效用户ID、有效组·添加组·进程组·对话期·控制终端·设置-用户-ID标志和设置-组-ID标志·当前工作目录·根目录·文件方式创建屏蔽字·信号屏蔽和排列·对任一打开文件描述符的在执行时关闭标志·环境·连接的共享存储段·资源限制父、子进程之间的区别是:·fork的返回值·进程·不同的父进程·子进程的tms迹茫模*常病絬time,tms迹茫模*常病絪time,tms迹茫模*常?〗cutime以及tms迹茫模*常病絬stime设置为0。·父进程设置的锁,子进程不继承·子进程的末决告警被清除·子进程的末决信号集设置为空集其中很多特性至今尚末讨论过,我们将在以后几章中对它们进行说明。使fork失败的两个主要原因是:(a)系统中已经有了太多的进程(通常意味着某个方面出了问题),或者(b)该实际用户ID的进程总数超过了系统限制。回忆图,其中CHILDCD*常病組AX规定了每个实际用户ID在任一时刻可具有的最大进程数。fork有两种用法:一个父进程希望复制自己,使父、子进程同时执行不同的代码段。这对网络服务进程是常见的〖CD2〗父进程等待委托者的服务请求。当这种请求到达时,父进程调用fork,使子进程处理此请求。父进程则继续等待下一个服务请求。一个进程要执行一道不同的程序。这对shell是常见的情况。在这种情况下,子进程在从fork返回后立即调用exec(我们将在节说明exec)。某些操作系统将2中的两个操作(fork之后执行exec)组合成一个,并称其为spawn。Unix将这两个操作分开,因为在很多场合需要单独使用fork,它后面并不跟随exec。另外,将这两个操作分开,使得子进程在fork和exec之间可以更改自己的属性。例如I/O重新定向、用户ID、信号排列等。在第十四章中有很多这方面的例子。〓vfork函数vfork函数的调用序列和返回值与fork相同,但两者的语义不同。vfork起源于较早的4BSD虚存版本。在Leffleretal[1989]的节中指出虽然它是特别有效率的,但是vfork的语义很奇特,通常认为它具有结构上的缺陷。尽管如此SVR4和4仍支持vfork。某些系统具有头文件,当调用vfork时,应当包括该头文件。vfork用于创建一个新进程,而该新进程的目的是exec一道新程序(为上节2中一样)。程序1中的shell基本部分就是这种类型程序的一个例子。vfork与fork一样都创建一个子进程,但是它并不将父进程的地址空间完全复制到子进程中,其设想是子进程会立即调用exec(或exit),于是也就不会存访该地址空间。不过在子进程调用exec或exit之前,它在父进程的空间中运行。这种工作方式在某些Unix的页式虚存实现中提高了效率(与我们上节中提及的,在fork之后跟随exec,并采用在写时复制技术相类似)。vfork和fork之间的另一个区别是:vfork保证子进程先运行,在它调用exec或exit之后父进程再可能被调度运行。(如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。实际在程序中使用vfork代替fork,并作其它相应修改得到程序。程序〓vfork函数的实例运行该程序得到:$befork子进程对变量glob和var作增1操作,结果改变了父进程中的变量值。因为子进程在父进程的地址空间中运行,所以这并不令人鹜讶。但是其作用的确与fork不同。注意,在程序中,调用了迹茫模*常病絜xit而不是exit。正如节所述,迹茫模?2〗exit并不执行标准I/O缓存的刷新操作。如果用exit而不是迹茫模*常病?exit,则该程序的输出是:$before从中可见,父进程printf的输出消失了。其原因是子进程调用了exit,它刷新开关闭了所有标准I/O流。这包括标准输出。虽然这是由子进程执行的,但却是在父进程的地址空间中进行的,所以所有受到影响的标准I/OFILE对象都是在父进程中。当父进程调用printf时,标准输出已被关闭了,于是printf返回-1。Leffleret[1989]的节中包含了fork和vfork实现方面的更多信息。练习8和则继续了对vfork的讨论。〓exit函数如同在节中所述,进程有三种