第八章:进程控制

8.1:引言

本章介绍Unix的进程控制,包括创建新进程、执行程序和进程终止。还将说明进程属性的各种ID--实际、有效和保存的用户和组ID,以及它们如何受到进程控制原语的影响。还包括解释器文件和system函数,最后讲述大多数Unix系统所提供的进程会计机制。

8.2:进程标识符

每个进程都有一个非负整型表示的唯一进程ID。虽然是唯一的,但是进程ID却可以重用,当一个进程终止后,其进程ID就可以再次使用了。Unix使用延迟重用算法,避免新进程的ID等于最近终止的进程的ID。

除了进程ID,每个进程还有一些其他的标识符。下列函数返回这些标识符:

#include <unistd.h>
pid_t getpid(void); // 返回调用进程的进程ID
pid_t getppid(void); // 返回调用进程的父进程ID
uid_t getuid(void); // 返回调用进程的实际用户ID
uid_t geteuid(void); // 返回调用进程的有效用户ID
gid_t getgid(void); // 返回调用进程的实际组ID
gid_t getegid(void); // 返回调用进程的有效组ID

8.3:fork函数

一个现有进程可以调用fork函数创建一个新进程:

#include <unistd.h>
pid_t fork(void); // 创建新进程,父进程中返回子进程ID,子进程中返回0,小于0,则出错

一般来说,在fork之后是父进程先运行还是子进程先运行,取决与系统内核的调度算法。如果要求父子进程之间相互同步,则需要某种形式的进程间通信。

fork的一个特性是父进程的所有打开文件描述符都被复制到子进程中。父子进程的每个相同的打开描述符共享一个文件表项。这种共享文件的方式使父子进程对同一文件使用了一个文件偏移量。

在fork之后处理文件描述符有两种常见的情况:

1.父进程等待子进程完成。在这种情况下,父进程无需对其描述符做任何处理。当子进程结束后,其曾进行过读写操作的任一共享描述符的文件偏移量已执行了相应更新。

2.父子进程各自执行不同的程序段。在这种情况下,父子进程各自关闭它们不需要使用的文件描述符,这样就不会干扰对方使用的文件描述符。这种方法是网络服务进程中经常使用的。

除了文件打开描述符之外,父进程的其他许多属性也由子进程继承,包括:

实际用户ID、实际组ID、有效用户ID、有效组ID
附加组ID
进程组ID
会话ID
控制终端
设置用户ID标志和设置组ID标志
当前工作目录
根目录
文件模式创建屏蔽字
信号屏蔽与安排
针对任一文件描述符的在执行时关闭(close-on-exec)标志
环境
连接的共享存储段
资源限制

父子进程之间的区别:

fork的返回值
进程ID不同
两个进程具有不同的父进程ID
子进程的tms_utime、tms_stime、tms_cutime以及tms_ustime均被设置为0
父进程设置的文件锁不会被子进程继承
子进程的未处理的闹钟被清除
子进程的未处理信号集设置为空

fork有两种用法:

1.一个父进程希望复制自己,使父子进程执行不同的代码段。这在网络服务进程中是最常见的--父进程等待客户端的请求连接。当这种请求到达时,父进程调用fork,使子进程处理此请求,父进程则继续等待请求连接。

2.一个进程要执行不同的程序。这对shell是常见的情况。在这种情况下。子进程从fork返回后立即调用exec函数。

8.4:vfork函数

vfork函数的调用序列和fork函数相同,但两者语义不同。

vfork用于创建一个新进程,而该进程的目的是exec一个新程序。vfork和fork一样都创建一个新进程,但它并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec,于是也就不会访问该地址空间。相反,在子进程调用exec或者exit之前,它在父进程的空间中运行。

vfork和fork的另一个区别是,vfork保证子进程先运行,在它调用exec或者exit之后父进程才可能被调度运行。

由于vfork调用之后,子进程调用exec或者exit之前,它在父进程空间中执行,所以,子进程对变量的修改会影响到父进程。

8.5:exit函数

如7.3节所述,进程有下面5种正常终止方式:

1.在main函数内执行return语句,这等效于调用exit函数。

2.调用exit函数。此函数由ISO C定义,其操作包括调用各终止处理程序(终止处理程序在atexit函数时登记),然后关闭所有标准IO流。

