Linux 进程控制

本章介绍U N I X的进程控制,包括创建新进程、执行程序和进程终止。还将说明进程的各
种I D—实际、有效和保存的用户和组I D,以及它们如何受到进程控制原语的影响。本章也包
括了解释器文件和s y s t e m函数。本章以大多数 U N I X系统所提供的进程会计机制结束。这使我
们从一个不同角度了解进程控制功能。
8.2 进程标识
每个进程都有一个非负整型的唯一进程I D。因为进程I D标识符总是唯一的,常将其用做其
他标识符的一部分以保证其唯一性。 5 . 1 3节中的t m p n a m函数将进程I D作为名字的一部分创建
一个唯一的路径名。
有某些专用的进程:进程ID 0是调度进程,常常被称为交换进程( s w a p p e r )。该进程并不执
行任何磁盘上的程序—它是内核的一部分,因此也被称为系统进程。进程 ID 1通常是i n i t进
程,在自举过程结束时由内核调用。该进程的程序文件在 U N I X的早期版本中是/ e t c / i n i t,在较
新版本中是/ s b i n / i n i t。此进程负责在内核自举后起动一个 U N I X系统。i n i t通常读与系统有关的
初始化文件( / e t c / r c*文件),并将系统引导到一个状态(例如多用户)。i n i t进程决不会终止。它是
一个普通的用户进程(与交换进程不同,它不是内核中的系统进程 ),但是它以超级用户特权运
行。本章稍后部分会说明i n i t如何成为所有孤儿进程的父进程。
在某些U N I X的虚存实现中,进程ID 2是页精灵进程( p a g e d a e m o n )。此进程负责支持虚存系
统的请页操作。与交换进程一样,页精灵进程也是内核进程。
除了进程I D,每个进程还有一些其他标识符。下列函数返回这些标识符。
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void); 返回:调用进程的进程I D
pid_t getppid(void); 返回:调用进程的父进程 I D
uid_t getuid(void); 返回:调用进程的实际用户 I D
uid_t geteuid(void); 返回:调用进程的有效用户 I D
gid_t getgid(void); 返回:调用进程的实际组 I D
gid_t getegid(void); 返回:调用进程的有效组 I D
注意,这些函数都没有出错返回,在下一节中讨论 f o r k函数时,将进一步讨论父进程I D。4 . 4节
中已讨论了实际和有效用户及组I D。
8.3 fork函数
一个现存进程调用f o r k函数是U N I X内核创建一个新进程的唯一方法 (这并不适用于前节提
及的交换进程、i n i t进程和页精灵进程。这些进程是由内核作为自举过程的一部分以特殊方式
创建的)。
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
返回:子进程中为0,父进程中为子进程I D,出错为-1 由f o r k创建的新进程被称为子进程( child process)。该函数被调用一次,但返回两次。两次返
回的区别是子进程的返回值是 0,而父进程的返回值则是新子进程的进程 I D。将子进程I D返回
给父进程的理由是:因为一个进程的子进程可以多于一个,所以没有一个函数使一个进程可以
获得其所有子进程的进程 I D。f o r k使子进程得到返回值 0的理由是:一个进程只会有一个父进
程,所以子进程总是可以调用g e t p p i d以获得其父进程的进程I D (进程ID 0总是由交换进程使用,
所以一个子进程的进程I D不可能为0 )。
子进程和父进程继续执行 f o r k之后的指令。子进程是父进程的复制品。例如,子进程获得
父进程数据空间、堆和栈的复制品。注意,这是子进程所拥有的拷贝。父、子进程并不共享这
些存储空间部分。如果正文段是只读的,则父、子进程共享正文段 (见7 . 6节)。
现在很多的实现并不做一个父进程数据段和堆的完全拷贝,因为在 f o r k之后经常跟随着
e x e c。作为替代,使用了在写时复制( C o p y - O n - Write, COW)的技术。这些区域由父、子进程共
享,而且内核将它们的存取许可权改变为只读的。如果有进程试图修改这些区域,则内核为有
关部分,典型的是虚存系统中的“页”,做一个拷贝。B a c h〔1 9 8 6〕的9 . 2节和L e ff l e r等〔1 9 8 9〕 的5 . 7节对这种特征做了更详细的说明。
实例
程序8 - 1例示了f o r k函数。如果执行此程序则得到:
$ a . o u t
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
一般来说,在f o r k之后是父进程先执行还是子进程先执行是不确定的。这取决于内核所使用的
调度算法。如果要求父、子进程之间相互同步,则要求某种形式的进程间通信。在程序 8 - 1中,
父进程使自己睡眠 2秒钟,以此使子进程先执行。但并不保证 2秒钟已经足够,在 8 . 8节说明竟
争条件时,还将谈及这一问题及其他类型的同步方法。在 1 0 . 6节中,在f o r k之后将用信号使父、
1 4 2 U N I X环境高级编程 下载
子进程同步。
注意,程序8 - 1中f o r k与I / O函数之间的关系。回忆第 3章中所述,w r i t e函数是不带缓存的。
因为在f o r k之前调用w r i t e,所以其数据写到标准输出一次。但是,标准 I / O库是带缓存的。回
忆一下5 . 1 2节,如果标准输出连到终端设备,则它是行缓存的,否则它是全缓存的。当以交互
方式运行该程序时,只得到 p r i n t f输出的行一次,其原因是标准输出缓存由新行符刷新。但是
当将标准输出重新定向到一个文件时,却得到 p r i n t f输出行两次。其原因是,在f o r k之前调用了
p r i n t f一次,但当调用f o r k时,该行数据仍在缓存中,然后在父进程数据空间复制到子进程中时,
该缓存数据也被复制到子进程中。于是那时父、子进程各自有了带该行内容的缓存。在 e x i t之
前的第二个p r i n t f将其数据添加到现存的缓存中。当每个进程终止时,其缓存中的内容被写到
相应文件中。
程序8-1 fork函数实例
文件共享
对程序8 - 1需注意的另一点是:在重新定向父进程的标准输出时,子进程的标准输出也被
重新定向。实际上, f o r k的一个特性是所有由父进程打开的描述符都被复制到子进程中。父、
子进程每个相同的打开描述符共享一个文件表项 (见图3 - 3 )。
考虑下述情况,一个进程打开了三个不同文件,它们是:标准输入、标准输出和标准出错。
在从f o r k返回时,我们有了如图8 - 1中所示的安排。
这种共享文件的方式使父、子进程对同一文件使用了一个文件位移量。考虑下述情况:一
个进程f o r k了一个子进程,然后等待子进程终止。假定,作为普通处理的一部分,父、子进程
都向标准输出执行写操作。如果父进程使其标准输出重新定向 (很可能是由s h e l l实现的),那么
子进程写到该标准输出时,它将更新与父进程共享的该文件的位移量。在我们所考虑的例子中,
当父进程等待子进程时,子进程写到标准输出;而在子进程终止后,父进程也写到标准输出上,
第 8章 进 程 控 制 1 4 3 下载
并且知道其输出会添加在子进程所写数据之后。如果父、子进程不共享同一文件位移量,这种
形式的交互就很难实现。
图8-1 fork之后父、子进程之间对打开文件的共享
如果父、子进程写到同一描述符文件,但又没有任何形式的同步(例如使父进程等待子进
程),那么它们的输出就会相互混合(假定所用的描述符是在 f o r k之前打开的)。虽然这种情况
是可能发生的(见程序8 - 1),但这并不是常用的操作方式。
在f o r k之后处理文件描述符有两种常见的情况:
(1) 父进程等待子进程完成。在这种情况下,父进程无需对其描述符做任何处理。当子进
程终止后,它曾进行过读、写操作的任一共享描述符的文件位移量已做了相应更新。
(2) 父、子进程各自执行不同的程序段。在这种情况下,在 f o r k之后,父、子进程各自关闭
它们不需使用的文件描述符,并且不干扰对方使用的文件描述符。这种方法是网络服务进程中
经常使用的。
除了打开文件之外,很多父进程的其他性质也由子进程继承:
• 实际用户I D、实际组I D、有效用户I D、有效组I D。
• 添加组I D。
• 进程组I D。
• 对话期I D。
• 控制终端。
• 设置-用户- I D标志和设置-组- I D标志。
• 当前工作目录。
1 4 4 U N I X环境高级编程 下载
父进程表项
子进程表项
(fd 标志)p t r
(fd 标志)p t r
文件状态标志
当前文件位移
v 节点指针
文件状态标志
当前文件位移
v 节点指针
文件状态标志
当前文件位移
v 节点指针
v 节点信息
i 节点信息
v 节点信息
i 节点信息
v 节点信息
i 节点信息
当前文件长度
当前文件长度
当前文件长度
文件表 v 节点表
• 根目录。
• 文件方式创建屏蔽字。
• 信号屏蔽和排列。
• 对任一打开文件描述符的在执行时关闭标志。
• 环境。
• 连接的共享存储段。
• 资源限制。
父、子进程之间的区别是:
• fork的返回值。
• 进程I D。
• 不同的父进程I D。
• 子进程的t m s _ u t i m e , t m s _ s t i m e , t m s _ c u t i m e以及t m s _ u s t i m e设置为0。
• 父进程设置的锁,子进程不继承。
• 子进程的未决告警被清除。
• 子进程的未决信号集设置为空集。
其中很多特性至今尚末讨论过,我们将在以后几章中对它们进行说明。
使f o r k失败的两个主要原因是:( a )系统中已经有了太多的进程(通常意味着某个方面出了问
题),或者( b )该实际用户I D的进程总数超过了系统限制。回忆表 2 - 7,其中C H I L D _ M A X规定了
每个实际用户I D在任一时刻可具有的最大进程数。
f o r k有两种用法:
(1) 一个父进程希望复制自己,使父、子进程同时执行不同的代码段。这在网络服务进程
中是常见的——父进程等待委托者的服务请求。当这种请求到达时,父进程调用 f o r k,使子进
程处理此请求。父进程则继续等待下一个服务请求。
(2) 一个进程要执行一个不同的程序。这对 s h e l l是常见的情况。在这种情况下,子进程在
从f o r k返回后立即调用e x e c (我们将在8 . 9节说明e x e c )。
某些操作系统将( 2 )中的两个操作( f o r k之后执行e x e c )组合成一个,并称其为s p a w n。U N I X
将这两个操作分开,因为在很多场合需要单独使用 f o r k,其后并不跟随e x e c。另外,将这两个
操作分开,使得子进程在f o r k和e x e c之间可以更改自己的属性。例如I / O重新定向、用户I D、信
号排列等。在第1 4章中有很多这方面的例子。
8.4 vfork函数
v f o r k函数的调用序列和返回值与f o r k相同,但两者的语义不同。
v f o r k起源于较早的4 B S D虚存版本。在L e ffler 等〔1 9 8 9〕的5 . 7节中指出:“虽
然它是特别有效率的,但是v f o r k的语义很奇特,通常认为它具有结构上的缺陷。”
尽管如此S V R 4和4 . 3 + B S D仍支持v f o r k。
某些系统具有头文件< v f o r k . h >,当调用v f o r k时,应当包括该头文件。
v f o r k用于创建一个新进程,而该新进程的目的是 e x e c一个新程序(如上节(2) 中一样)。程
序1 - 5中的s h e l l基本部分就是这种类型程序的一个例子。 v f o r k与f o r k一样都创建一个子进程,
但是它并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用 e x e c (或e x i t ),于
第 8章 进 程 控 制 1 4 5 下载
是也就不会存访该地址空间。不过在子进程调用 e x e c或e x i t之前,它在父进程的空间中运行。
这种工作方式在某些 U N I X的页式虚存实现中提高了效率(与上节中提及的,在 f o r k之后跟随
e x e c,并采用在写时复制技术相类似)。
v f o r k和f o r k之间的另一个区别是:v f o r k保证子进程先运行,在它调用 e x e c或e x i t之后父进
程才可能被调度运行。(如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会
导致死锁。)
实例
在程序8 - 1中使用v f o r k代替f o r k,并做其他相应修改得到程序8 - 2。
程序8-2 vfork 函数实例
运行该程序得到:
$ a . o u t
before vfork
pid = 607, glob = 7, var = 89
子进程对变量g l o b和v a r做增1操作,结果改变了父进程中的变量值。因为子进程在父进程的地
址空间中运行,所以这并不令人惊讶。但是其作用的确与 f o r k不同。
注意,在程序8 - 2中,调用了_ e x i t而不是e x i t。正如8 . 5节所述,_ e x i t并不执行标准I / O缓存
的刷新操作。如果用e x i t而不是_ e x i t,则该程序的输出是:
$ a . o u t
before vfork
从中可见,父进程p r i n t f的输出消失了。其原因是子进程调用了 e x i t,它刷新开关闭了所有标准
I / O流,这包括标准输出。虽然这是由子进程执行的,但却是在父进程的地址空间中进行的,
所以所有受到影响的标准I/O FILE对象都是在父进程中的。当父进程调用p r i n t f时,标准输出已
1 4 6 U N I X环境高级编程 下载
被关闭了,于是p r i n t f返回- 1。
L e ffler 等 [1989] 在5 . 7节中包含了f o r k和v f o r k实现方面的更多信息。习题8 . 1和8 . 2则继续了
对v f o r k的讨论。
8.5 exit函数
如同7 . 3节所述,进程有三种正常终止法及两种异常终止法。
(1) 正常终止:
(a) 在m a i n函数内执行r e t u r n语句。如在7 . 3节中所述,这等效于调用e x i t。
(b) 调用e x i t函数。此函数由ANSI C定义,其操作包括调用各终止处理程序(终止处理程序
在调用a t e x i t函数时登录),然后关闭所有标准I / O流等。因为ANSI C并不处理文件描述符、
多进程(父、子进程)以及作业控制,所以这一定义对U N I X系统而言是不完整的。
© 调用_ e x i t系统调用函数。此函数由 e x i t调用,它处理U N I X特定的细节。 _ e x i t是由
P O S I X . 1说明的。
(2) 异常终止:
(a) 调用a b o r t。它产生S I G A B RT信号,所以是下一种异常终止的一种特例。
(b) 当进程接收到某个信号时。(第1 0章将较详细地说明信号。)进程本身(例如调用
a b o r t函数)、其他进程和内核都能产生传送到某一进程的信号。例如,进程越出其
地址空间访问存储单元,或者除以0,内核就会为该进程产生相应的信号。
不管进程如何终止,最后都会执行内核中的同一段代码。这段代码为相应进程关闭所有打
开描述符,释放它所使用的存储器等等。
对上述任意一种终止情形,我们都希望终止进程能够通知其父进程它是如何终止的。对
于e x i t和_ e x i t,这是依靠传递给它们的退出状态( exit status)参数来实现的。在异常终止情
况,内核(不是进程本身)产生一个指示其异常终止原因的终止状态( termination status)。
在任意一种情况下,该终止进程的父进程都能用 w a i t或w a i t p i d函数(在下一节说明)取得其终止
状态。
注意,这里使用了“退出状态”(它是传向e x i t或_ e x i t的参数,或m a i n的返回值)和“终止
状态”两个术语,以表示有所区别。在最后调用 _ e x i t时内核将其退出状态转换成终止状态(回
忆图7 - 1)。下一节中的表 8 - 1说明了父进程检查子进程的终止状态的不同方法。如果子进程正
常终止,则父进程可以获得子进程的退出状态。
在说明f o r k函数时,一定是一个父进程生成一个子进程。上面又说明了子进程将其终止状
态返回给父进程。但是如果父进程在子进程之前终止,则将如何呢 ?其回答是对于其父进程已
经终止的所有进程,它们的父进程都改变为 i n i t进程。我们称这些进程由i n i t进程领养。其操作
过程大致是:在一个进程终止时,内核逐个检查所有活动进程,以判断它是否是正要终止的进
程的子进程,如果是,则该进程的父进程 I D就更改为1 ( i n i t进程的I D )。这种处理方法保证了每
个进程有一个父进程。
另一个我们关心的情况是如果子进程在父进程之前终止,那么父进程又如何能在做相应检
查时得到子进程的终止状态呢?对此问题的回答是内核为每个终止子进程保存了一定量的信
息,所以当终止进程的父进程调用 w a i t或waitpid 时,可以得到有关信息。这种信息至少包括
进程I D、该进程的终止状态、以反该进程使用的 C P U时间总量。内核可以释放终止进程所使
用的所有存储器,关闭其所有打开文件。在 U N I X术语中,一个已经终止、但是其父进程尚未
对其进行善后处理(获取终止子进程的有关信息、释放它仍占用的资源)的进程被称为僵死
第 8章 进 程 控 制 1 4 7 下载
进程(z o m b i e)。p s ( 1 )命令将僵死进程的状态打印为 Z。如果编写一个长期运行的程序,它
f o r k了很多子进程,那么除非父进程等待取得子进程的终止状态,否则这些子进程就会变成僵
死进程。
系统V提供了一种避免僵死进程的非标准化方法,这将在 1 0 . 7中介绍。
最后一个要考虑的问题是:一个由 i n i t进程领养的进程终止时会发生什么 ?它会不会变成一
个僵死进程?对此问题的回答是“否”,因为i n i t被编写成只要有一个子进程终止, i n i t就会调
用一个w a i t函数取得其终止状态。这样也就防止了在系统中有很多僵死进程。当提及“一个 i n i t
的子进程”时,这指的是 i n i t直接产生的进程(例如,将在9 . 2节说明的g e t t y进程),或者是其父
进程已终止,由init 领养的进程。
8.6 wait和w a i t p i d函数
当一个进程正常或异常终止时,内核就向其父进程发送 S I G C H L D信号。因为子进程终止
是个异步事件(这可以在父进程运行的任何时候发生 ),所以这种信号也是内核向父进程发的异
步通知。父进程可以忽略该信号,或者提供一个该信号发生时即被调用执行的函数 (信号处理
程序)。对于这种信号的系统默认动作是忽略它。第 1 0章将说明这些选择项。现在需要知道的
是调用w a i t或w a i t p i d的进程可能会:
• 阻塞(如果其所有子进程都还在运行)。
• 带子进程的终止状态立即返回(如果一个子进程已终止,正等待父进程存取其终止状态 )。
• 出错立即返回(如果它没有任何子进程)。
如果进程由于接收到S I G C H L D信号而调用w a i t,则可期望w a i t会立即返回。但是如果在一
个任一时刻调用w a i t,则进程可能会阻塞。
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *s t a t l o c) ;
pid_t waitpid(pid_t p i d, int *s t a t l o c, int o p t i o n s) ;
两个函数返回:若成功则为进程 I D,若出错则为-1
这两个函数的区别是:
• 在一个子进程终止前, wait 使其调用者阻塞,而 waitpid 有一选择项,可使调用者不阻
塞。
• waitpid并不等待第一个终止的子进程—它有若干个选择项,可以控制它所等待的进程。
如果一个子进程已经终止,是一个僵死进程,则 w a i t立即返回并取得该子进程的状态,否
则w a i t使其调用者阻塞直到一个子进程终止。如调用者阻塞而且它有多个子进程,则在其一个
子进程终止时,w a i t就立即返回。因为w a i t返回终止子进程的进程I D,所以它总能了解是哪一
个子进程终止了。
这两个函数的参数s t a t l o c是一个整型指针。如果 s t a t l o c不是一个空指针,则终止进程的终
止状态就存放在它所指向的单元内。如果不关心终止状态,则可将该参数指定为空指针。
依据传统,这两个函数返回的整型状态字是由实现定义的。其中某些位表示退出状态(正
1 4 8 U N I X环境高级编程 下载
常返回),其他位则指示信号编号(异常返回),有一位指示是否产生了一个 c o r e文件等等。
P O S I X . 1规定终止状态用定义在< s y s / w a i t . h >中的各个宏来查看。有三个互斥的宏可用来取得进
程终止的原因,它们的名字都以 W I F开始。基于这三个宏中哪一个值是真,就可选用其他宏来
取得终止状态、信号编号等。这些都在表 8 - 1中给出。在8 . 9节中讨论作业控制时,将说明如何
停止一个进程。
表8-1 检查w a i t和w a i t p i d所返回的终止状态的宏
宏 说 明
W I F E X I T E D(s t a t u s) 若为正常终止子进程返回的状态,则为真。对于这种情况可执行
W E X I T S T A T U S(s t a t u s)
取子进程传送给e x i t或_ e x i t参数的低8位
W I F S I G N A L E D(s t a t u s) 若为异常终止子进程返回的状态,则为真(接到一个不捕捉的信号)。对于
这种情况,可执行
W T E R M S I G(s t a t u s)
取使子进程终止的信号编号。
另外,S V R 4和4 . 3 + B S D(但是,非P O S I X . 1)定义宏:
W C O R E D U M P(s t a t u s)
若已产生终止进程的c o r e文件,则它返回真
W I F S T O P P E D(s t a t u s) 若为当前暂停子进程的返回的状态,则为真。对于这种情况,可执行
W S T O P S I G(s t a t u s)
取使子进程暂停的信号编号
实例
程序8 - 3中的函数p r _ e x i t使用表8 - 1中的宏以打印进程的终止状态。本章的很多程序都将调
用此函数。注意,如果定义了W C O R E D U M P,则此函数也处理该宏。
程序8 - 4调用p r _ e x i t函数,例示终止状态的不同值。运行程序8 - 4可得:
$ a . o u t
normal termination, exit status = 7
abnormal termination, signal number = 6 (core file generated)
abnormal termination, signal number = 8 (core file generated)
程序8-3 打印e x i t状态的说明
第 8章 进 程 控 制 1 4 9 下载
程序8-4 例示不同的e x i t值
不幸的是,没有一种可移植的方法将 W T E R M S I G得到的信号编号映射为说明性的名字。
( 1 0 . 2 1节中说明了一种方法。 )我们必须查看 < s i g n a l . h >头文件才能知道 S I G A B RT的值是6,
S I G F P E的值是8。
正如前面所述,如果一个进程有几个子进程,那么只要有一个子进程终止, w a i t就返回。
如果要等待一个指定的进程终止 (如果知道要等待进程的I D ),那么该如何做呢?在早期的U N I X
版本中,必须调用w a i t,然后将其返回的进程I D和所期望的进程I D相比较。如果终止进程不是
所期望的,则将该进程I D和终止状态保存起来,然后再次调用 w a i t。反复这样做直到所期望的
进程终止。下一次又想等待一个特定进程时,先查看已终止的进程表,若其中已有要等待的进
程,则取有关信息,否则调用 w a i t。其实,我们需要的是等待一个特定进程的函数。 P O S I X . 1
1 5 0 U N I X环境高级编程 下载
定义了w a i t p i d函数以提供这种功能(以及其他一些功能)。
w a i t p i d函数是新由P O S I X . 1定义的。S V R 4和4 . 3 + B S D都提供此函数,但早期
的系统V和4 . 3 B S D并不提供此函数。
对于w a i t p i d的p i d参数的解释与其值有关:
• pid == -1 等待任一子进程。于是在这一功能方面w a i t p i d与w a i t等效。
• pid > 0 等待其进程I D与p i d相等的子进程。
• pid == 0 等待其组I D等于调用进程的组I D的任一子进程。
• pid < -1 等待其组I D等于p i d的绝对值的任一子进程。
( 9 . 4节将说明进程组。) w a i t p i d返回终止子进程的进程I D,而该子进程的终止状态则通过 s t a t l o c
返回。对于w a i t,其唯一的出错是调用进程没有子进程(函数调用被一个信号中断时,也可能返
回另一种出错。第1 0章将对此进行讨论)。但是对于w a i t p i d,如果指定的进程或进程组不存在,
或者调用进程没有子进程都能出错。
o p t i o n s参数使我们能进一步控制w a i t p i d的操作。此参数或者是0,或者是表8 - 2中常数的逐
位或运算。
表8-2 waitpid的选择项常数
常 数 说 明
W N O H A N G 若若由p i d指定的子进程并不立即可用,则 w a i t p i d不阻塞,此时其返回值为 0
W U N T R A C E D 若若某实现支持作业控制,则由 p i d指定的任一子进程状态已暂停,且其状态自暂
停以来还未报告过,则返回其状态。 W I F S TO P P E D宏确定返回值是否对应于一个暂
停子进程
S V R 4支持两个附加的非标准的o p t i o n s常数。W N O WA I T使系统将其终止状态
已由 w a i t p i d返回的进程保持在等待状态,于是该进程就可被再次等待。对于
W C O N T I N U E D,返回由p i d指定的某一子进程的状态,该子进程已被继续,其状
态尚未报告过。
w a i t p i d函数提供了w a i t函数没有提供的三个功能:
(1) waitpid等待一个特定的进程(而w a i t则返回任一终止子进程的状态 )。在讨论p o p e n函数
时会再说明这一功能。
(2) waitpid提供了一个w a i t的非阻塞版本。有时希望取得一个子进程的状态,但不想阻塞。
(3) waitpid支持作业控制(以W U N T R A C E D选择项)。
实例
回忆一下8 . 5节中有关僵死进程的讨论。如果一个进程要 f o r k一个子进程,但不要求它等待
子进程终止,也不希望子进程处于僵死状态直到父进程终止,实现这一要求的诀窍是调用 f o r k
两次。程序8 - 5实现了这一点。
在第二个子进程中调用 s l e e p以保证在打印父进程 I D时第一个子进程已终止。在 f o r k之后,
父、子进程都可继续执行——我们无法预知哪一个会先执行。如果不使第二个子进程睡眠,则
在f o r k之后,它可能比其父进程先执行,于是它打印的父进程 I D将是创建它的父进程,而不是
第 8章 进 程 控 制 1 5 1 下载
i n i t进程(进程ID 1)。
程序8-5 fork两次以避免僵死进程
执行程序8 - 5得到:
$ a . o u t
$ second child, parent pid = 1
注意,当原先的进程(也就是e x e c本程序的进程)终止时,s h e l l打印其指示符,这在第二个子进
程打印其父进程I D之前。
8.7 wait3和w a i t 4函数
4 . 3 + B S D提供了两个附加函数w a i t 3和w a i t 4。这两个函数提供的功能比P O S I X . 1函数w a i t和
w a i t p i d所提供的分别要多一个,这与附加参数 ru s a g e有关。该参数要求内核返回由终止进程及
其所有子进程使用的资源摘要。
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <sys/resource.h>
pid_t wait3(int *s t a t l o c, int o p t i o n s, struct rusage *ru s a g e) ;
1 5 2 U N I X环境高级编程 下载
pid_t wait4(pid_t p i d, int *s t a t l o c, int o p t i o n s, struct rusage ru s a g e) ;
两个函数返回:若成功则为进程 I D,若出错则为-1
S V R 4在其B S D兼容库中也提供了w a i t 3函数。
资源信息包括用户 C P U时间总量、系统C P U时间总量、缺页次数、接收到信号的次数等。
有关细节请参阅g e t r u s a g e ( 2 )手册页。这些资源信息只包括终止子进程,并不包括处于停止状态
的子进程(这种资源信息与 7 . 11节中所述的资源限制不同)。表8 - 3中列出了各个w a i t函数所支
持的不同的参数。
表8-3 不同系统上各个w a i t函数所支持的参数
函 数 p i d o p t i o n s ru s a g e P O S I X . 1 S V R 4 4 . 3 + B S D
w a i t • • •
w a i t p i d • • • • •
w a i t 3 • • • •
w a i t 4 • • • •
8.8 竞态条件
从本书的目的出发,当多个进程都企图对共享数据进行某种处理,而最后的结果又取决于
进程运行的顺序时,则我们认为这发生了竞态条件( race condition)。如果在f o r k之后的某种逻
辑显式或隐式地依赖于在 f o r k之后是父进程先运行还是子进程先运行,那么 f o r k函数就会是竞
态条件活跃的孳生地。通常,我们不能预料哪一个进程先运行。即使知道哪一个进程先运行,
那么在该进程开始运行后,所发生的事情也依赖于系统负载以及内核的调度算法。
在程序8 - 5中,当第二个子进程打印其父进程 I D时,我们看到了一个潜在的竞态条件。如
果第二个子进程在第一个子进程之前运行,则其父进程将会是第一个子进程。但是,如果第一
个子进程先运行,并有足够的时间到达并执行 e x i t,则第二个子进程的父进程就是i n i t。即使在
程序中调用s l e e p,这也不保证什么。如果系统负担很重,那么在第二个子进程从 s l e e p返回时,
可能第一个子进程还没有得到机会运行。这种形式的问题很难排除,因为在大部分时间,这种
问题并不出现。
如果一个进程希望等待一个子进程终止,则它必须调用 w a i t函数。如果一个进程要等待其
父进程终止(如程序8 - 5中一样),则可使用下列形式的循环:
while(getppid() !=1)
s l e e p ( 1 ) ;
这种形式的循环(称为定期询问( p o l l i n g))的问题是它浪费了C P U时间,因为调用者每隔1秒
都被唤醒,然后进行条件测试。
为了避免竞态条件和定期询问,在多个进程之间需要有某种形式的信号机制。在 U N I X中
可以使用信号机制,在 1 0 . 1 6节将说明它的一种用法。各种形式的进程间通信 ( I P C )也可使用,
在第1 4、1 5章将对此进行讨论。
在父、子进程的关系中,常常出现下述情况。在 f o r k之后,父、子进程都有一些事情要做。
第 8章 进 程 控 制 1 5 3 下载
例如,父进程可能以子进程I D更新日志文件中的一个记录,而子进程则可能要为父进程创建一
个文件。在本例中,要求每个进程在执行完它的一套初始化操作后要通知对方,并且在继续运
行之前,要等待另一方完成其初始化操作。这种情况可以描述如下:
假定在头文件o u r h d r. h中定义了各个需要使用的变量。五个例程T E L L _ WA I T、TELL_ PA R E N T、
T E L L _ C H I L D、WA I T _ PA R E N T以及WA I T _ C H I L D可以是宏,也可以是函数。
在后面的一些章中会说明实现这些 T E L L和WA I T例程的不同方法:1 0 . 1 6节中说明用信号
的一种实现,程序1 4 - 3中说明用流管道的一种实现。下面先看一个使用这五个例程的实例。
实例
程序8 - 6输出两个字符串:一个由子进程输出,一个由父进程输出。因为输出依赖于内核
使进程运行的顺序及每个进程运行的时间长度,所以该程序包含了一个竞态条件。
程序8-6 具有竞态条件的程序
1 5 4 U N I X环境高级编程 下载
在程序中将标准输出设置为不带缓存的,于是每个字符输出都需调用一次 w r i t e。本例的
目的是使内核能尽可能多次地在两个进程之间进行切换,以例示竞态条件。 (如果不这样做,
可能也就决不会见到下面所示的输出。没有看到具有错误的输出并不意味着竞态条件不存在,
这只是意味着在此特定的系统上未能见到它。 )下面的实际输出说明该程序的运行结果是会改
变的。
$ a . o u t
output from child
output from parent
$ a . o u t
oouuttppuutt ffrroomm cphairledn
t
$ a . o u t
oouuttppuutt ffrroomm pcahrielndt
$ a . o u t
ooutput from parent
utput from child
修改程序8 - 6,使其使用T E L L和WA I T函数,于是形成了程序 8 - 7。行首标以+号的行是新
增加的行。
程序8-7 修改程序8 - 6以避免竞态条件
第 8章 进 程 控 制 1 5 5 下载
运行此程序则能得到所预期的输出——两个进程的输出不再交叉混合。
程序8 - 7是使父进程先运行。如果将f o r k之后的行改变成:
else if (pid == 0) {
charatatime(“output from child\n”);
T E L L _ P A R E N T ( g e t p p i d ( ) ) ;
} else {
WAIT_CHILD(); /
child goes first */
charatatime(“output from parent\n”);
}
则子进程先运行。习题8 . 3继续这一实例。
8.9 exec函数
8 . 3节曾提及用f o r k函数创建子进程后,子进程往往要调用一种e x e c函数以执行另一个程序。
当进程调用一种e x e c函数时,该进程完全由新程序代换,而新程序则从其 m a i n函数开始执行。
因为调用e x e c并不创建新进程,所以前后的进程 I D并未改变。e x e c只是用另一个新程序替换了
当前进程的正文、数据、堆和栈段。
有六种不同的e x e c函数可供使用,它们常常被统称为e x e c函数。这些e x e c函数都是U N I X进
程控制原语。用f o r k可以创建新进程,用e x e c可以执行新的程序。e x i t函数和两个w a i t函数处理
终止和等待终止。这些是我们需要的基本的进程控制原语。在后面各节中将使用这些原语构造
另外一些如p o p e n和s y s t e m之类的函数。
#include <unistd.h>
int execl(const char * p a t h n a m e, const char a rg 0, … / (char *) 0 */);
int execv(const char * p a t h n a m e, char *const a rgv [] );
int execle(const char * p a t h n a m e, const char a rg 0, …
/
(char *)0, char *const e n v p [] */);
int execve(const char * p a t h n a m e, char *const a rgv [], char *const envp [] );
int execlp(const char * f i l e n a m e, const char a rg 0, … / (char *) 0 */);
int execvp(const char * f i l e n a m e, char *const a rgv [] );
六个函数返回:若出错则为- 1,若成功则不返回
这些函数之间的第一个区别是前四个取路径名作为参数,后两个则取文件名作为参数。当
指定f i l e n a m e作为参数时:
1 5 6 U N I X环境高级编程 下载
• 如果f i l e n a m e中包含/,则就将其视为路径名。
• 否则就按PAT H环境变量,在有关目录中搜寻可执行文件。
PAT H变量包含了一张目录表 (称为路径前缀),目录之间用冒号 ( : )分隔。例如下列n a m e = v a l u e
环境字符串:
P A T H = / b i n : / u s r / b i n : / u s r / l o c a l / b i n :.
指定在四个目录中进行搜索。(零长前缀也表示当前目录。在 v a l u e的开始处可用:表示,在行
中间则要用::表示,在行尾以:表示。)
有很多出于安全性方面的考虑,要求在搜索路径中决不要包括当前目录。请
参见Garfinkel 和S p a fford [1991] 。
如果e x e c l p和e x e c v p中的任意一个使用路径前缀中的一个找到了一个可执行文件,但是该
文件不是由连接编辑程序产生的机器可执行代码文件,则就认为该文件是一个 s h e l l脚本,于是
试着调用/ b i n / s h,并以该f i l e n a m e作为s h e l l的输入。
第二个区别与参数表的传递有关 ( l表示表( l i s t ),v表示矢量( v e c t o r ) )。函数e x e c l、e x e c l p和
e x e c l e要求将新程序的每个命令行参数都说明为一个单独的参数。这种参数表以空指针结尾。
对于另外三个函数( e x e c v, e x e c v p和e x e c v e ),则应先构造一个指向各参数的指针数组,然后将该
数组地址作为这三个函数的参数。
在使用ANSI C原型之前,对e x e c l , e x e c l e和e x e c l p三个函数表示命令行参数的一般方法是:
char * a rg 0, char *a rg 1, …, char *a rg n, (char *) 0
应当特别指出的是:在最后一个命令行参数之后跟了一个空指针。如果用常数 0来表示一个空
指针,则必须将它强制转换为一个字符指针,否则它将被解释为整型参数。如果一个整型数的
长度与char *的长度不同,e x e c函数实际参数就将出错。
最后一个区别与向新程序传递环境表相关。以 e结尾的两个函数( e x e c l e和e x e c v e)
可以传递一个指向环境字符串指针数组的指针。其他四个函数则使用调用进程中的
e n v i r o n变量为新程序复制现存的环境。(回忆 7 . 9节及表 7 - 2中对环境字符串的讨论。其中
曾提及如果系统支持 s e t e n v和p u t e n v这样的函数,则可更改当前环境和后面生成的子进
程的环境,但不能影响父进程的环境。)通常,一个进程允许将其环境传播给其子进程,
但有时也有这种情况,进程想要为子进程指定一个确定的环境。例如,在初始化一个新
登录的 s h e l l时, l o g i n程序创建一个只定义少数几个变量的特殊环境,而在我们登录时,
可以通过 s h e l l起动文件,将其他变量加到环境中。在使用 ANSI C 原型之前, execle 的参
数是:
char * p a t h n a m e, char *a rg 0, …, char a rg n, (char )0, char * e n v p[ ]
从中可见,最后一个参数是指向环境字符串的各字符指针构成的数组的指针。而在 ANSI C原
型中,所有命令行参数,包括空指针,e n v p指针都用省略号(…)表示。
这六个e x e c函数的参数很难记忆。函数名中的字符会给我们一些帮助。字母 p表示该函数
取f i l e n a m e作为参数,并且用PAT H环境变量寻找可执行文件。字母 l表示该函数取一个参数表,
它与字母v互斥。v表示该函数取一个a rg v[ ]。最后,字母e表示该函数取e n v p[ ] 数组,而不使
用当前环境。表8 - 4显示了这六个函数之间的区别。
第 8章 进 程 控 制 1 5 7 下载
表8-4 六个e x e c函数之间的区别
函 数 p a t h n a m e f i l e n a m e 参 数 表 a rg v[ ] e n v i r o n e n v p[ ]
e x e c l • • •
e x e c l p • • •
e x e c l e • • •
e x e c v • • •
e x e c v p • • •
e x e c v e • • •
(字母表示) p l v e
每个系统对参数表和环境表的总长度都有一个限制。在表 2 - 7中,这种限制是A R G _ M A X。 在P O S I X . 1系统中,此值至少是4 0 9 6字节。当使用s h e l l的文件名扩充功能产生一个文件名表时,
可能会受到此值的限制。例如,命令
grep _POSIX_SOURCE /usr/include/
/
.h
在某些系统上可能产生下列形式的s h e l l错误:
arg list too long
由于历史原因,系统 V中此限制是5 1 2 0字节。4 . 3 B S D和4 . 3 + B S D在分发时此
限制是2 0 4 8 0字节。作者所用的系统则允许多至1 M字节(见程序2 - 1的输出)!
前面曾提及在执行e x e c后,进程I D没有改变。除此之外,执行新程序的进程还保持了原进
程的下列特征:
• 进程I D和父进程I D。
• 实际用户I D和实际组I D。
• 添加组I D。
• 进程组I D。
• 对话期I D。
• 控制终端。
• 闹钟尚余留的时间。
• 当前工作目录。
• 根目录。
• 文件方式创建屏蔽字。
• 文件锁。
• 进程信号屏蔽。
• 未决信号。
• 资源限制。
• tms_utime, tms_stime, tms_cutime以及t m s _ u s t i m e值。
对打开文件的处理与每个描述符的 e x e c关闭标志值有关。见图 3 - 1以及 3 . 1 3节中对
F D _ C L O E X E C的说明,进程中每个打开描述符都有一个 e x e c关闭标志。若此标志设置,则在
执行e x e c时关闭该描述符,否则该描述符仍打开。除非特地用 f c n t l设置了该标志,否则系统的
默认操作是在e x e c后仍保持这种描述符打开。
1 5 8 U N I X环境高级编程 下载
P O S I X . 1明确要求在e x e c时关闭打开目录流(见4 . 2 1节中所述的o p e n d i r函数)。这通常是由
o p e n d i r函数实现的,它调用f c n t l函数为对应于打开目录流的描述符设置e x e c关闭标志。
注意,在e x e c前后实际用户I D和实际组I D保持不变,而有效I D是否改变则取决于所执行程
序的文件的设置 -用户- I D位和设置-组- I D位是否设置。如果新程序的设置 -用户- I D位已设置,
则有效用户I D变成程序文件所有者的I D,否则有效用户I D不变。对组I D的处理方式与此相同。
在很多U N I X实现中,这六个函数中只有一个e x e c v e是内核的系统调用。另外五个只是库函
数,它们最终都要调用系统调用。这六个函数之间的关系示于图8 - 2中。在这种安排中,库函数
execlp 和execvp 使用PAT H环境变量查找第一个包含名为f i l e n a m e的可执行文件的路径名前缀。
图8-2 六个e x e c函数之间的关系
实例
程序8 - 8例示了e x e c函数。
程序8-8 exec函数实例
第 8章 进 程 控 制 1 5 9 下载
构造a rg v
试每一个
P A T H前缀
使用
e n v i r o n
构造a rg v 构造a rg v
e x e c v e(系统调用)
在该程序中先调用e x e c l e,它要求一个路径名和一个特定的环境。下一个调用的是 e x e c l p,
它用一个文件名,并将调用者的环境传送给新程序。 e x e c l p在这里能够工作的原因是因为目录
/ h o m e / s t e v e n s / b i n是当前路径前缀之一。注意,我们将第一个参数(新程序中的 a rgv [0])设置
为路径名的文件名分量。某些s h e l l将此参数设置为完全的路径名。
在程序8 - 8中要执行两次的程序e c h o a l l示于程序8 - 9中。这是一个普通程序,它回送其所有
命令行参数及其全部环境表。
程序8-9 回送所有命令行参数和所有环境字符串
执行程序8 - 8时得到:
$ a . o u t
argv[0]: echoall
argv[1]: myarg1
argv[2]: MY ARG2
U S E R = u n k n o w n
P A T H = / t m p
argv[0]: echoall
$ argv[1]: only 1 arg
U S E R = s t e v e n s
H O M E = / h o m e / s t e v e n s
L O G N A M E = s t e v e n s
其中3 1行没有显示
E D I T O R = / u s r / u c b / v i
注意,s h e l l提示出现在第二个e x e c打印a rg v [ 0 ]和a rg v [ 1 ]之间。这是因为父进程并不等待该子进
程结束。
8.10 更改用户I D和组I D
可以用s e t u i d函数设置实际用户I D和有效用户I D。与此类似,可以用s e t g i d函数设置实际组
I D和有效组I D。
#include <sys/types.h>
#include <unistd.h>
int setuid(uid_t u i d) ;
1 6 0 U N I X环境高级编程 下载
int setgid(gid_t g i d) ;
两个函数返回:若成功则为0,若出错则为-1
关于谁能更改I D有若干规则。现在先考虑有关改变用户 I D的规则(在这里关于用户 I D所说明
的一切都适用于组I D)。
(1) 若进程具有超级用户特权,则 s e t u i d函数将实际用户 I D、有效用户I D,以及保存的设
置-用户-I D设置为u i d。
(2) 若进程没有超级用户特权,但是u i d等于实际用户I D或保存的设置-用户- I D,则s e t u i d只
将有效用户I D设置为u i d。不改变实际用户I D和保存的设置-用户- I D。
(3) 如果上面两个条件都不满足,则e r r n o设置为E P E R M,并返回出错。
在这里假定_ P O S I X _ S AV E D _ I D S为真。如果没有提供这种功能,则上面所说的关于保存
的设置-用户-I D部分都无效。
FIPS 151-1要求此功能。
S V R 4支持_ P O S I X _ S AV E D _ I D S功能。
关于内核所维护的三个用户I D,还要注意下列几点:
(1) 只有超级用户进程可以更改实际用户 I D。通常,实际用户 I D是在用户登录时,由
l o g i n ( 1 )程序设置的,而且决不会改变它。因为 l o g i n是一个超级用户进程,当它调用 s e t u i d时,
设置所有三个用户I D。
(2) 仅当对程序文件设置了设置-用户- I D位时,e x e c函数设置有效用户 I D。如果设置-用 户- I D位没有设置,则e x e c函数不会改变有效用户 I D,而将其维持为原先值。任何时候都可以
调用s e t u i d,将有效用户I D设置为实际用户I D或保存的设置-用户- I D。自然,不能将有效用户
I D设置为任一随机值。
(3) 保存的设置-用户- I D是由e x e c从有效用户I D复制的。在e x e c按文件用户I D设置了有效用
户I D后,即进行这种复制,并将此副本保存起来。
表8 - 5列出了改变这三个用户I D的不同方法。
表8-5 改变三个用户I D的不同方法
I D
e x e c s e t u i d(u i d)
设置-用户-I D位关闭 设置-用户-I D位打开 超级用户 非特权用户
实际用户I D 不变 不变 设为u i d 不变
有效用户I D 不变 设置为程序文件的用户I D 设为u i d 设为u i d
保存的设置-用户-I D 从有效用户I D复制 从有效用户I D复制 设为u i d 不变
注意,用8 . 2节中所述的g e t u i d和g e t e u i d函数只能获得实际用户 I D和有效用户I D的当前值。
我们不能获得所保存的设置-用户- I D的当前值。
实例
为了说明保存的设置-用户- I D特征的用法,先观察一个使用该特征的程序。我们所观察的
是伯克利t i p ( 1 )程序(系统V的c u ( 1 )程序与此类似)。这两个程序都连接到一个远程系统,或者是
直接连接,或者是拨号一个调制解调器。当 t i p使用调制解调器时,它必须通过使用锁文件来独
第 8章 进 程 控 制 1 6 1 下载
占使用它。此锁文件与U U C P程序共享,因为这两个程序可能要同时使用同一调制解调器。对
其工作步骤说明如下:
(1) tip程序文件是由用户u u c p拥有的,并且其设置-用户- I D位已设置。当e x e c此程序时,则
关于用户I D得到下列结果:
实际用户I D=我们的用户I D
有效用户I D=u u c p
保存设置-用户- I D=u u c p
(2) tip存取所要求的锁文件。这些锁文件是由名为 u u c p的用户所拥有的,因为有效用户 I D
是u u c p,所以t i p可以存取这些锁文件。
(3) tip执行s e t u i d ( g e t u i d ( ) )。因为t i p不是超级用户进程,所以这仅仅改变有效用户 I D。此时
得到:
实际用户I D=我们的用户I D (未改变)
有效用户- I D=我们的用户I D (未改变)
保存设置-用户- I D=u u c p (未改变)
现在,t i p进程是以我们的用户 I D作为其有效用户 I D而运行的。这就意味着能存取的只有
我们通常可以存取的,没有额外的许可权。
(4) 当执行完所需的操作后, t i p执行s e t u i d(u u c p u i d),其中u u c p u i d是用户u u c p的数值
用户I D(t i p很可能在起动时调用 g e t e u i d,得到u u c p的用户I D,然后将其保存起来,我们并
不认为t i p会搜索口令文件以得到这一数值用户 I D)。因为s e t u i d的参数等于保存的设置 -用户-
I D,所以这种调用是许可的(这就是为什么需要保存的设置 -用户- I D的原因)。现在得到:
实际用户I D =我们的用户I D(未改变)
有效用户I D = u u c p
保存设置-用户- I D = u u c p(未改变)
(5) tip现在可对其锁文件进行操作以释放它们,因为 t i p的有效用户I D是u u c p。
以这种方法使用保存的设置 -用户- I D,在进程的开始和结束部分就可以使用由于程序文件的设
置用户I D而得到的额外优先权。但是,进程在其运行的大部分时间只具有普通的许可权。如果
进程不能在其结束部分切换回保存的设置 -用户- I D,那么就不得不在全部运行时间都保持额外
的许可权(这可能会造成麻烦)。
下面来看一看如果在 t i p运行时为我们生成一个 s h e l l进程(先f o r k,然后e x e c )将发生什么。
因为实际用户I D和有效用户I D都是我们的普通用户I D (上面的第( 3 )步),所以该s h e l l没有额外的
许可权。它不能存取t i p运行时设置成u u c p的保存的设置-用户- I D,因为该s h e l l所保存的设置-用 户- I D是由e x e c复制有效用户I D而得到的。所以在执行 e x e c的子进程中,所有三个用户 I D都是
我们的普通用户I D。
如果程序是设置-用户- I D为r o o t ,那么我们关于t i p如何使用s e t u i d所做的说明是不正确的。因
为以超级用户特权调用s e t u i d就会设置所有三个用户I D。使上述实例按我们所说明的进行工作,
只需s e t u i d设置有效用户I D。
8.10.1 setreuid和s e t r e g i d函数
4 . 3 + B S D支持s e t r e g i d函数,其功能是交换实际用户I D和有效用户I D的值。
#include <sys/types.h>
1 6 2 U N I X环境高级编程 下载
#include <unistd.h>
int setreuid(uid_t ru i d, uid_t e u i d) ;
int setregid(gid_t rg i d, gid_t e g i d) ;
两个函数返回:若成功则为0,若出错则为-1
其作用很简单:一个非特权用户总能交换实际用户 I D和有效用户I D。这就允许一个设置-用户-
I D程序转换成只具有用户的普通许可权,以后又可再次转换回设置 -用户- I D所得到的额外许可
权。P O S I X . 1引进了保存的设置-用户- I D特征后,其作用也相应加强,它也允许一个非特权用
户将其有效用户I D设置为保存的设置-用户- I D。
S V R 4在其B S D兼容库中也提供这两个函数。
4 . 3 B S D并没有上面所说的保存的设置 -用户-I D功能。它用s e t r e u i d和s e t r e g i d
来代替。这就允许一个非特权用户前、后交换这两个用户 I D的值,而4 . 3 B S D中 的t i p程序就是用这种功能编写的。但是要知道,当此版本生成 s h e l l进程时,它必
须在e x e c之前,先将实际用户I D设置为普通用户I D。如果不这样做的话,那么实
际用户I D就可能是u u c p(由s e t r e u i d的交换操作造成。),然后s h e l l进程可能会调
用s e t r e u i d交换两个用户 I D值并取得u u c p许可权。作为一个保护性的程序设计措
施,t i p将子进程的实际用户I D和有效用户I D都设置成普通用户I D。
8.10.2 seteuid和s e t e g i d函数
在对P O I X . 1的建议更改中包含了两个函数s e t e u i d和s e t e g i d。它们只更改有效用户I D和有效
组I D。
#include <sys/types.h>
#include <unistd.h>
int seteuid(uid_t u i d) ;
int setegid(gid_t g i d) ;
两个函数返回:若成功则为0,若出错则为-1
一个非特权用户可将其有效用户 I D设置为其实际用户I D或其保存的设置-用户- I D。对于一个特
权用户则可将有效用户I D设置为u i d。(这区别于s e t u i d函数,它更改三个用户I D。)这一建议更
改也要求支持保存的设置-用户- I D。
S V R 4和4 . 3 + B S D都支持这两种函数。
图8 - 3给出了本节所述的修改三个不同用户I D的各个函数。
8.10.3 组I D
本章中所说明的一切都以类似方式适用于各个组I D。添加组I D不受s e t g i d函数的影响。
第 8章 进 程 控 制 1 6 3 下载
图8-3 设置不同的用户I D的各函数
8 . 11 解释器文件
S V R 4和4 . 3 + B S D都支持解释器文件。这种文件是文本文件,其起始行的形式是:
#! pathname [o p t i o n a l - a rg u m e nt]
在惊叹号和p a t h n a m e之间的空格是可任选的。最常见的是以下列行开始:
#!/ b i n / s h
p a t h n a m e通常是个绝对路径名,对它不进行什么特殊的处理 (不使用PAT H进行路径搜索)。对这
种文件的识别是由内核作为 e x e c系统调用处理的一部分来完成的。内核使调用 e x e c函数的进程
实际执行的文件并不是该解释器文件,而是在该解释器文件的第一行中p a t h n a m e所指定的文件。
一定要将解释器文件(文本文件,它以 #!开头)和解释器(由该解释器文件第一行中的
p a t h n a m e指定)区分开来。
很多系统对解释器文件第一行有长度限制 ( 3 2个字符)。这包括#!、p a t h n a m e、可选参数
以及空格数。
实例
让我们观察一个实例,从中了解当被执行的文件是个解释器文件时,内核对 e x e c函数的参
数及该解释器文件第一行的可选参数做何种处理。程序 8 - 1 0调用e x e c执行一个解释器文件。
程序8-10 执行一个解释器文件的程序
1 6 4 U N I X环境高级编程 下载
超级用户
非特权的
s e t u i d或s e t e u i d
非特权的
s e t u i d或s e t e u i d
非特权的 非特权的 实际用户I D 有效用户I D 保存的
设置-用户-I D
超级用户 超级用户
设置执行
-用户-I D的e x e c
下面先显示要被执行的该解释器文件(只有一行)的内容,接着是运行程序 8 - 1 0的结果。
$ cat /home/stevens/bin/testinterp
#!/home/stevens/bin/echoarg foo
$ a . o u t
argv[0]: /home/stevens/bin/echoarg
argv[1]: foo
argv[2]: /home/stevens/bin/testinterp
argv[3]: myarg1
argv[4]: MY ARG2
程序e c h o a rg (解释器 )回送每一个命令行参数 (它就是程序 7 - 2 )。注意,当内核 e x e c该解释器
( / h o m e / s t e v e n s / b i n / e c h o a rg )时,a rgv[0] 是该解释器的p a t h n a m e,a rgv[1] 是解释器文件中的可选
参数,其余参数是p a t h n a m e ( / h o m e / s t e v e n s / b i n / t e s t i n t e r p ),以及程序8 - 1 0中调用e x e c l的第二和
第三个参数( m y a rg1 和MY ARG2)。调用e x e c l时的a rgv[1] 和a rgv[2] 已右移了两个位置。注意,
内核取e x e c l中的p a t h n a m e代替第一个参数( t e s t i n t e r p ),因为一般p a t h n a m e包含了较第一个参数
更多的信息。
实例
在解释器p a t h n a m e后可跟随可选参数,它们常用于为支持 - f选择项的程序指定该选择项。
例如,可以以下列方式执行a w k ( 1 )程序:
awk -f myfile
它告诉a w k从文件m y f i l e中读a w k程序。
在很多系统中,有 a w k的两个版本。a w k常常被称为“老 a w k”,它是与V 7一
起分发的原始版本。 n a w k (新a w k )包含了很多增强功能,对应于在 A h o、
K e r n i g h a n和We i n b e rg e r〔1 9 8 8〕中说明的语言。此新版本提供了对命令行参数的
存取,这是下面的例子所需的。 S V R 4提供了两者,老的 a w k既可用a w k也可用
o a w k调用,但是S V R 4已说明在将来的版本中a w k将是n a w k。P O S I X . 2将新a w k语
句就称为a w k,这正是本书所使用的。
在解释器文件中使用- f选择项,可以写成:
#!/bin/awk -f
(在解释器文件中后随a w k程序)
例如,程序8 - 11是一个在/ u s r / l o c a l / b i n / a w k e x a m p l e解释器文件中的程序。
第 8章 进 程 控 制 1 6 5 下载
程序8 - 11 解释器文件中的a w k程序
如果路径前缀之一是/ u s r / l o c a l / b i n,则可以下列方式执行程序 8 - 11 (假定我们已打开了该文
件的执行位):
$ awkexample filel FILENAME2 f3
ARGV[0] = /bin/awk
ARGV[1] = file1
ARGV[2] = FILENAME2
ARGV[3] = f3
执行/ b i n / a w k时,其命令行参数是:
/bin/awk -f /usr/local/bin/awkexample file1 FILENAME2 f3
解释器文件的路径名( / u s r / l o c a l / b i n / a w k e x a m p l e)被传送给解释器。因为不能期望该解释器
(在本例中是/ b i n / a w k)会使用PAT H变量定位该解释器文件,所以只传送其路径名中的文件名
是不够的。当a w k读解释器文件时,因为#是a w k的注释字符,所以在a w k读解释器文件时,它
忽略第一行。
可以用下列命令验证上述命令行参数。
$ s u 成为超级用户
P a s s w o r d : 输入超级用户口令

