第八章〓进程控制? 8?1〓引言? 本章介绍Unix的进程控制,包括创建新进程、执行程序和进程终止。我们也说明进 程的各种 ID〖CD2〗实际、有效和保存的用户和组ID,以及它们如何受到进程控制原语的影 响。本章 也包括了解释器文件和system函数。本章以大多数Unix系统所提供的进程会计机制 结束。这 使我们从一个不同角度了解进程控制功能。? 8?2〓进程标识? 每个进程都有一个非负整型的唯一进程ID。因为进程ID标识符总是唯一的,常将其 用作为其 它标识符的一部分以保证其唯一性。在5?13节中的tmpnam函数将进程ID作为名字 的一部分 创建一个唯一的路径名。? 有某些专用的进程:进程ID0是调度进程,常常被称为交换进程(swapper)。该进程 并不执行 任何磁盘上的程序。〖CD2〗它是系统核的一部分,因此也被称为系统进程。进程 ID1通常是 init进程,在自举过程结束时由系统核调用。该进程的程序文件在Unix的较早版本 中是/etc /init,在版新版本中是/sbin/init。此进程负责在系统核自举后起动一个Unix系 统。init 通常读与系统有关的初始化文件(/etc/rc*文件),并将系统引导到一个状态(例如 多用户)。 init进程决不会终止。它是一个普通的用户进程(与交换进程不同,它不是一个在 系统核内 的系统进程),但是它以超级用户特权运行。在本章稍后部分会说明init如何成为 所有孤儿 进程的父进程。? 在某些Unix的虚存实现中,进程ID2是页精灵进程(pagedaemon)。此进程负责支持 虚存系统 的请页操作。? 除了进程ID,每个进程还有一些其它标识符。下列函数返回这些标识符。? #include ? #include ? pid 迹茫模*常病絫 getpid(voide;〓〓〖CD2〗调用进程的进程ID? pid 迹茫模*常病絫 getppid(void);〓〓〖CD2〗调用进程的父进程ID? uid 迹茫模*常病絫 getuid(void);〓〓〖CD2〗调用进程的实际用户ID? uid 迹茫模*常病絫 geteuid(void);〓返回:〓〓〖CD2〗调用进程的有效用户I D? gid 迹茫模*常病絫 getgid(void);〓〓〖CD2〗调用进程的实际组ID? gid 迹茫模*常病絫 getegid(void);〓〓〖CD2〗调用进程的有效组ID? 注意,这些函数都没有出错返回,在下一章中讨论fork函数时,将进一步讨论父进 程ID。在 4?4节中已讨论了实际和有效用户及组ID。? 8?3〓fork函数? 一个现存进程调用fork函数是Unix核创建一个新进程的唯一方法。(这并不适用于 前节提及 的交换进程、init进程和页精灵进程。这些进程是由系统核作为自举过程的一部分 以特殊方 式创建的。)? #include ? #include ? pid 迹茫模*常病絫 fork(void);? Returns:0 in child,process ID of child in parent,-1 on error〓返回:子进 程中为0 ,父进程中为子进程ID,出错为0? 由fork创建的新进程被称为子进程。该函数被调用一次,但返回二次。两次返回的 区别是子 进程的返回值是0,而父进程的返回值则是新子进程的进程ID。将子进程ID返回给 父进程的 理由是:因为一个进程的子进程可以多于一个,所以没有一个函数使一个进程可以 获得其所 存子进程的进程ID。fork使子进程得到返回值0的理由是:一个进程只会有一个父 进程,所 以子进程总是可以调用getppid以获得其父进程的进程ID。(进程ID0总是由交换进 程使用, 所以一个子进程的进程ID不可能为0。)? 子进程和父进程继续执行fork之后的指令。子进程是父进程的复制器。例如,子进 程就得父 进程数据空间、堆和栈的复制器。注意,这是子进程所拥用的拷贝。父、子进程并 不共享这 些存储空间部分。如果正文段是只读的,则父、子进程共享正文段(7?6节)。? 很多现在的实现并不做一个父进程数据的栈和堆的完全拷贝,因为在fork之后经常 跟随着ex ec。作为替代,使用了在写时复制(COW)的技术。这些区域由父、子进程共享,而 且系统核 将它们的存取权改变为只读的。如果有进程试图修改这些区域,则系统核为有关部 分,典型 的是虚存系统中的"页",作一个拷贝。Bach[1986]的9?2节和Lefflen等[ 198 9]的5?7节对这种特征作了更详细的说明。? 实例? 程序8?1例示了Fork函数。如果执行此程序则得到:? $ a?out? a write to stdout? before fork? pid=430,glob=7,var=89〓〓子进程的变量值改变了? pid=429,glob=6,var=88〓〓父进程的变量值没有改变? $ a?out>temp?out? $ cat temp?out? a write to stdout? before fork? pid=432,glob=7,var=89? before fork? pid=431,glob=6,var=88? 一般,在fork之后里先进程先执行,还是子进程先执行是不确定的。这取决于系统 核所使用 的调度算法。如果要求文、字进程之间相互同步,则要求某种形式的进程间通信。 在程序8 ?1中,父进程体自己腔眠2秒钟,以此该子进程先执行。但并不保证2秒钟已经足 够,在8? 8市说明竟学条件时,我们还够深及这一问题及其它类型的同步方法。在10?6节口 ,在fork之后我们将用信号体、父、子进程同步。? 注意,程序8?1中fork与I/O函数之间的关系。回忆第三章中所述,Wrik函数是不 带缓存的。国灰在fork之间调用Wrir后,所以具数据写到标准输出上一次。但是, 标准I/O库是带缓存的。回忆一下第5?12节,如果标准输出连到终设备,则它是 行缓冲的,否则它是冷缓冲的。当以交互方式运行该程序时,我们只课到printf 输出的行一次,具原因是标准输出缓存收新行符刷新。但是当收标准输出重新定 向到一个文件时,我们却得到printf输出行两次时,该行数据仍在缓冲中,然后 在父进程数据空间复制到子进程中时该缓存数据也被复制到子进程中。 于是那时父、子进程各自有了带该行内容的缓冲。在exit之前的第二个printf将
其数据添加到现存的缓存中。当每个进程终止时,其缓存中的内容被写到相应文件 中。?? P190?? 程序8?1〓fork函数的实例? 文件共享? 对程序8?1需注意的另一点是:在重新定向父进程的标准输出时,子进程的标准输 出也被重 新定向。确定,fork的一个特性是所有由父进程打开的描述符都复制到子进程中。 父、子进 程每个相同的打开描述符共享一个文件表项。(回忆图3?4)。? 考虑下述情况,一个进程打开了三个不同文件,它们是:标准输入、标准输出和标 准出错。 在从fork返回时,我们有了如图8?1中所示的安排。?? P191?? 图8?1〓fork之后父子、进程之间对打开文件的共享? 这种共享文件的方式使父、子进程对同一文件使用了一个文件位移量。考虑下述情 况:一个 进程fork了一个子进程,然后等待子进程终止。假定,作为普通处理的一部分,父 、子进程 都向标准输出执行写操作。如果父进程使其标准输出重新定向(很可能是由shell实 现的), 那么子进程写到该标准输出时,它将更新与父进程共享的该文件的位移量。在我们 所考虑的 例子中,当父进程等待子进程时,子进程写到标准输出;而在子进程终止后,父进 程也写到 标准输出上,并且知道其输出会添加在子进程所写数据之后。如果父、子进程不共 享同一文 件位移量,这种形式的交互作用就很难实现。? 如果父、子进程写到同一描述符文件,但又没有任何形式的同步(例如使父进程等 待子进程) ,那么它们的输出就会相互混合(假定所用的描述符是在fork之前打开的)。虽然这 种情况是 可能发生的(见程序8?1),但这并不是常用的操作方式。? 有两种常见的在fork之后处理文件描述符的情况:? 1?父进程等待子进程完成。在这种情况下,父进程无需对其描述符作任何处理。 当子进程终止后,它曾进行过读、写操作的任一共享描述符的文件位移量也作了 相应更新。? 2?父、子进程各自执行不同的程序段。在这种情况下,在fork之后,父、子进程 各自关闭 它们不需使用的文件描述符,并且不干扰对方使用的文件描述符。这种方法是网络 服务进程 常常使用的。? 除了打开文件之外,很多父进程的其它性质也由子进程继承:? ·实际用户ID、实际组ID、有效用户ID、有效组ID? ·添加组ID? ·进程组ID? ·对话期ID? ·控制终端? ·设置-用户-ID标志和设置-组-ID标志? ·当前工作目录? ·根目录? ·文件方式创建屏蔽字? ·信号屏蔽和排列? ·对任一打开文件描述符的在执行时关闭标志? ·环境? ·连接的共享存储段? ·资源限制? 父、子进程之间的区别是:? · fork的返回值? ·进程ID? ·不同的父进程ID? ·子进程的tms 迹茫模*常病絬time,tms 迹茫模*常病絪time,tms 迹茫模*常?nbsp; 〗cutime 以及tms 迹茫模*常病絬stime设置为0。? ·父进程设置的锁,子进程不继承? ·子进程的末决告警被清除? ·子进程的末决信号集设置为空集? 其中很多特性至今尚末讨论过,我们将在以后几章中对它们进行说明。? 使fork失败的两个主要原因是:(a)系统中已经有了太多的进程(通常意味着某个方 面出了 问题),或者(b)该实际用户ID的进程总数超过了系统限制。回忆图2?7,其中CHI LD CD *常病組AX规定了每个实际用户ID在任一时刻可具有的最大进程数。? fork有两种用法:? 1?一个父进程希望复制自己,使父、子进程同时执行不同的代码段。这对网络服 务进程是 常见的〖CD2〗父进程等待委托者的服务请求。当这种请求到达时,父进程调用fo rk,使子 进程处理此请求。父进程则继续等待下一个服务请求。? 2?一个进程要执行一道不同的程序。这对shell是常见的情况。在这种情况下,子 进程在从 fork返回后立即调用exec(我们将在8?9节说明exec)。? 某些操作系统将2中的两个操作(fork之后执行exec)组合成一个,并称其为spawn。 Unix将这 两个操作分开,因为在很多场合需要单独使用fork,它后面并不跟随exec。另外, 将这两个 操作分开,使得子进程在fork和exec之间可以更改自己的属性。例如I/O重新定向 、用户ID 、信号排列等。在第十四章中有很多这方面的例子。? 8?4〓vfork函数? vfork函数的调用序列和返回值与fork相同,但两者的语义不同。? vfork起源于较早的4BSD虚存版本。在Leffler et al [1989]的5?7节中指出"虽 然它是 特 别有效率的,但是vfork的语义很奇特,通常认为它具有结构上的缺陷"。尽管如此 SVR4和4 ?3+BSD仍支持vfork。? 某些系统具有头文件,当调用vfork时,应当包括该头文件。? vfork用于创建一个新进程,而该新进程的目的是exec一道新程序(为上节2中一样 )。程序1 ?5中的shell基本部分就是这种类型程序的一个例子。vfork与fork一样都创建一 个子进程 ,但是它并不将父进程的地址空间完全复制到子进程中,其设想是子进程会立即调 用exec( 或exit),于是也就不会存访该地址空间。不过在子进程调用exec或exit之前,它 在父进程 的空间中运行。这种工作方式在某些Unix的页式虚存实现中提高了效率(与我们上 节中提及 的,在fork之后跟随exec,并采用在写时复制技术相类似)。? vfork和fork之间的另一个区别是:vfork保证子进程先运行,在它调用exec或exi t之后父进 程再可能被调度运行。(如果在调用这两个函数之前子进程依赖于父进程的进一步 动作,则 会导致死锁。)? 实际? 在程序8?1中使用vfork代替fork,并作其它相应修改得到程序8?2。? ? P194?? 程序8?2〓vfork函数的实例? 运行该程序得到:? $ a?out? befork vfork? pid=607,glob=7,var=89? 子进程对变量glob和var作增1操作,结果改变了父进程中的变量值。因为子进程在 父进程的 地址空间中运行,所以这并不令人鹜讶。但是其作用的确与fork不同。? 注意,在程序8?2中,调用了 迹茫模*常病絜xit而不是exit。正如8?5节所述, 迹茫模?nbsp; ?2〗exit并不执行标准I/O缓存的刷新操作。如果用exit而不是 迹茫模*常病?nbsp; exit,则 该程序的输出是:? $ a?out? before vfork? 从中可见,父进程printf的输出消失了。其原因是子进程调用了exit,它刷新开关 闭了所有 标准I/O流。这包括标准输出。虽然这是由子进程执行的,但却是在父进程的地址 空间中进 行的,所以所有受到影响的标准I/O FILE对象都是在父进程中。当父进程调用pri ntf时,标 准输出已被关闭了,于是printf返回-1。? Leffler et al?[1989]的5?7节中包含了fork和vfork实现方面的更多信息 。练习8 ?1和8?2则继续了对vfork的讨论。? 8?5〓exit函数? 如同在7?3节中所述,进程有三种正常终止法,有两种异常终止法。? 1?正常终止? (a)在main函数内执行return语句。如在73节中所述,这等效于调用exit。? (b)调用exit函数。此函数由ANSIC定义,其操作包括了调用各终止处理程序(终止 处理程序 是在调用atexit函数时登录的),然后关闭所有标准I/O流等。因为ANSIC并不处理 文件描述 符、多进程(父、子进程)以及作业控制,所以这一定义对Unix系统而言是不完整的 。? (c)调用-exit系统调用函数。此函数由exit调用,它处理Unix特定的细节。-exit 是由POSIX ?1说明的。? 2?异常终止:? (a)调用abort。它产业SIGABRT信号,所以是下一种异常终止的一种特例。? (b)当进程接收到某个信号时。(第十章将较详细地说明信号)。进程本身(例如调用 abort函 数)、其它进程和系统核都能产生传送到某一进程的信号。例如,进程越出其地址 空间访问 存储单元,或者除以0,系统核就会为该进程产生相应的信号。? 不管进程是如何终止的,最后都会执行系统核中的同一段代码。这段代码为相应进 程关闭所 有打开描述符,释放它所使用的存储器等等。? 对上述任何一种终止情形,我们都希望终止进程能够通知其父?趟?侨绾沃罩沟?nbsp; 。对于ex it和-exit,这是依靠传递给空们的退出状态参数来实现的。在异常终止情况,系 统核(不是 进程本身)产生一个指示其异常终止原因的终止状态。在任一种情况下,该终止进 程的父进 程都能用wait或waitpid函数(在下一节说明)取得其终止状态。? 注意,我们在这里使用了"退出状态"(它是传向exit或-exit的参数,或main的返回 值)和 "终止状态"两个术语,以表示有所区别。在最后调用-exit时,系统核将其退出状 态转换 成终止状态(请回忆图7?1)。图8?2说明了父进程检查子进程的终止状态的不同方 法。如果 子进程正常终止,则父进程可以获得子进程的退出状态。? 在说明fcrk函数时,一定是一个父进程生成一个子进程。上面又说明了子进程将其 终止状态 返回给父进程。但是如何父进程在子进程之前终止,则将如何呢?其回答是对于其 父进程已 经终止的所有进程,它们的父进程都改变为init进程。我们称这些进程由init进程 领养。其 操作过程大致是:在一个进程终止时,系统核逐个检查所有活动进程,以判断它是 否是正要 终止的进程的子进程,如果是,则该进程的父进程ID就更改为1(init进程的ID)。 这种处理 方法保证了每个进程有一个父进程。? 另一个我们关心的情况是如果子进程在父进程之前终止,那么父进程又如何能在作 相应检查 时得到子进程的终止状态呢?对此问题的回答是系统核为每个终止子进程保存了一 定量的信 息,所以当终止进程的父进程调用wait或waitpid时,可以得到有关信息。这种信 息至少包 括进程ID、该进程的终止状态、以反该进程使用的CPU时间总量。系统核可以释放 终止进程 所使用的所有存储器,关闭其所有打开文件。在Unix术语中,一个已经终止,但是 其父进程 尚末对其进行善后处理(获取终止子进程的有关信息、释放它仍占用的资源)的进程 被称为僵 死进程。PS(1)命令将僵死进程的状态打印为Z。如果编写一个长期运行的程序,它 fork了很 多子进程,那么除非父进程等待取得子进程的终止状态,否则这些子进程就会变成 僵死进程 。? 系统V提供了一种避免僵死进程的非标准化方法,这将在10?7中介绍。? 最后一个要考虑的问题是:一个由init进程领养的进程终止时会发生什么?它会不 会变成一 个僵死进程?对此问题的回答是"否",因为init被编写成只要有一个子进程终止, init就 会调用一个wait函数取得其终止状态。这样也就防止了在系统中有很多僵死进程。 当提及" 一个init的子进程"时,这指的是init直接产生的进程(例如,将在9?2节说明的g etty进程 ),或者是其父进程已终止,由init收养的进程。? 8?6〓wait和waitpid函数? 当一个进程正常或异常终止时,系统核就向其父进程发送SIGCHLD信号。因为子进 程终止是 个异步文件(这可以在父进程运行的任何时候发生),所以这种信号也是系统核向父 进程发的 异步通知。父进程可以忽略该信号,或者提供一个该信号发生时即被调用执行的函 数(信号 处理程序)。对于这种信号的系统默认动作是忽略它。在第十章将说明这些选择项 。现在需 要知道的是调用wait或waitpid的进程可能会:? ·阻塞(如果其所有子进程都还在运行),或者? ·带子进程的终止状态立即返回(如果一个子进程已终止,正等待父进程存取其终 止状态), 或者? ·出错立即返回(如果它没有任何子进程)。? 如果进程由于接收到SIGCHLD信号而调用wait,则可期望wait会立即返回。但是如 果在一个 任意时刻调用wait,则进程可能会阻塞。? #include ? #include ? pid 迹茫模*常病絫 wait(int *?statloc);?? pid 迹茫模*常病絫 waitpid(pid 迹茫模*常病絫 ?pid,?int ??statloc,? int ?op tions);?? Both return:process ID if OK,0(see later),or -1 on error〓两个函数返回: 若成功为 进程ID,出错为-1? 这两个函数的区别是:? ·在一个子进程终止前,wait使其调用者阻塞,而waitpid有一选择项,它可使用 调用者不 阻塞。? ·waitpid并不等待第一个终止的子进程〖CD2〗它有若干个选择项,可以控制它所 等待的进 程。? 如果一个子进程已经终止,是一个僵死进程,则wait立即返回并取得该子进程的状 态,否则 wait使其调用者阻塞直到一个子进程终止。如调用者阻塞而且它有多个子进程,则 在其一个 子进程终止时,wait就立即返回。因为wait返回终止子进程的进程ID,所以它总能 了解是那 一个子进程终止了。? 这两个函数的参数staloc是一个整型指针。如果staloc子是一个空指针,则终止进 程的终止 状态就存放在它所指向的单元内。如果不关心终止状态,则可将该参数指定为空指 针。? 依据传统,这两个函数返回的整型状态字是由实现定义的。其中某些位表示退出状 态(正常 返回),其它位则指示信号编号(异常返回),有一位指示是否产生了一个core文件 等等。POS IX?1规定终止状态用定义在中的各个宏来查看。有三个互斥的宏可 用来取得 进程终止的原因,它们的名字都以WIF开始。基于这三个宏中哪一个值是真,就可 选用其它 宏来取得终止状态,信号编号等。这些都示于图8?2。在8?9节中讨论作业控制时 ,将说明 如何停止一个进程。?? P198?? 图8?2〓检查wait和waitpid所返回的终止状态的宏? 实例? 程序8?3中的函数pr-exit使用图8?2中的宏以打印进程的终止状态。在本章的很 多程序中 都将调用此函数。注意,如果定义了WCOREDUMP,则此函数也处理该宏。? 程序8?4调用pr-exit函数,例示终止状态的不同值。运行程序8?4可得:? $ a?out? normal termination,exit status=7? abnormal termination,signal number=6(core file generated)? abnormal termination,signal number=8(core file generated)?? P199? 程序8?3〓打印exit状态的说明?? P200? 程序8?4〓例示不同的exit值? 不幸的是,没有一种可移植的方法将WTERMSIG得到的信号编号映照为说明性的名字 。(10?2 1节中说明了一种方法。)我们必须查看头文件才能知道SIGABRT的值是 6,SIGFP E的值是8。? 正如前面已提到的,如果一个进程有几个子进程,那么只要有一个子进程终止,w ait就返回 。如果要等待一个指定的进程终止(如果知道要等待进程的ID),那么该如何做呢? 在较早的U nix版本中,必须调用wait,然后将其返回的进程ID和所期望的进程ID相比较。如 果终止进 程不是所期望的,则将该进程ID和终止状态保存起来,然后再次调用wait。反复这 样做直到 所期望的进程终止。下一次又想等待一个特定进程时,先查看已终止的进程表,若 其中已有 要等待的进程,则取有关信息,否则调用wait。其实,我们需要的是等待一个特定 进程的函 数。POSIX?1定义了waitpid函数以提供这种功能(以及其它一些功能)。? waitpid函数是新由POSIX?1定义的。SVR4和4?3+BSD都提供此函数,但早期的系 统V和4?3 BSD并不提供此函数。?? P200?? 图8?3〓waitpid的选择项常数? 对于waitpid的pid参数的解释与其值有关:? pid==-1〓等待任一子进程。于是在这一功能方面waitpid与wait等效。? pid>0〓等待其进程ID与pid相等的子进程。? pid==0〓等待其组ID等于调用进程的组ID的任一子进程。? pid<-1〓等待其组ID等于pid的绝对值的任一子进程。? (在9?4节说明进程组)。waitpid返回该终止子进程的进程ID,而该子进程的终止 状态则通 过statloc返回。对于wait,其唯一的出错是调用进程没有子进程。(在此函数调用 由一个信 号中断时,也可能返回另一种出错。第十章将对此进程讨论。)但是对于waitpid, 如果指定 的进程或进程组不存在,或者调用进程没有子进程都能出错。? options参数使我们能进一步控制waitpid的操作。此参数或者是0,或者是图8?3 中常数的 按位或。? SVR4支持两个附加的非标准的options常数。WNOWAIT使系统将其终止状态已由wai tpid返回 的进程保持在等待状态,于是该进程就可被再次等待。对于WCONTINUED,返回由p id指定的 某一子进程的状态,该子进程已被继续,其状态尚末报告过。? waitpid函数提供了wait函数没有提供的三个功能:? 1? waitpid等待一个特定的进程(而wait则返回任一终止子进程的状态)。在讨论 popen函数 时会再说明这一功能。? 2? waitpid提供了一个wait的非阻塞版本。有时希望取得一个子进程的状态,但 不想阻塞 。? 3? waitpiol支持作业控制(以WUNTRACED选择项)。? 实例? 回忆一下8?5节中有关僵死进程的讨论。如果一个进程要fork一个子进程,但不要 求它等待 子进程终止,也不希望子进程处于僵死状态直到父进程终止,实现这一要求的决巧 是调用fo rk两次。程序8?5实现了这一点。? 在第二个子进程中调用sleep以保证在打印父进程ID时第一个子进程已终止。在fo rk之后, 父、子进程都可继续执行〖CD2〗我们无法予知那一个会先执行。如果不使第二个 子进程睡 眠,则在fork之后,它可能比其父进程先执行,于是它打印的父进程ID会是它的父 进程,而 不是init进程(进程ID1)。? ? P202?? 程序8?5〓fork两次以避免僵死进程? 执行程序8?5得到:? $ a?out? $ second child,parent pid=1? 注意,当原先的进程(也就是exec本程序的进程)终止时,shell打印其指示符,这 在第二个 子进程打印其父进程ID之前。? 8?7〓wait3和wait4函数? 4?3+BSD提供了两个附加函数wait3和wait4。这两个函数提供的功能比POSIX?1函 数wait和 waitpid所提供的分别要多一个,它与附加参数rusage有关。该参数要求系统核返 回由终止 进程及其所有子进程使用的资源摘要。? #include ? #include ? #include ? #include ? pid 迹茫模*常病絫 wait3(int ?statloc?,int ?options?,struct rusage *?rusae) ;?? pid 迹茫模*常病絫 wait4(pid 迹茫模*常病絫 ?pid,?int *?statloc,?in t ?optio ns,?struct rusage *?rusage)?;? Both return:process ID if OK,0,or -1 on error〓两个函数返回:若成功为进 程ID,出 错为-1? SVR4在其BSD兼容库中也提供了wait3函数。? 资源信息包括用户CPU时间总量,系统CPU时间总量,缺页次数,接收到信号的次数 等。有关 细节请参阅getrusage(2)手册页。这些资源信息只包括终止子进程,并不包括处于 停止状态 的子进程。(这种资源信息与7?11节中所述的资源限制不同。)图8?4中列出了各 个wait函 数所支持的不同的参数。?? P203?? 图8?4〓在不同系统上各个wait函数所支持的参数? 8?8〓竟态条件? 从本书的目的出发,当多个进程都企图对共享数据进行某种处理,而最后的结果又 取决于进 程运行的顺序时,则我们认为这发生了竟态条件。如果在fork之后的某种逻辑显式 或隐式地 依赖于在fork之后是父进程先运行还是子进程先运行,那么fork函数就会是竟态条 件活跃的 孳生地。通常,我们不能予料哪一个进程先运行。即使知道那一个进程先运行,那 么在该进 程开始运行后,所发生的事情也依赖于系统负载以及系统核的调度算法。? 在程序8?5中,当第二个子进程打印其父进程ID时,我们看到了一个潜在的竟态条 件。如果 第二个子进程在第一个子进程之前运行,则其父进程将会是第一个子进程。但是, 如果第一 个子进程先运行,并有足够的时间到达并执行exit,则第二个子进程的父进程就是 init。即 使在程序中调用sleep,这也不保证什么。如果系统负担很重,那么在第二个子进 程从sleep 返回时,可能第一个子进程还没有得到机会运行。这种形式的问题很难排除,因为 在大部分 时间,这种问题并不出现。? 如果一个进程希望等待一个子进程终止,则它必须调用wait函数。如果一个进程要 等待其父 进程终止(如程序8?5中一样),则可使用下列形式的循环:? while(getppid() !=1)? sleep(1);? 这种形式循环(称为定期询问)的问题是它浪费了CPU时间,因为调用者每隔1秒都被 唤醒,然 后进行条件测试。? 为了避免竟态条件和定期询问,在多个进程之间需要有某种形式的信号机制。在U nix中可以 使用信号机制,在10?16节将说明它的一种用法。各种形式的进程间通信(IPC)也 可使用, 在第十四、十五章将对此进行讨论。? 在父、子进程关系中,常常有下述景况。在fork之后,父、子进程都有一些子情要 做。例如 ,父进程可能以子进程ID更新日志文件中的一个记录,而子进程则可能要为父进程 创建一个 文件。在这一例子中,我们要求每个进程在执行完它的一套初始化操作后要通知对 方,并且 在继续运行之前,要等待另一方完成其初始化操作。这种景况可以描述如下:? #include "ourhdr?h"? TELL 迹茫模*常病絎AIT(); /* set things up for TELL 迹茫模*常病絰xx & WAIT〖 茫模*常病絰xx */? if((pid=fork())<0)? err 迹茫模*常病絪ys("fork error");? else if (pid==0){〓〓/* 子进程? /? 子进程执行所需的各操作 */? TELL 迹茫模*常病絇ARENT(getppid());〓/? tell parent we're done */通知 父进程已 执行完毕? WAIT 迹茫模*常病絇ARENT();〓/* and wait for parent */等待父进程? /* and the child continues on its way ··· */ 〓子进程继续运行? }? /* 〓父进程执行所需的各操作 */? TELL 迹茫模*常病紺HILD(pid);〓/* tell child we're done */ 通知子进程已 执行完毕 ? WAIT 迹茫模*常病紺HILD();〓/* and wait for child */ 等待子进程? /* and the parent continues on its way ··· */ 父进程继续运行? exit(0);? 假定在头文件ourbdr?h中定义了各个需要使用的变量。五个例程TELL 迹茫模*?nbsp; 2〗WAIT 、TELL 迹茫模*常病絇ARENT、TELL 迹茫模*常病紺HILD、WAIT 迹茫模*常病?nbsp; PAREN以及 wAIT 迹茫模*常病紺HILD可以是宏,也可以是函数。? 在后面的一些章中会说明以不同的方法实现这些TELL和WAIT例程:10?16节中说明 用信号的 一种实现,程序14?3中说明用流管道的一种实现。下面先看一个使用这五个例程 的实例。 ? 实例? 程序8?6输出两个字符串:一个由子进程输出,一个由父进程输出。因为输出依赖 于系统核 使进程运行的顺序及每个进程运行的时间长度,所以该程序包含了一个竟态条件。 ?? P205?? 程序8?6〓具有竟态条件的程序? 在程序中将标准输出设置为不带缓存的,于是每个字符输出都需调用一次write。 本例的目 的是使系统核能尽可能多次地在两个进程之间进行切换,以例示竟态条件。(如果 不这样做 ,可能也就决不会见到下面所示的输出。没有看到具有错误的输出并不意味着竟态 条件不存 在,这只是意味着在此特定的系统上未能见到它。)下面的实际输出说明该程序的 运行结果 是会改变的。? $ a?out? output from child? output from parent? $ a?out? oouuttppuutt ffrroomm cphairledn? t? $ a?out? oouuttppuutt ffrroomm pcahrielndt? $ a?out? ooutput from parent? utput from child? 修改程序8?6,使其使用TELL和WAIT函数,于是形成了程序8?7。在行首标以'+号 的行是 新增加的行。?? P206?? 程序8?7〓修改程序8?6以避免竟态条件? 运行此程序则能得到所予期的输出〖CD2〗两个进程的输出不再交叉混合。? 程序8?7是使父进程先运行。如果将fork之后的行改变成:? else if (pid==0) {? charatatime("output from child\n");? TELL 迹茫模*常病絇ARENT(getppid());? } else {? WAIT 迹茫模*常病紺HILD();〓〓/* child goes first */ 子进程先运行? charatatime("output from parent\n");? }? 则子进程先运行。练习8?3继续这一实例。? 8?9〓exec函数? 在8?3节曾提及用fork函数创建子进程后,子进程往往要调用一种exec函数以执行 另一道程 序。当一个进程调用一种exec函数时,该进程完全由新程序代换,而新程序则从其 main函数 开始执行。因为调用exec并不创建新进程,所以在其前后的进程ID并末改变。exe c只是用盘 上另一道新程序代换了当前进程的正文、数据、堆和栈段。? 有六种不同的exec函数可供使用,它们常常被统称为exec函数。这些exec函数都是 Unix进程 控制原语。用fork可以创建新进程,用exec可以执行新的程序。exit函数和两个w ait函数处 理终止和等待终止。这些是我们需要的基本的进程控制原语。在后面各节中将使用 这些原语 构造另外一些如popen和system之类的函数。? #include ? int execl(const char *?pathname,?const char *?arg0,???? /* (char *) 0 */) ;? int execv(const char *?pathname,?char *const ?argv?[]);? int execle(const char *?pathname,?const char *?arg0,????? /* (char *)0,char *const ?envp?[] */);? int execve(const char *?pathname,?char *const ?argv?[],char *cons t ?envp []);?? int execlp(const char *?filename,?const char *?arg0,???? /* (cha r *) 0 */ );? int execvp(const char *?filename,?char *const ?argv[]);?? All six return:-1 on error,no return on success? 六个函数都返回:出错为-1,成功不返回? 这些函数之间的第一个区别是前四个取路径名作为参数,后两个则取文件名作为参 数。当指 定文件名作为参数时:? ·如果文件名中包含'/',则就将其视为路径名。? ·否则就按PATH环境变量,在有关目录中搜寻可执行文件。? PATH变量包含了一张目录表(称为路径前缀),目录之间用冒号(':')分隔。例如下 列name =value环境字符串:? PATH=/bin:/usr/bin:/usr/local/bin:。? 指定在四个目录中进行搜索。(一个零长前缀地表示当前目录。在value的开始处可 用:表示 它,在行中间则要用::表示,在行尾以:表示它。)? 有很多出于安全性方面的考虑,要求在搜索路径中决不要包括当前目录。请参见G arfinkel and Spafford[1991]。? 如果execlp和execup这两个函数中的任一个使用路径前缀中的一个找到了一个可执 行文件, 但是该文件不是由连接编辑程序产生的机器可执行的代码文件,则就认为该文件是 一个shel l脚本,于是试着调用/bin/sh,并以该文件名作为shell的输入。? 第二个区别与参数表的传递有关(l表示表(list),v表示矢量(vector))。函数exe cl、execl p和execle要求将新程序的每个命令行参数都说明为一个单独的参数。这种参数表 以空指针 结尾。对于另外三个函数(execv,execvp和execve),则应先构造一个指向各参数的 指针数组 ,然后将该数组地址作为这三个函数的参数。? 在使用ANSIC原型之前,对execl,execle和execlp三个函数表示命令行参数的一般 方法是: char *arg0,char *arg1,???,char *argn,(char *) 0? 应当特别指出的是:在最后一个命令行参数之后跟了一个空指针。如果用常数0来 表示一个 空指针,则必须将它强制转换为一个字符指针,否则它将被解释为整型参数。如果 一个整型 数的长度与char*的长度不同,exec函数实际参数就将出错。? 最后一个区别与向新程序传递环境表相关。名字以e结尾的两个函数(execle和exe cve)使我 们可以向其传递一个指向环境字符串的指针数组的指针。其它四个函数则使用调用 进程中的 environ变量为新程序复制现存的环境。(请回忆7?9节及图7?5中对环境字符串的 讨论。其 中曾提及如果系统支持setenv和putenv这样的函数,则可更改当前环境和后面生成 的子进程 的环境,但不能影响父进程的环境。)通常,一个进程允许将其环境传播给其子进 程,但有 时也有这种情况,一个进程想要为一个子进程指定一个确定的环境。例如,在初始 化一个新 登录的shell时,login程序创建一个只定义少数几个变量的特殊环境,而在我们登 录时,可 以通过shell起动文件,将其它变量加到环境中。在使用ANSIC原型之前,execle的 参数是: ? char *pathname,char *arg0,…,char *argn,(char *)0,char * envpl)? 从中可见,最后一个参数是指向环境字符串的各字符指针构成的数组的指针。而在 ANSIC原 型中,所有命令行参数,包括空指针,envp指针都用省略号(…)表示。? 这六个exec函数的参数很难记忆。函数名中的字符会给我们一些帮助。字母p表示 该函数取 文件名作为参数,并且用PATH环境变量寻找可执行文件。字母l表示该函数取一个 参数表, 它与字母v互斥。v表示该函数取一个argv[]。最后,字母e表示该函数取envp[ ]数组, 而不使用当前环境。图8?5显示了这六个函数之间的区别。?? P209?? 图8?5〓六个exec函数之间的区别? 每个系统对参数表和环境表的总长度都有一个限制。在图2?7中,这种限制是ARG -MAX。在P OSIX?1系统中,此值至少是4096字节。当使用shell的文件名扩充功能产生一个文 件名表时 ,可能会受到此值的限制。例如,此命令? grep -POSIX 迹茫模*常病絊OURCE〓/usr/include/*/*?h? 在某些系统上可能产生下列形式的shell错:? arg〓〓list〓〓too〓〓long? 由于历史原因,系统V中此限制是5120字节。4?3BSD和4?3+BSD在分发时此限制是 20,480 字节。作者所用的系统则允许多至一兆字节!(见程序2?1的输出)? 前面曾提及在执行exec后,进程ID没有改变。除此之外,执行新程序的进程还保持 了原进程 的下列特征:? ·进程ID和父进程ID? ·实际用户ID和实际组ID? ·添加组ID? ·进程组ID? ·对话期ID? ·控制终端? ·闹钟尚余留的时间? ·当前工作目录? ·根目录? ·文件方式创建屏蔽字? ·文件锁? ·进程信号 帘为?nbsp; ·末决信号? ·资源限制? ·tms-utime,tms-stime,tms-cutime以及tms-ustime值? 对打开文件的处理与每个描述符的进程中每个在exec时关闭标志值有关。回忆图3 ?2以及3 ?13节中对PD-CLOEXEC的说明,打开描述符都有一个在exec时关闭标志。若此标志 设置,则 在执行exec时关闭该描述符,否则该描述符的打开除非特地用fcntl设置了该标志 ,否则系 统的默认操作是在exec后仍保持这种描述符打开。? POSIX?1明确要求在exec时关闭打开目录流。(回忆4?2)节中所述的opendir函数 。)这通常 是由opendir函数实现的,它调用fcntl函数为对应于打开目录流的描述符设置在e xec时关闭 标志。? 注意,在exec前后实际用户ID和实际组ID保持不变,而有效ID是否改变则取决于所 执行程序 的文件的设置用户ID位和设置组ID位是否设置。如果新程序的设置用户ID位已设置 ,则有效 用户ID变成程序文件属主的ID,否则有效用户ID不变。对组ID的处理方式与此相同 。? 在很多Unix实现中,这六个函数中只有一个execve是系统核的系统调用。另外五个 只是库函 数,它们最终都要调用系统调用。这六个函数之间的关系示于图8?6中。在这种安 排中,库 函数execlp和execvp使用PATH环境变量查找第一个包含名为filename的可执行文件 的路径名 前缀。?? P211? 图8?6〓六个exec函数之间的关系? 实际? 程序8?8例示了exec函数。?? P211? 程序8?8〓exec函数的实例? 在该程序中先调用execle,它要求一个路径名和一个特定的环境。下一个调用的是 execlp, 它用一个文件名,并将调用者的环境传送给新程序。execlp在这里能够工作的原因 是因为目 录/home/stevens/bin是当前路径前缀之一。注意,我们将第一个参数(在新程序中 的argv[ 0])设置为路径名的文件名分量。某些shell将此参数设置为完全的路径名。? 在程序8?8中要执行两次的程序echoall示于程序8?9中。这是一个普通程序,它 回送其所 有命令行参数及其全部环境表。? ? P212? 程序8?9〓回送所有命令行参数和所有环境字符串? 执行程序8?8时得到:? $ a?out? argv[0]:echoall? argv[1]:myarg1? argv[2]:MY ARG2? USER=unknown? PATH=/tmp? argv[0]:echoall? $ argv[1]:only 1 arg? USer=stevens? HOME=/home/stevens? LOGNAME=stevens? 31 ?more lines that aren't shown?其中31行没有显示? EDITOR=/usr/ucb/vi? 注意,shell提示出现在第二个exec打印argv[0]和argv[1]之间。这是因为父 进程并不 等待该子进程结束。? 8?10〓更改用户ID和组ID? 可以用setuid函数设置实际用户ID和有效用户ID。与此类似,可以用setgid函数设 置实际组 ID和有效组ID。? #include ? #include ? int setuid(uid 迹茫模*常病絫 ?uid);?? int setgid(gid 迹茫模*常病絫 ?gid);?? Both return:0 if OK,-1 on error两个函数返回:若成功为0,出错为-1? 关于谁能更改ID有若干规则。现在先考虑有关改变用户ID的规则(在这里关于用户 ID所说明 的一切都适用于组ID)。? 1?若进程具有超级用户特权,则setuid函数将实际用户ID,有效用户ID,以前保 存的设置 用户ID设置为uid。? 2?若进程没有超级用户特权,但是uid等于实际用户ID或保存的设置用户ID,则s etuid只将 有效用户ID设置为uid。不改变实际用户ID和保存的设置用户ID。? 3?如果上面两个条件都不满足,则errno设置为EPERM,并出错返回。? 在这里假 摇迹茫模*常病絇OSIX 迹茫模*常病絊AVED 迹茫模*常病絀DS为真。 如果没有 提供这种功能,则上面所说的关于保存的设置用户ID的部分都无效。? FIPS 151-1要求此功能。? SVR4支持 迹茫模*常病絇OSIX 迹茫模*常病絊AVED 迹茫模*常病絀DS功能。?
关于系统核所维护的三个用户ID,还要注意下列几点:? 1?只有超级用户进程可以更改实际用户ID。通常,实际用户ID是在用户登录时, 由login(1 )程序设置的,而且决不会改变它。因为login是一个超级用户进程,当它调用set uid时,设 置所有三个用户ID。? 2?仅当对程序文件设置了设置-用户-ID位时,exec函数再设置有效用户ID。如果 设置-用户 -ID位没有设置,则exec函数不会改变有效用户ID,而将其维持为原先值。任何时 候都可以 调用setuid,将有效用户ID设置为或者实际用户ID,或者保存的设置-用户-ID。自 然,不能 将有效用户ID设置为任一随机值。? 3?保存的设置-用户-ID是由exec从有效用户ID复制的。在exec按文件用户ID设置 了有效用 户ID后,即进行这种复制,并将此副本保存起来。? 图8?7摘要列出了改变这三个用户ID的不同方法。?? P214?? 图8?7〓改变三个用户ID的不同方法? 注意,用8?2节中所述的getuid和geteuid函数只能获得实际用户ID和有效用户ID 的当前值 。我们不能获得所保存的设置-用户-ID的当前值。? 实例? 为了说明保存的设置-用户-ID特征的用法,让我们先观察一个使用该特征的程序。 我们所观 察的是贝克莱tip(1)程序(系统V的cu(1)程序与此类似。)这两个程序都连接到一个 远程系统 ,或者是直接连接,或者是拨号一个调制解调器。当tip使用一个调制解调器时, 它必须通 过使用锁文件来独占使用它。此锁文件是与UUCP共享的,因为这两个程序可能在同 时要使用 同一调制解调器。将发生下列步骤:? 1?tip程序文件是由用户uucp拥有的,并且其设置-用户-ID位已设置。当exec此程 序时,则 关于用户ID得到下列结果:? 实际用户ID=我们的用户ID? 有效用户ID=UUCP? 保存设置用户ID=UUCP? 2?tip存取所要求的锁文件。这些锁文件是由名为UUCP的用户所拥有的,因为有效 用户ID是 UUCP,所以tip可以存取这些锁文件。? 3?tip执行setuid(getuid())。因为tip不是超级用户进程,所以这仅仅改变有效 用户ID。 此时得到:? 实际用户ID=我们的用户ID(未改变)? 有效用户ID=我们的用户ID(未改变)? 保存设置用户ID=UUCP(未改变)? 现在,tip进程是以我们的用户ID作为其有效用户ID而运行的。这就意味着能存取 的只是我 们通常可以存取的,没有附加的存取权。? 4?当执行完我们所需的操作后,tip执行setuid(uucpuid),其中uucpuid是用户u ucp的数值 用户ID(tip很可能在起动时调用geteuid,得到uucp的用户ID,然后将其保存起来 ,我们并 不认为tip会搜索口令字文件以得到这一数值用户ID。)因为setuid的参数等于保存 的设置- 用户-ID,所以这种调用是许可的(这就是为什么需要保存的设置-用户-ID的原因。 )现在得 到:? 实际用户ID=我们的用户ID(末改变)? 有效用户ID=uucp? 保存设置用户ID=uucp(末改变)? 5?tip现在可对其锁文件进行操作以释放它们,因为tip的有效用户ID是。? 以这种方法使用保存的设置-用户-ID,在进程的开始和结束部分就可以使用由于程 序文件的 设置用户ID而得到的额外的优先权。但是,进程在其运行的大部分时间只具有普通 的许可权 。如果进程不能在其结束部分切换回保存的设置-用户-ID,那么就不得不在全部运 行时间都 保持额外的许可权(这可能会造成麻烦)。? 让我们来看一看如果在tip运行时为我们生成一个shell进程(先fork,然后exec)将 发生什么 。因为实际用户ID和有效用户ID都是我们的普通用户ID(上面的第三步),所以该s hell没有 额外的许可权。它不能存取tip运行时设置成uucp的保存的设置-用户-ID,因为该 shell的保 存的设置-用户-ID是由exec复制有效用户ID而得到的。所以在执行exec的子进程中 ,所有三 个用户ID都是我们的普通用户ID。如若程序是设置-用户-ID为root,那么我们关于 tip如何使 用setuid所作的说明是不正确的。因为以超级用户特权调用setuid就会设置所有三 个用户ID 。使上述实例按我们所说明的进行工作,只需setuid设置有效用户ID。? setreuid和setregid函数? 4?3+BSD支持函数,其功能是交换实际用户ID和有效用户ID的值。? #include ? #include ? int setreuid(uid 迹茫模*常病絫 ?ruid,?uid 迹茫模*常病絫 ?euid);??
int setregid(gid 迹茫模*常病絫 ?rgid,?gid 迹茫模*常病絫 ?egid);??
Both return:0 if OK,-1 on error〓两个函数返回:若成功为0,出错为-1? 其作用很简单:一个非特权用户总能交换实际用户ID和有效用户ID。这就允许一个 设置-用 户-ID程序转换成只具有用户的普通许可权,以后又可再次转换回设置-用户-ID所 得到的额 外许可权。POSIX?1引进了保存的设置-用户-ID特证后,其作用也相应加强,它也 允许一个 非特权用户将其有效用户ID设置为保存的设置-用户-ID。? SVR4在其BSD兼容库中也提供这两个函数。? 4?3BSD并没有上面所说的保存的设置-用户-ID功能。它用setreuid和setregid来 代替。这 就允许一个非特权用户前、后交换这二个用户ID的值,而4?3BSD中的tip程序就是 用这种功 能编写的。但是要知道,当此版本生成shell进程时,它必须在exec之前,先将实 际用户ID 设置为普通用户ID。如果不这样做的话,那么实际用户ID就可能是uucp(由setreu id的交换 操作造成。)然后shell进程可能会调用setreuid交换两个用户ID值并取得uucp许可 权。作为 一个保护性的程序设计措施,tip将子进程的实际用户ID和有效用户ID都设置成普 通用户ID 。? seteuid和setegid函数? 在对POIX?1的建议更改中包含了两个函数seteuid和setegid。它们只更改有效用 户ID和有 效组ID。? #include ? #include ? int seteuid(uid 迹茫模*常病絫 ?uid);?? int setegid(gid 迹茫模*常病絫 ?gid);?? Both return:0 if OK,-1 on error〓两个函数返回:若成功为0,出错为-1? 一个非特权用户可将其有效用户ID设置为其实际用户ID或其保存的设置-用户-ID。 对于一个 特权用户则可将有效用户ID设置为uid。(这区别于setuid函数,它更改三个用户I D。)这一 建议更改也要求支持保存的设置-用户-ID。? SVR4和4?3+BSD都支持这两种函数。? 图8?8〓摘要列出了本节所述的修改三个不同的用户ID的各个函数。?? P217?? 图8?8〓设置不同的用户ID的各函数摘要? 组ID? 至此,在本章中所说明的一切都以类似方式适用于各个组ID。添加组ID不受setgi d函数的影 响。? 8?11〓解释器文件? SVR4和4?3+BSD都支持解释器文件。这种文件是文本文件,其起始行的形式是:?
#! pathname[optional-argument]? 在骛叹号和pathname之间的空格是可任选的。最常见的是以下列行开始:? #!/bin/sh? pathname通常是个绝对路径名,对它不进行什么特殊的处理(不使用PATH进行路径 搜索)。对 这种文件的识别是由系统核作为exec系统调用处理的一部分来完成的。系统核使调 用exec函 数的进程实际执行的文件并不是该解释器文件,而是在该解释器文件的第一行中p athname所 指定的文件。一定要将解释器文件(文本文件,它以#!开头)和解释器(由该解释器 文件第一 行中的pathname指定)区分开来。? 要了解,很多系统对解释器文件第一行有长度限止(32个字符)。这包括#!,path name,选 择参数以及空格数。? 实例? 让我们观察一个实例,从中了解当被执行的文件是个解释器文件时,系统核对exe c函数的参 数及该解释器文件第一行的可任选参数作何种处理。程序8?10调用execl执行一个 解释器文 件。?? P218?? 程序8?10〓执行一个解释器文件的程序? 下面先显示要被执行的该解释器文件(只有一行)的内容,接着是运行程序8?10的 结果。? $ cat /home/stevens/bin/testinterp? #!/home/stevens/bin/echoarg foo? $ a?out? argv[0]: /home/stevens/bin/echoarg? argv[1]: foo? argv[2]: /home/stevens/bin/testinterp? argv[3]: myarg1? argv[4]: MY ARG2? 程序ecboarg(解释器)回送每一个命令行参数。(它就是程序7?2。)注意,当系统 核exec该 解释器(/home/stevens/bin/ecboarg)时,argv[0]是该解释器的路径名,argv[ 1]是解 释器 文件中的可任选择参数,其余参数是路径名(/bome/stevens/bin/testinterp),以 及程序8 ?10中调用execl的第二和第三个参数(myarg1,和MY ARG2)。调用execl时的argv[ 1]和arg v[2]和argv[3]已向右移了两个位置。? 注意,系统核取了execl中的路径名以代替第一个参数(testinterp),因为一般路 径名包含 了较第一个参数更多的信息。? 实例? 在解释器路径名(pathname)后可跟随任选参数,它们常用于为支持-f任选项的程序 指定该任 选项。例如,可以以下列方式执行awk(1)程序:? awk -f myfile? 它告诉awk从文件myfile中读awk程序? 在很多系统中,有awk的两个版本。awk常常被称为"老awk",它是与version 7一起 分发的 原始版本。nawk(新awk)包含了很多增强功能,对应于在Aho,Kernighan和Weinber ger[1988 ] 中说明的语言。此新版本提供了对命令行参数的存取,这是下面的例子所需的。S VR4提供了 两者,老的awk既可用awk也可用oawk调用,但是SVR4已说明在将来的版本中awk将 是nawk。P OSIX?2章节中将新awk语句就称为awk,这正是在本书中所使用的。? 在解释器文件中使用-f任选项,使我们可以写出:? #!/bin/awk -f? (在此解释器文件中后随awk程序)? 例如,程序8?11是一个在/usr/local/bin/awkexample解释器文件中的程序。??
P218?? 程序8?11〓在一个解释器文件中的awk程序。? 如果路径前缀之一是/usr/local/bin,则可以下列方式执行程序8?11(假定我们已 打开了该 文件的执行位):? $ awkexample filel FILENAME2 f3? ARGV[0]=/bin/awk? ARGV[1]=file1? ARGV[2]=FILENAME2? ARGV[3]=f3? 执行/bin/awk时,其命令行参数是:? /bin/awk -f /usr/local/bin/awkexample file1 FILENAME2 f3? 解释器文件的路径名(/usr/local/bin/awkexample)传送给解释器。因为不能期望 该解释器( 在本例中是/bin/awk)会使用PATH变量定位该解释器文件,所以只传春路径名中的 文件名是 不够的。当awk读解释器文件时,因为'#'是awk的注释字符,所以在awk读解释器 文件时 ,它忽略第一行。? 可以用下列命令验证上述命令行参数。 $ su〓〓成为超级用户? Password:〓输入超级用户口令? # mv/bin/awk /bin/awk?save〓保存原先的程序? # cp /home/stevens/bin/echoarg /bin/awk〓暂时代换它? # suspend〓用作业控制挂起超级用户shell? [1]+Stopped〓〓su? $ awkexample file1 FILENAME2 f3? argv[0]: /bin/awk? argv[1]: -f? argv[2]: /usr/local/bin/awkexample? argv[3]: file1? argv[4]: FILENAME2? argv[5]: f3? $ fg〓〓用作业控制恢复超级用户shell? su? # mv /bin/awk?save /bin/awk〓恢复原先的程序? # exit〓终止超级用户shell? 在此例子中,解释器的-f任选项是需要的。正如前述,它告诉awk在什么地方得到 awk程序 。如果在解释器文件中删除-f任选面,则其结果是:? $ awkexample file1 FILENAME2 f3? /bin/awk:syntax error at source line 1? context if? >>> /user/local <<< /bin/awkexample? /bin/awk: bailing out at source line 1? 因为在这种情况下命令行参数是:? /bin/awk /usr/local/bin/awkexampel file1 FILENAME2 f3? 于是awk企图将字符串/usr/local/bin/awkexample解释为一个awk程序。如果不能 向解释器 至少传递一个可任选参数(在本例中是-f),那么这些解释器文件只有对shell才是 有用的。 ? 是否一定需要解释器文件呢?那也不完全如此。但是它们确实使用户得到效率方面 的好处, 其代价是系统核的额外开销(因为系统核需要识别解释器文件)。由于下述理由,解 释器文件 是有用的。? 1?某些程序是用某种语言写的脚本,这一事实可以隐藏起来。例如,为了执行程 序8?11, 只需使用下列命令行:? awkexample optional-arguments? 而并不需要知道该程序实际上是一个awk脚本,否则我们就要以下列方式执行该程 序:? awk -f awkexample optional-arguments? 2?解释器脚本在效率方面也提供了好处。再考虑一下前面的例子。我们仍旧隐藏 该程序是 一个awk脚本的事实,但是将其放在一个shell脚本中:? awk 'BEGIN {? for(i=0;iprintf "ARGV[%d]=%s\n",i,ARGVp[i]? exit? }' $*? 这种解决方法的问题是要求做更多的工作。首先,shell读此命令,然后试图exec lp此文件 名。因为shell脚本是一个可执行文件,但却不是机器可执行的,于是返回一个错 误;然后 ,execlp就认为该文件是一个shell脚本(它实际上就是这种文件)。然后,exec / bin/sh, 并以该Shell脚本的路径名作为其参数。shell正确地解释执行我们的shell脚本, 但是为了 运行awk程序,它调用fork,exec和wait。用一个shell脚本代替解释器脚本有更多 的开销。 ? 3?解释器脚本使我们可以使用除/bin/sh以外的其它shell来编写shell脚本。当e xeclp找到 一个非机器可执行的可执行文件时,它总是调用/bin/sh来解释执行该文件。但是 用解释器 脚本,则可编写成:? #!/bin/csh? (在此解释器文件中后随C Shell脚本)? 再一次,我们也可将此放在一个/bin/sh脚本中(然后由其调用C shell),但是要有 更多的开 销。? 如果三个shell和awk没有用'#'作为注释符,则我们上面说的都不会工作。? 8?12〓System函数? 在一个程序中执行一个命令字符串是方便的。例如,假定我们要将时间和日期放到 一个文件 中,则可使用6?9节说明的函数实现这一点。调用time得到当前日历时间,接着调 用localt ime将日历时间变换为年、月、日、时、分、秒、周日形式,然后调用Strftime对 上面的结 果进行格式化处理,最后将结果写到文件中。但是用下面的system函数则更容易做 到这一点 。? system("date>file");? ANSI C定义了system函数,但是其操作是强烈依赖于系统的。? 因为system不属于操作系统界面而是shell界面,所以POSIX?1没有定义它,POSI X?2则正 在对其进行标准化。下列说明与POSIX?2标准的草案11?2相一致。? #include ? int system(const char *?cmdstring);?? Returns:(see below)返回:(见下)? 如果cmdstring是一个空指针,则仅当命令处理程序可用时,system返回非0值,使 用这一特 征可以决定在一个给定的操作系统上是否支持system函数。在Unix中,system总是 可用的。 ? 因为system在其实现中调用了fork、exec和waitpid,因此有三种返回值:? 1?如果fork失败或者waitpid返回除EINTR之外的出错,则system返回-1,而且er ro中设置 了错误类型。? 2?如果exec失败(表示不能执行shell),则其返回值如同shell执行了exit(127)一 样。? 3?否则所有三个函数(fork,exec和waitpid)都成功,则system的返回值是shell的 终止状态 ,其格式是在waitpid中所说明的。? 如果waitpid是一个捕捉到的信号中断,则system很多当前的实现都返回一个错误 (EINTR), 要求在这种情况下system不返回一个错误已被加到POSIX?2的最近草案中。(在10 ?5节中将 讨论被中断的系统调用。)? 程序8?12是system函数的一种实现。它对信号没有进行处理。在10?18节中将修 改此函数 使其进行信号处理。? Shell的-C任选项告诉shell程序取下一个命令行参数(在这里是cmdstring)作为命 令输入(而 不是从标准输入或从一个给定的文件中读命令)。Shell对以null字符终止的命令字 符串进行 语法分析,将它们分成分隔开的命令行参数。传递给shell的实际命令串可以包含 任一有效 的shell命令。例如,可以用<和>对输入和输出重新定向。? 如果不使用shell执行此命令,而是试图由我们自己去执行它,那么这会是相当困 难的。首 先,我们必须用execlp而不是execl,象shell那样使用PATH变量。我们必须将nul l符结尾的 命令字符串分成各个命令行参数,以便衰和execlp。最后,我们也不能使用任何一 个shell 元字符。? 注意,我们调用-exit而不是exit。这是为了防止任一标准I/O缓存(这些缓存会在 fork中由 父进程复制到子进程)在子进程中被刷新。?? P223?? 程序8?12〓system函数(没有对信号进行处理)? 用程序8?13对这种实现的system函数进行测试(pr-exit函数定义在程序8?3中)。 运行程序 8?13得到:? $ a?out? Thu Aug 29 14:24:19 MST 1991? normal termination,exit status=0〓〓对于date? sh:nosuchcommand:not found? normal termination,exit status=1〓对于无此种命令? stevens console Aug 25 11:49? stevens ttyp0 〓Aug 29 05:56? stevens ttyp1 〓Aug 29 05:56? stevens ttyp2 〓Aug 29 05:56? normal termination,exit status=44〓对于exit?? 程序8?13〓调用system函数? 使用system而不是直接使用fork和exec的优点是:system进行了所需的各种出错处 理,以及 各种信号处理(在10?18节中的下一个版本system函数中)。? 在Unix的早期版本中,包括SVR3?2和4?3BSD,都没有waitpid函数,于是父进程 用下列形 式的语句等待子进程:? while((lastpid=wait(& status)) !=pid && lastpid !=-1)? ;? 如果调用system的进程在调用它之前已经生成一个子进程(并exec一道程序),则就 会引起问 题。因为上面的while语句一直循环执行,直到由system产生的子进程终止才停止 ,如果其 任一个不是用pid标识的子进程在此之前终止,则它们的进程ID和终止状态都被wh ile语句丢 弃。确实,由于wait不能等待一个指定的进程,POSIX?1才为此及其它一些原因定 义了wait pid函数。如果不提供waitpid,对于popen和pclose函数也会发生同样的问题。?
设置-用户-ID程序? 如果在一个设置-用户-ID程序中调用system,那么发生什么呢?这是一个安全性方 面的漏洞 ,决不应当这样做。程序8?14是一个简单程序,它只是对其命令行参数调用syte m函数。? ? P225? 程序8?14〓用system执行命令行参数? 将此程序编译成可执行目标文件tsys。? 程序8?15是另一个简单程序,它打印其实际和有效用户ID。?? P225? 程序8?15〓打印实际和有效用户ID? 将此程序编译成可执行目标文件printuids运行这两个程序,得到下列结果:? $ tsys printuids〓〓正常执行,无特权? real uid=224,effective uid=224? normal termination,exit status=0? $ su〓〓成为超级用户? Password:〓〓输入超级用户口令? # chown root tsys〓〓更改属主? # chmod u+s tsys〓〓增加设置用户-ID? # ls -1 tsys〓〓检验文件许可权和属主? -rwsrwxr-x 1 root〓105737 Ang 18 11:21 tsys? # exit〓〓终止超级用户shell? $ tsys printuids? real uid=224,effective uid=0〓〓?oops,?这是一个安全性空洞? normal termination,exit status=0? 我们给予tsys程序的超级用户许可权在system中执行了fork和exec之后仍被保持下 来,也就 是说执行system中shell命令的进程也是有了超级用户许可权。? 如果一个进程正以特殊的许可权(设置-用户-ID或设置-组-ID)运行,它又想生成另 一个进程 执行另一道程序,则它应当直接使用fork和exec,而且在fork之后,exec之前要改 回到普通 许可权。设置-用户-ID或设置-组-ID程序决不应调用system函数。? 这种警告的一个理由是:system调用shell对命令字符串进行语法分析,而shell则 使用IFS 变量作为其输入字段分隔符。较早的shell版本在被调用时不将此变量恢复为普通 字符集。 这就允许一个不怀好意的用户在调用system之前设置IFS,造成system执行一个不 同的程序 。? 8?13〓进程会计? 很多Unix系统提供了一个任选项以进行进程会计事务处理。当取了这种任选项后, 每当进程 结束时系统核就写一个会计记录。典型的会计记录是32个字节长的二进制数据,包 括命令名 、所使用的CPU时间总量、用户ID和组ID、起动时间等。在本节我们要比较细仔地 说明这种 会计记录,这样也使我们得到了一个再次观察进程的机会,得到了使用5?9节中所 介绍的fi lad函数的机会。? 任一标准都没有对进程会计进行过说明。本节的说明依据SVR4和4?3+BSD实现。S VR4提供了 很多程序处理这种原始的会计数据〖CD2〗例如见runacct和acctcom。4?3+BSD提 供sa(8)处 理并摘要原始会计数据。? 一个至今没有说明过的函数(acct)起动和终止进程会计。唯一使用这一函数的是S VR4和4?3 +BSD的acction(8)命令。超级用户执行一个带路径各参数的acction命令起动会计 处理。该 路径名通常是/var/adm/pacct(较老的系统中这是/usr/adm/acct。执行不带任何参 数的acct 命令则停止会计处理。? 会计记录结构定义在头文件中,其样式是:? typedef u 迹茫模*常病絪hort comp 迹茫模*常病絫; /* 〓3位基数,8位指数 ,13位分 数? struct acct? {? char〓ac 迹茫模*常病絝lag; /*〓标志(见图8?9)? char〓ac 迹茫模*常病絪tat; /*〓终止状态(只是信号和core标志)? 〓〓〓〓〓/*〓(BSD系统不提供)? uid 迹茫模*常病絫〓ac 迹茫模*常病絬id;〓/*〓实际用户ID? gid 迹茫模*常病絫〓ac 迹茫模*常病絞id;〓/*〓实际组ID? dev 迹茫模*常病絫〓ac 迹茫模*常病絫ty;〓 ?〓控制终端? time 迹茫模*常病絫〓ac 迹茫模*常病絙time;〓 ?〓起始日历时间? comp 迹茫模*常病絫〓ac 迹茫模*常病絬time; 〓/*〓用户CPU时间(滴答)? comp 迹茫模*常病絫〓ac 迹茫模*常病絪time;〓/*〓系统CPU时间(滴答)? comp 迹茫模*常病絫〓ac 迹茫模*常病絜time;〓 ?〓经过的时间(滴答)? comp 迹茫模*常病絫〓ac 迹茫模*常病絤em;〓/〓*平均使用的内存? comp 迹茫模*常病絫〓ac 迹茫模*常病絠o;〓/*〓读、写传输的字节数? comp 迹茫模*常病絫〓ac 迹茫模*常病絩w;〓/*〓块读或写 ?常??nbsp; char ac-comm[B]; 〓/?命令名:SVR4是 郏 ],4?3+BSD是[10 荨?常?nbsp; ? };? 由于历史传统,贝克莱系统,包括4?3+BSD不提供ac-stat变量。? 其中,ac-flag记录了进程执行期间的某些事件。这些事件如图8?9中所示。? ?? P227?? 图8?9〓会计记录中的ac-flag值? 会计记录所需的各个数据(各CPU时间,传输的字符数等)都由系统核保存在进程表 中,并在 一个新进程被创建时置初值(例如fork之后在子进程中)。一个进程终止时写一个会 计记录。 这就意味着在会计文件中记录的顺序对应于进程终止的顺序,而不是它们起动的顺 序。为了 确定起动顺序,须要读全部会计文件,并按起动日历时间?信判颉U獠皇且恢趾?nbsp; 完善的方 法,因为日历时间的单位是秒(1?10节),在一个给定的秒中可能起动了多个进程 。而墙上 时钟时间的单位是时钟滴答(通常,每秒滴答数在50~100之间)。但是我们并不 知道进程 的终止时间,我们所知道的只是起动时间和终止顺序。这就意味着,即使墙上时间 比起动时 间要精确得多,但是我们仍不能按照会计文件中的数据重构各进程的精确起动顺序 。? 会计记录对应于进程而不是程序。在fork之后,系统核为子进程初始化一个记录, 这不是在 一个新程序被执行时。虽然exec并不创建一个新的会计记录,但相应记录中的命令 名改变了 ,AFORK标志则被清除。这意味着,如果一个进程顺序exec了三道程序(A exec B, B exec C, 最后C exit),但只写一个会计记录。在该记录中的命令名对应于程序C,但CPU时 间是程序A 、B、C之和。? 实例? 为了得到某些会计数据以便查看,运行程序8?16,它调用fork四次。每个子进程 做不同的 事情,然后终止。此程序所做的基本工作示于图8?10中。? 程序8?17则从会计记录中选择出一些字段并打印出来。?? P228? 程序8?16〓产生会计数据的程序?? P229? 图8?10〓会计处理实例的进程结构? 然后,执行下列操作步骤:? 1?变成为超级用户,用acction命令起动会计事务处理。注意,当此命令结束时, 会计事务 处理已经起动,因此在会计文件中的第一个记录应来自这一命令。? 2?运行程序8?16。这会加五个记录到会计文件中(父进程1个,四个子进程各一个 )。在第 二个子进程中,execl并不创建一个新进程,所以对第二个进程只有一个会计记录 。? 3?变成为超级用户,停止会计事务处理。因为在acction命令终止时已停止处理会 计事务, 所以不会在会计文件中增加一个记录。? 4?运行程序817,从打印文件中选出字段并打印之。? 步骤4的输出如下面所示。在每一行以斜体字对进程加了说明,以便后面讨论。?
accton〓e 〓7,chars〓64,stat=〓0:〓S? dd〓e=〓37,chars=221888,stat〓0:〓〓第2子进程? a?out〓e=〓128,chars=〓0,stat=〓0:〓〓父进程? a?out〓e=〓274,chars=〓0,stat=134:F ?摹。亍??子进程? a?out〓e=〓360,chars=〓0,stat=〓9:F〓〓X〓第4子进程? a?out〓e=〓484,chars=〓0,stat=〓0:F〓〓第3子进程? ? P230? 程序8?17〓打印从系统会计文件中选出的字段? 墙上日历时间值的单位是CLK 迹茫模*常病絋CK。从图2?6中可见,本系统的值是 60。例如 ,在父进程中 的sleep(2)对应于墙上日历时间128个时钟滴答。对于第一个子进程,sleep(4)变 成274时钟 滴答。注意,一个进程睡眠的时间总量并不精确(在第十章中将返回到sleep函数。 )调用for k和exit也要一些时间。? 注意,ac-stat并不是进程的真正终止状态。它只是8?6节中讨论的终止状态的一 部分。如 果进程异常终止,则在此字节中的信息只是core标志位(一般是最高位)以及信号编 号数(一 般是低7位)。如果进程正常终止,则从会计文件不能得到进程的退出(exit)状态。 对于第一 个进程,此值是128+6。128是core标志位,6是此系统上信号SIGBART的值(它是由 调用abort 产生的)。第四个子进程的值是9,它对应于SIGKILL的值。从会计文件的数据中不 能得到父 进程在exit时所用的参数值是2,三个子进程exit时所用的参数值是0。? dd进程复制到第二个子进程中的文件/boot的长度是110,888 byte。而I/O字符数 是此值的 二倍,因为读了110,888byta,然后又写了110,888byte。即使输出到null设备, 仍对I/O 字符数?屑扑恪*?nbsp; ac-flag值与我们所予料的相同。除调用了execl的第二个子进程以外,其它子进程 都设置了 F标志。父进程没有设置F标志,其原因是交互式shell曾调用过fork生成父进程, 然后调用e xec a?out文件。调用了abort的第一个子进程的core转储标志(D)打开。因为abo rt产生信 号 SIGABRT以产生core转储。该进程的X标志也打开,因为它是由信号终止的。第四个 子进程的 X标志也打开,但是SIGKILL信号并不产生core转储,它只是终止该进程。? 最后要说明的是:第一个子进程的I/O字符数为O,但是该进程产生了一个core文件 。其原因 是写core文件所需的I/O并不由该进程负担。? 8?14〓用户标识? 任一进程都可以得到其实际和有效用户ID及组ID。但是有时希望找到运行该程序的 用户的登 录名。我们可以调用getpwuid(getuid()),但是如果一个用户有多个登录名,这些 登录名又 对应着同一个用户ID,这又将如何呢?(一个人在口令字文件中可以有多个记录项, 它们的用 户ID相同,但登录shell则不同)。系统通常保存用户的登录名(6?7节),用getlo gin函数可 以存取此登录名。? #include ? char *getlogin(void);? Returns:pointer to string giving login name if OK,NULL on error?返回: 若成功为 指向登录名字符串的指针,出错为NULL。? 如果调用此函数的进程没有连接到用户登录时所用的终端,则本函数会失败。我们 通常称这 些进程为精灵进程,将在第十三章对这种进程专门进行讨论。? 得到了登录名,就可用getpwnam在口令字文件查找相应记录以确定其登录shell等 。? 为了找到登录名,Unix系统在历史上一直是调用tlyname函数(11?9节),然后在u tmp文件(6 ?7节)中找匹配项。4?3+BSD将登录名存放在进程表项中,并提供系统调用存、取 该登录名 。? 系统V提供cuserid函数返回登录名。此函数先调用getlogin函数,如果失败则再调 用getpwu id(getuid())。IEEE S+d?1003?1-1988说明了cuserid,但是它以有效用户ID而 不是实际 用户ID来调用。POSIX?1的1990最后版本,删除了cuserid函数。? FIPS151-1要求登录shell定义一个环境变量LOGNAME,其值为用户的登录名。在4? 3+BSD中, 此变量由login设置,并由登录shell继承。但是,要理解一个用户是可以改变环境 变量的, 所以我们不能使用LOGNAME来确认用户。作为代替,应当使用getlogin函数。? 8?15〓进程时间? 在1?10节中说明了墙上时钟时间、用户CPU时间和系统CPU时间。任一进程都可调 用times函 数以获得它自己及终止子进程的上述值。? #include ? clock 迹茫模*常病絫 times(struct tms *?buf));?? Returns:elapsed wall clock time in clock ticks if OK,-1 on error? 返回:若成功为经过的墙上时钟时间(单位:滴答),出错为-1? 此函数填写由buf指向的tms结构,该结构定义是:? struct tms{? clock 迹茫模*常病絫 tms 迹茫模*常病絬time; /* user CPU time */〓用户C PU时间? clock 迹茫模*常病絫 tms 迹茫模*常病絪time; /* system CPU time */ 系统 CPU时间? clock 迹茫模*常病絫 tms 迹茫模*常病絚utime; /* user CPU time,terminat ed childr en */各终止子进程的用户CPU时间? clock 迹茫模*常病絫 tms 迹茫模*常病絚stime; /* system CPU time,termin ated chil dren */?各终止子进程的系统CPU时间? 注意,此结构没有包含墙上时钟时间。作为代替times此函数以函数值返回墙上时 钟时间。 此值是相对于过去的某一时刻度量的,所以不能用其绝对值而必须使用其相对值。 例如,调 用times,保存其返回值。在以后某个时间再次调用times,从新返回的值中减去以 前返回的 值。此差值就是墙上时钟时间。(一个长期运行的进程可能其墙上时钟时间会溢出 ,当然这 种可能性极小。? 结构中两个针对子进程的字段包含了此进程已等待到的各子进程的值。? 所有由此函数返回的CLOCK-t值都用-SC-CLK-TCK(由sysconf函数返回的每秒时钟滴 答数,见 2?5?4节)换成秒数。? 贝克莱类系统,包括4?3BSD继承了Version 7的times版本,它不返回墙上时钟时 间。这一 老版本如果执行成功返回0,如失败则返回-1。4?3+BSD支持POSIX?1版本。? 4?3+BSD和SVR4(在BSD兼容库中)提供了getrusage(2)函数,此函数返回CPU时间, 以及指示 资源使用情况的另外十四个值。? 实例? 程序818将每个命令行参数作为Shell命令串执行,对每个命令计时,并打印从tms 结构取得 的值。按下列方式运行此程序,得到:? $ a?out "sleep 5" "date"? command: sleep 5? real:〓〓5?25? user:〓〓0?00? sys:〓〓0?00? child user:〓〓0?02? child sys:〓〓0?13? normal termination,exit status=0? command:date? Sun Aug 18 09:25:38 MST 1991? real :〓〓0?27? user:〓〓0?00? sys:〓〓〓0?00? child user:〓〓0?05? child sys:〓〓0?10? normal termination,exit status=0? 在两个实例中,在child user和child sys行中显示的时间是执行shell和命令的子 进程所使用的CPU时间。??? 程序8?18〓时间以及执行命令行参数? 让我们再运行1?10节中的例子:?? P235?? 如同所期望的那样,所有三个值(实际时间和子进程CPU时间)都与1?10高中的值相 近。? 8?16〓摘要? 对在Unix环境中的高级程序设计而言,完整地了解Unix的进程控制是非常重要的。 其中必须熟练掌握的只有几个〖CD2〗fork、exec族、-exit、wait和waitpid。很多 应用程序都使用这些原语。fork原语也给了我们一个了解竟态条件的机会。? 在本章中说明了system函数和进程会计,与此同时也使我们可以看到这些进程控制 函数的应用情况。本章还说明了exec函数的另一种变体;解释器文件及它们工作的 方式。对各种不同的用户ID和组ID(实际,保存的)的理解和编写安全的设置用户ID程序是至 ?nbsp; 重要的。
? 在下一章我们在了解进程和子进程的基础上,进一步说明进程和其它进程的关系〖 CD2〗对话期和作业控制。在第十章我们将说明信号机制以此结束对进程的讨论。
| | |