3.调用_exit函数或_Exit函数。ISO C定义_Exit,其目的是为进程提供一种无需运行终止处理程序或信号处理程序而终止的方法。对标准IO流是否冲洗,取决于实现。在Unix系统中,_Exit和_exit是同义的,并不冲洗标准IO流。_exit函数被exit函数调用,它处理Unix特定的细节。_exit是由POSIX定义的。

4.进程的最后一个线程在其启动例程中执行返回语句。但是,该进程的返回值不会作为进程的返回值。当最后一个线程从其启动例程返回时,该进程以终止状态0返回。

5.进程的最后一个线程调用pthread_exit函数。如同前面一样,这种情况,进程的终止状态总是0.

三种异常终止方式如下:

1.调用abort函数。

2.当进程接收到某些信号时。

3.最后一个线程对“取消”做出相应。

不管进程如何终止,最后都会执行内核中的同一段代码。这段代码为相应的进程关闭所有打开文件描述符,释放它使用的存储器等。

对于任意一种终止情形,我们都希望终止进程能够通知其父进程它是如何终止的。对于三个终止函数(exit、_exit、_Exit),实现这一点的方法是,将其退出状态作为参数传递给函数。在异常终止情况下,内核(不是进程本身)产生一个指示其异常终止原因的终止状态(termination status)。在任意一种情况下,该终止进程的父进程都能用wait或waitpid函数取得其终止状态。

注意,这里使用了“退出状态”(它是传给exit或_exit的参数,或main的返回值)和终止状态两个术语,以表示有所区别。在最后调用_exit时,内核将退出状态转换为终止状态。

在Unix术语中,一个已经终止,但是其父进程尚未对其处理的进程被称为僵死进程。

8.6:wait和waitpid函数

 当一个进程正常或异常终止时,内核就向其父进程发送SIGCHLD信号。因为子进程终止是一个异步事件(这可以在父进程运行的任时候发生),所以这种信号也是内核向父进程发的异步通知。

调用wait或waitpid时可能发生的情况有:

1.如果其所有子进程都还在运行,则阻塞。

2.如果一个i子进程已经终止,正等待父进程获取其终止状态啊,则取得该进程的终止状态并立即返回。

3.如果它没有任何子进程,则立即出错返回。

#include <unistd.h>
pid_t wait(int *staloc); // 等待子进程终止
pid_t waitpid(pid_t pid, int *staloc, int options); 等待指定子进程终止

这两个进程的区别如下:

1.在一个进程终止前,wait使其调用者阻塞,而waitpid有一个选项,可以使调用者不阻塞。

2.waitpid并不等待在其调用后的第一个终止子进程,它有若干个选项,可以控制它所等待的进程。

整型指针staloc用于获取终止状态,如果不关心终止状态,则可以为空。

对于waitpid函数中pid参数的作用解释如下:

pid == -1  等待任一子进程。就这一方面而言,waitpid与wait等效。

pid > 0   等待其进程ID与pid相同的子进程

pid == 0   等待其组ID等于调用进程组ID的任一子进程。

pid < -1   等待其组ID等于pid绝对值的任一子进程。

waitpid返回终止子进程的进程ID,并将子进程的终止状态存放在由staloc指向的存储单元中。对于wait,其唯一的出错是调用进程没有子进程。

waitpid函数中options常量的

WCONTINUED    若实现作业控制,那么由pid指定的任一子进程在暂停后已经继续,但其状态尚未报告,则返回其状态。

WNOHANG      若由pid指定的子进程并不是立即可用的,则waitpid不阻塞,此时其返回值为0

WUNTRACED      若某实现支持作业控制,而由pid指定的任一子进程已处于暂停状态,并且其状态自暂停以来还未报告过,则返回其状态。

waitpid函数提供了wait函数没有提供的三个功能:

1.waitpid可等待一个特定的进程,而wait则返回任一终止子进程的状态。

2.waitpid提供了一个wait函数的非阻塞版本。

3.waitpid提供作业控制。

实例:

如果一个进程fork一个子进程,但不要它等待子进程终止,也不希望子进程处于僵死状态直到父进程终止,实现这一需求的技巧是调用fork两次。程序如下:

8.7:waitid函数

 

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。