mv /bin/awk /bin/awk.save 保存原先的程序

cp /home/stevens/bin/echoarg /bin/awk暂时替换它

s u s p e n d 用作业控制挂起超级用户 s h e l l

[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
$ f g 用作业控制恢复超级用户 s h e l l
s u

mv /bin/awk.save /bin/awk 恢复原先的程序

e x i t 终止超级用户s h e l l

在此例子中,解释器的- f选择项是必需的。正如前述,它告诉a w k在什么地方得到a w k程序。
如果在解释器文件中删除- f选择项,则其结果是:
$ awkexample file1 FILENAME2 f3
/bin/awk:syntax error at source line 1
context is

/usr/local <<< /bin/awkexample
1 6 6 U N I X环境高级编程 下载
/bin/awk: bailing out at source line 1
因为在这种情况下命令行参数是:
/bin/awk /usr/local/bin/awkexample file1 FILENAME2 f3
于是a w k企图将字符串/ u s r / l o c a l / b i n / a w k e x a m p l e解释为一个a w k程序。如果不能向解释器传递
至少一个可选参数(在本例中是-f),那么这些解释器文件只有对s h e l l才是有用的。
是否一定需要解释器文件呢?那也不完全如此。但是它们确实使用户得到效率方面的好处,
其代价是内核的额外开销(因为内核需要识别解释器文件)。由于下述理由,解释器文件是有
用的:
(1) 某些程序是用某种语言写的脚本,这一事实可以隐藏起来。例如,为了执行程序 8 - 11,
只需使用下列命令行:
awkexample o p t i o n a l - a rg u m e n t s
而并不需要知道该程序实际上是一个a w k脚本,否则就要以下列方式执行该程序:
awk -f awkexample o p t i o n a l - a rg u m e n t s
(2) 解释器脚本在效率方面也提供了好处。再考虑一下前面的例子。仍旧隐藏该程序是一
个a w k脚本的事实,但是将其放在一个s h e l l脚本中:
awk ‘BEGIN {
for (i = 0; i < ARGC; i++)
printf “ARGV[%d] = %s\n”, i, ARGV[i]
e x i t
}’ $*
这种解决方法的问题是要求做更多的工作。首先, s h e l l读此命令,然后试图 e x e c l p此文件名。
因为s h e l l脚本是一个可执行文件,但却不是机器可执行的,于是返回一个错误, e x e c l p就认为
该文件是一个s h e l l脚本(它实际上就是这种文件 )。然后执行/ b i n / s h,并以该s h e l l脚本的路径名
作为其参数。s h e l l正确地执行我们的s h e l l脚本,但是为了运行a w k程序,它调用f o r k , e x e c和w a i t。
用一个s h e l l脚本代替解释器脚本需要更多的开销。
(3) 解释器脚本使我们可以使用除/ b i n / s h以外的其他s h e l l来编写s h e l l脚本。当e x e c l p找到一
个非机器可执行的可执行文件时,它总是调用/ b i n / s h来解释执行该文件。但是,用解释器脚本,
则可编写成:

! / b i n / c s h

(在解释器文件中后随C shell脚本)
再一次,我们也可将此放在一个/ b i n / s h脚本中(然后由其调用C shell),但是要有更多的开销。
如果三个s h e l l和a w k没有用#作为注释符,则上面所说的都无效。
8.12 system函数
在程序中执行一个命令字符串很方便。例如,假定要将时间和日期放到一个文件中,则可
使用6 . 9节中的函数实现这一点。调用t i m e得到当前日历时间,接着调用l o c a l t i m e将日历时间变
换为年、月、日、时、分、秒、周日形式,然后调用 s t r f t i m e对上面的结果进行格式化处理,
最后将结果写到文件中。但是用下面的s y s t e m函数则更容易做到这一点。
system(“date > file”);
第 8章 进 程 控 制 1 6 7 下载
ANSI C定义了s y s t e m函数,但是其操作对系统的依赖性很强。
因为s y s t e m不属于操作系统界面而是 s h e l l界面,所以 P O S I X . 1没有定义它,
P O S I X . 2则正在对其进行标准化。下列说明与P O S I X . 2标准的草案11 . 2相一致。
#include <stdlib.h>
int system(const char * c m d s t r i n g) ;
返回:(见下)
如果c m d s t r i n g是一个空指针,则仅当命令处理程序可用时, s y s t e m返回非0值,这一特征可以
决定在一个给定的操作系统上是否支持s y s t e m函数。在U N I X中,s y s t e m总是可用的。
因为s y s t e m在其实现中调用了f o r k、e x e c和w a i t p i d,因此有三种返回值:
(1) 如果f o r k失败或者w a i t p i d返回除E I N T R之外的出错,则s y s t e m返回-1,而且e r r n o中设
置了错误类型。
(2) 如果e x e c失败(表示不能执行s h e l l ),则其返回值如同s h e l l执行了e x i t ( 1 2 7 )一样。
(3) 否则所有三个函数( f o r k , e x e c和w a i t p i d )都成功,并且s y s t e m的返回值是s h e l l的终止状态,
其格式已在w a i t p i d中说明。
如果w a i t p i d由一个捕捉到的信号中断,则 s y s t e m很多当前的实现都返回一个
错误( E I N T R ),在这种情况下s y s t e m不返回一个错误的要求已被加到 P O S I X . 2的最
近草案中。( 1 0 . 5节中将讨论被中断的系统调用。)
程序8 - 1 2是s y s t e m函数的一种实现。它对信号没有进行处理。 1 0 . 1 8节中将修改此函数使其
进行信号处理。
s h e l l的- c选择项告诉s h e l l程序取下一个命令行参数(在这里是c m d s t r i n g)作为命令输入(而不
是从标准输入或从一个给定的文件中读命令 )。s h e l l对以n u l l字符终止的命令字符串进行语法分
析,将它们分成分隔开的命令行参数。传递给s h e l l的实际命令串可以包含任一有效的s h e l l命令。
例如,可以用<和>对输入和输出重新定向。
如果不使用s h e l l执行此命令,而是试图由我们自己去执行它,那么将相当困难。首先,我
们必须用e x e c l p而不是e x e c l,像s h e l l那样使用PAT H变量。我们必须将n u l l符结尾的命令字符串
分成各个命令行参数,以便调用e x e c l p。最后,我们也不能使用任何一个s h e l l元字符。
注意,我们调用_e x i t而不是e x i t。这是为了防止任一标准I / O缓存(这些缓存会在f o r k中由父
进程复制到子进程)在子进程中被刷新。
程序8-12 system函数(没有对信号进行处理)
1 6 8 U N I X环境高级编程 下载
用程序8 - 1 3对这种实现的s y s t e m函数进行测试( p r _ e x i t函数定义在程序8 - 3中)。运行程序8 -
1 3得到:
$ a . o u t
Thu Aug 29 14:24:19 MST 1991
normal termination, exit status = 0对于d a t e
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对于e x i t
程序8-13 调用s y s t e m函数
第 8章 进 程 控 制 1 6 9 下载
使用s y s t e m而不是直接使用f o r k和e x e c的优点是:s y s t e m进行了所需的各种出错处理,以
及各种信号处理(在1 0 . 1 8节中的下一个版本s y s t e m函数中)。 在U N I X的早期版本中,包括S V R 3 . 2和4 . 3 B S D,都没有w a i t p i d函数,于是父进程用下列形
式的语句等待子进程:
while ((lastpid = wait(&status)) != pid && lastpid != -1)
;
如果调用s y s t e m的进程在调用它之前已经生成一个子进程(并执行一个程序),那么将引
起问题。因为上面的w h i l e语句一直循环执行,直到由s y s t e m产生的子进程终止才停止,如果其
任意一个不是用p i d标识的子进程在此之前终止,则它们的进程 I D和终止状态都被w h i l e语句丢
弃。实际上,由于 w a i t不能等待一个指定的进程, P O S I X . 1才为此及其他一些原因定义了
w a i t p i d函数。如果不提供w a i t p i d,对于p o p e n和p c l o s e函数也会发生同样的问题。
设置-用户-I D程序
如果在一个设置-用户-I D程序中调用s y s t e m,那么发生什么呢?这是一个安全性方面的漏洞,
决不应当这样做。程序8 - 1 4是一个简单程序,它只是对其命令行参数调用 s y s t e m函数。
程序8-14 用s y s t e m执行命令行参数
将此程序编译成可执行目标文件t s y s。
程序8 - 1 5是另一个简单程序,它打印其实际和有效用户 I D。
程序8-15 打印实际和有效用户I D
将此程序编译成可执行目标文件p r i n t u i d s。运行这两个程序,得到下列结果:
$ tsys printuids 正常执行,无特权
real uid = 224, effective uid = 224
1 7 0 U N I X环境高级编程 下载
normal termination, exit status = 0
$ s u 成为超级用户
P a s s w o r d : 输入超级用户口令

chown root tsys 更改所有者

chmod u+s tsys 增加设置-用户-I D

ls -1 tsys 检验文件许可权和所有者

-rwsrwxr-x 1 root 105737 Aug 18 11:21 tsys

e x i t 终止超级用户s h e l l

$ tsys printuids
real uid = 224, effective uid = 0哎呀! 这是一个安全性漏洞
normal termination, exit status = 0
我们给予t s y s程序的超级用户许可权在s y s t e m中执行了f o r k和e x e c之后仍被保持下来,也就是说
执行s y s t e m中s h e l l命令的进程也具有了超级用户许可权。
如果一个进程正以特殊的许可权 (设置-用户-I D或设置-组-I D )运行,它又想生成另一个进
程执行另一个程序,则它应当直接使用f o r k和e x e c,而且在f o r k之后、e x e c之前要改回到普通许
可权。设置-用户-I D或设置-组-I D程序决不应调用s y s t e m函数。
这种警告的一个理由是: s y s t e m调用s h e l l对命令字符串进行语法分析,而
s h e l l则使用I F S变量作为其输入字段分隔符。早期的 s h e l l版本在被调用时不将此变
量恢复为普通字符集。这就允许一个不怀好意的用户在调用 s y s t e m之前设置I F S,
造成s y s t e m执行一个不同的程序。
8.13 进程会计
很多U N I X系统提供了一个选择项以进行进程会计事务处理。当取了这种选择项后,每当
进程结束时内核就写一个会计记录。典型的会计记录是 3 2字节长的二进制数据,包括命令名、
所使用的C P U时间总量、用户I D和组I D、起动时间等。本节将比较译细地说明这种会计记录,
这样也使我们得到了一个再次观察进程的机会,得到了使用 5 . 9节中所介绍的f r e a d函数的机会。
任一标准都没有对进程会计进行过说明。本节的说明依据 S V R 4和4 . 3 + B S D实
现。S V R 4提供了很多程序处理这种原始的会计数据——例如 r u n a c c t和a c c t c o m。
4 . 3 + B S D提供s a ( 8 )命令处理并总结原始会计数据。
一个至今没有说明过的函数 ( a c c t )起动和终止进程会计。唯一使用这一函数的是 S V R 4和
4 . 3 + B S D的a c c t o n ( 8 )命令。超级用户执行一个带路径名参数的 a c c t o n命令起动会计处理。该路
径名通常是/ v a r / a d m / p a c c t(早期系统中为/ u s r / a d m / a c c t)。执行不带任何参数的a c c t o n命令则停
止会计处理。
会计记录结构定义在头文件< s y s / a c c t . h >中,其样式如下:
第 8章 进 程 控 制 1 7 1 下载
由于历史原因,伯克利系统,包括4 . 3 + B S D都不提供a c _ s t a t变量。
其中,a c _ f l a g记录了进程执行期间的某些事件。这些事件见表 8 - 6。 表8-6 会计记录中的a c _ f l a g值
a c _ f l a g 说 明
A F O R K 进程是由f o r k产生的,但从未调用e x e c
A S U 进程使用超级用户优先权
A C O M P A T 进程使用兼容方式(仅VA X)
A C O R E 进程转储c o r e(不在S V R 4)
A X S I G 进程由信号消灭(不在S V R 4)
会计记录所需的各个数据(各C P U时间、传输的字符数等)都由内核保存在进程表中,并
在一个新进程被创建时置初值 (例如f o r k之后在子进程中)。进程终止时写一个会计记录。这就
意味着在会计文件中记录的顺序对应于进程终止的顺序,而不是它们起动的顺序。为了确定起
动顺序,需要读全部会计文件,并按起动日历时间进行排序。这不是一种很完善的方法,因为
日历时间的单位是秒(见 1 . 1 0节),在一个给定的秒中可能起动了多个进程。而墙上时钟时间
的单位是时钟滴答(通常,每秒滴答数在 5 0~1 0 0之间)。但是我们并不知道进程的终止时间,
所知道的只是起动时间和终止顺序。这就意味着,即使墙上时间比起动时间要精确得多,但是
仍不能按照会计文件中的数据重构各进程的精确起动顺序。
会计记录对应于进程而不是程序。在f o r k之后,内核为子进程初始化一个记录,而不是在一个
新程序被执行时。虽然e x e c并不创建一个新的会计记录,但相应记录中的命令名改变了,A F O R K
标志则被清除。这意味着,如果一个进程顺序执行了三个程序(A exec B,B exec C,最后C exit),但只
写一个会计记录。在该记录中的命令名对应于程序C,但C P U时间是程序A、B、C之和。
实例
为了得到某些会计数据以便查看,运行程序 8 - 1 6,它调用f o r k四次。每个子进程做不同的
事情,然后终止。此程序所做的基本工作示于图 8 - 4中。
程序8 - 1 7则从会计记录中选择一些字段并打印出来。
程序8-16 产生会计数据的程序
1 7 2 U N I X环境高级编程 下载
图8-4 会计处理实例的进程结构
然后,执行下列操作步骤:
(1) 成为超级用户,用a c c t o n命令起动会计事务处理。注意,当此命令结束时,会计事务处
第 8章 进 程 控 制 1 7 3 下载
父进程
第一个子进程
第二个子进程
第三个子进程
第四个子进程
理已经起动,因此在会计文件中的第一个记录应来自这一命令。
(2) 运行程序8 - 1 6。这会加五个记录到会计文件中(父进程一个,四个子进程各一个)。在第
二个子进程中,e x e c l并不创建一个新进程,所以对第二个进程只有一个会计记录。
(3) 成为超级用户,停止会计事务处理。因为在 a c c t o n命令终止时已停止处理会计事务,所
以不会在会计文件中增加一个记录。
(4) 运行程序8 - 1 7,从打印文件中选出字段并打印。
第( 4 )步的输出如下面所示。在每一行中都对进程加了说明,以便后面讨论。
accton e = 7, chars = 64, stat = 0: S
dd e = 37, chars = 221888, stat = 0: 第二个子进程
a.out e = 128, chars = 0, stat = 0: 父进程
a.out e = 274, chars = 0, stat = 134: F D X 第一个子进程
a.out e = 360, chars = 0, stat = 9: F X 第四个子进程
a.out e = 484, chars = 0, stat = 0: F 第三个子进程
程序8-17 打印从系统会计文件中选出的字段
1 7 4 U N I X环境高级编程 下载
墙上日历时间值的单位是C L K _ T C K。从表2 - 6中可见,本系统的值是6 0。例如,在父进程
中的s l e e p ( 2 )对应于墙上日历时间1 2 8个时钟滴答。对于第一个子进程, s l e e p ( 4 )变成2 7 4时钟滴
答。注意,一个进程睡眠的时间总量并不精确。(第1 0章将返回到s l e e p函数。)调用f o r k和e x i t
也要一些时间。
注意,a c _ s t a t并不是进程的真正终止状态。它只是8 . 6节中讨论的终止状态的一部分。如果
进程异常终止,则此字节中的信息只是 c o r e标志位(一般是最高位)以及信号编号数(一般是低7 位)。如果进程正常终止,则从会计文件不能得到进程的退出 ( e x i t )状态。对于第一个进程,此
值是1 2 8 + 6。1 2 8是c o r e标志位,6是此系统上信号 S I G A B RT的值(它是由调用 a b o r t产生的)。
第四个子进程的值是 9,它对应于S I G K I L L的值。从会计文件的数据中不能了解到,父进程在
退出时所用的参数值是2,三个子进程退出时所用的参数值是0。
d d进程复制到第二个子进程中的文件 / b o o t的长度是110 888字节。而I / O字符数是此值的二
倍,因为读了110 888字节,然后又写了110 888字节。即使输出到n u l l设备,仍对I / O字符数进
行计算。
a c _ f l a g值与我们所预料的相同。除调用了e x e c l的第二个子进程以外,其他子进程都设置了
F标志。父进程没有设置F标志,其原因是交互式s h e l l曾调用过f o r k生成父进程,然后执行a . o u t
文件。调用了a b o r t的第一个子进程的c o r e转储标志( D )打开。因为a b o r t产生信号S I G A B RT以产
生c o r e转储。该进程的X标志也打开,因为它是由信号终止的。第四个子进程的 X标志也打开,
但是S I G K I L L信号并不产生c o r e转储,它只是终止该进程。
最后要说明的是:第一个子进程的 I / O字符数为0,但是该进程产生了一个 c o r e文件。其原
因是写c o r e文件所需的I / O并不由该进程负担。
8.14 用户标识
任一进程都可以得到其实际和有效用户I D及组I D。但是有时希望找到运行该程序的用户的
登录名。我们可以调用g e t p w u i d ( g e t u i d ( ) ),但是如果一个用户有多个登录名,这些登录名又对
应着同一个用户I D,那么又将如何呢?(一个人在口令文件中可以有多个登录项,它们的用户
I D相同,但登录s h e l l则不同。)系统通常保存用户的登录名(见 6 . 7节),用g e t l o g i n函数可以存
取此登录名。
#include <unistd.h>
char *getlogin(void);
返回:若成功则为指向登录名字符串的指针,若出错则为 N U L L
如果调用此函数的进程没有连接到用户登录时所用的终端,则本函数会失败。通常称这些进程
为精灵进程(d a e m o n),第1 3章将对这种进程专门进行讨论。
得到了登录名,就可用g e t p w n a m在口令文件中查找相应记录以确定其登录s h e l l等。
第 8章 进 程 控 制 1 7 5 下载
为了找到登录名, U N I X系统在历史上一直是调用 t t y n a m e函数(见11 . 9节),
然后在u t m p文件(见6 . 7节)中找匹配项。 4 . 3 + B S D将登录名存放在进程表项中,
并提供系统调用存取该登录名。
系统V提供c u s e r i d函数返回登录名。此函数先调用 g e t l o g i n函数,如果失败则
再调用g e t p w u i d ( g e t u i d ( ) )。IEEE Std.1003.1-1988说明了c u s e r i d,但是它以有效用
户I D而不是实际用户I D来调用。P O S I X . 1的1 9 9 0最后版本删除了c u s e r i d函数。
FIPS 151-1要求登录s h e l l定义一个环境变量L O G N A M E ,其值为用户的登录名。
在4 . 3 + B S D中,此变量由l o g i n设置,并由登录s h e l l继承。但是,用户可以改变环
境变量,所以不能使用L O G N A M E来确认用户,而应当使用g e t l o g i n函数。
8.15 进程时间
在1 . 1 0节中说明了墙上时钟时间、用户 C P U时间和系统C P U时间。任一进程都可调用t i m e s
函数以获得它自己及终止子进程的上述值。
#include <sys/times.h>
clock_t times(struct tms * b u f) ) ;
返回:若成功则为经过的墙上时钟时间(单位:滴答),若出错则为-1
此函数填写由b u f指向的t m s结构,该结构定义如下:
注意,此结构没有包含墙上时钟时间。作为代替,t i m e s函数返回墙上时钟时间作为函数值。此值
是相对于过去的某一时刻度量的,所以不能用其绝对值而必须使用其相对值。例如,调用t i m e s,
保存其返回值。在以后某个时间再次调用t i m e s,从新返回的值中减去以前返回的值,此差值就
是墙上时钟时间。(一个长期运行的进程可能其墙上时钟时间会溢出,当然这种可能性极小。)
结构中两个针对子进程的字段包含了此进程已等待到的各子进程的值。
所有由此函数返回的c l o c k _ t值都用_S C_C L K_T C K (由s y s c o n f函数返回的每秒时钟滴答数,
见2 . 5 . 4节)变换成秒数。
伯克利系统,包括 4 . 3 B S D继承了V 7的t i m e s版本,它不返回墙上时钟时间。
这一老版本如执行成功则返回0,如失败则返回-1。4 . 3 + B S D支持P O S I X . 1版本。
4 . 3 + B S D和S V R 4 (在B S D兼容库中)提供了g e t r u s a g e ( 2 )函数,此函数返回C P U
时间,以及指示资源使用情况的另外1 4个值。
实例
程序8 - 1 8将每个命令行参数作为 s h e l l命令串执行,对每个命令计时,并打印从 t m s结构取
1 7 6 U N I X环境高级编程 下载
得的值。按下列方式运行此程序,得到:
$ 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行中显示的时间是执行 s h e l l和命令的子进程所使用的
C P U时间。
程序8-18 时间以及执行命令行参数
第 8章 进 程 控 制 1 7 7 下载
让我们再运行1 . 1 0节中的例子:
如同所期望的那样,所有三个值(实际时间和子进程C P U时间)都与1 . 1 0节中的值相近。
8.16 小结
对在U N I X环境中的高级程序设计而言,完整地了解 U N I X的进程控制非常重要。其中必须
熟练掌握的只有几个——f o r k、e x e c族、_ e x i t、w a i t和w a i t p i d。很多应用程序都使用这些原语。
f o r k原语也给了我们一个了解竞态条件的机会。
本章说明了 s y s t e m函数和进程会计,以及这些进程控制函数的应用情况。本章还说明了
e x e c函数的另一种变体:解释器文件及它们的工作方式。对各种不同的用户 I D和组I D (实际,
有效和保存的)的理解和编写安全的设置-用户- I D程序是至关重要的。
在了解进程和子进程的基础上,下一章将进一步说明进程和其他进程的关系—对话期和
作业控制。第1 0章将说明信号机制并以此结束对进程的讨论。
习题
8 . 1调在程序8 - 2中用e x i t取代_ e x i t将关闭标准输出,修改程序验证p r i n t f确实返回-1。
8 . 2调调用v f o r k后,子进程运行在父进程的地址空间中。如果不是在m a i n函数中调用v f o r k,
而是在v f o r k以后子进程从这个函数返回,那将会如何?请编写一段程序验证并且画出堆栈中
的映像。
8 . 3调当用$ a . o u t执行程序8 - 7一次,其输出是正确的。但是若将该程序按下列方式执行多次,
则其输出不正确。
$ a.out ; a.out ; a.out
output from parent
ooutput from parent
ouotuptut from child
1 7 8 U N I X环境高级编程 下载
put from parent
output from child
utput from child
原因是什么?怎样才能更正此种错误?如果使子进程首先输出,还会发生此问题吗?
8 . 4在在程序8 - 1 0中,调用e x e c l,指定解释文件为p a t h n a m e。如果调用e x e c l p,指定t e s t i n t e r p
为f i l e n a m e,并且目录/ h o m e / s t e v e n s / b i n是路径前缀,则运行该程序时,a rg v [ 2 ]的打印输出是什
么?
8 . 5在一个进程怎样才能获得其保存的设置-用户- I D?
8 . 6在编写一段程序,用于创建一个僵死进程,然后调用 s y s t e m执行p s ( 1 )命令以验证该进程
是僵死进程。
8 . 7在8 . 9节中提及P O S I X . 1要求在e x e c时关闭打开目录流。按下列方法对此进行验证:对根
目录调用o p e n d i r,查看D I R结构,然后打印e x e c关闭标志。接着打开同一目录读并打印 e x e c关
闭标志。
第 8章 进 程 控 制 1 7 9 下载

发布了95 篇原创文章 · 获赞 0 · 访问量 1911

猜你喜欢

转载自blog.csdn.net/qq_42894864/article/details/104106244