引言:
对于每个进程,都有一个非负整数表示的唯一进程ID。虽然进程的ID是唯一的,但却是可重用的。系统中有一些专用的进程。如ID为0的进程通常是调度进程,也成交换进程或系统进程(它是内核进程)。进程ID为1通常是init进程,它是一个普通的用户进程。一些与进程ID有关的函数:
#include <unistd.h>
pid_t getpid(void); //返回值:调用进程的进程ID
pit_t getppid(void); //返回值:调用进程的父进程ID
uid_t getuid(void); //返回值:调用进程的实际用户ID
uid_t geteuid(void); //返回值:调用进程的有效组ID
gid_t getgid(void); //返回值:调用进程的有效用户ID
git_t getegid(void); //返回值:调用进程的有效组ID
(一)
一个现有进程可以通过调用fork函数创建一个新的进程。由fork创建的新进程被称为子进程。fork函数被调用一次,但返回两次。两次返回的唯一区别是子进程的返回值是0,而父进程的返回值则是新的子进程的进程ID。子进程和父进程继续执行fork调用之后的指令。子进程是父进程的副本。例如,子进程获得父进程数据空间、堆和栈的副本。注意,这是子进程所拥有的副本。父子进程并不共享这些存储空间部分。父子进程共享正文段。由于在fork之后,经常跟随者exec,所以现在的很多实现并不执行一个父进程数据段、栈和堆的完全复制。作为替代,使用写时复制技术。
fork的一般使用形式如下:
if((pid = fork()) < 0){ // 用fork创建新进程 printf("fork error"); }else if(pid == 0){ //子进程的操作 }else{ //父进程的操作 }
父、子进程之间的区别是:
fork的返回值不同;进程ID不同;两个进程具有不同的父进程ID:子进程的父进程ID是创建它的进程的ID,而父进程的父进程ID则不变;子进程的tms_utime、tms_stime、tms_cutime和tms_ustime均被设置为0。父进程设置的文件锁不会被子进程继承。子进程的未处理的闹钟被清除。子进程的未处理信号集设置为空集。
子进程除了继承了父进程打开的文件外,还包括:实际用户ID、实际组ID、有效用户ID、有效组ID,附加组ID、进程组ID、会话ID,控制终端、存储映射等等。
通常情况下fork都会成功,但也有可能失败。使fork失败的两个主要原因是:1、系统中已经有了太多的进程,2、该实际用户ID的进程总数超过了系统限制。
fork有下面两种用法:
1、一个父进程希望复制自己,使父、子进程同时执行不同的代码段。这在网络服务进程中是常见的——父进程等待客户端的服务请求。当这种请求到达时,父进程调用fork,使子进程处理此请求。父进程则继续等待下一个服务请求到达。
2、一个进程要执行一个不同的程序。这对shell是常见的情况。这种情况下,子进程从fork返回后立即调用exec。
除了fork创建一个新进程外还有vfork同样用来创建一个新进程:
vfork函数的调用序列和返回值与fork相同,但两者的语义有如下几点不同:
1、vfork用于创建一个新进程,而该新进程的目的是exec一个新程序。
2、vfork与fork一样都创建一个子进程,但它并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec(或exit),于是也就不会存访该地址空间。
3、vfork与fork之间的另一个区别是:vfork保证子进程先运行,在它调用exec或exit之后父进程才可能运行。
(二)
进程是有生命周期的,从其被创建到终止,就是其生命周期。进程的终止有8种方式,5种正常终止方式、3种异常终止方式。不管进程如何终止,最后都会执行内核中的同一段代码。这段代码为相应进程关闭所有打开描述符,释放它所使用的存储器等。
不管是正常终止还是异常终止,我们都希望终止进程能够通知其父进程它是如何终止的。对于三个终止函数(exit、_exit和_Exit),实现这一点的方法是,将其退出状态作为参数传送给函数。在异常终止情况下,内核(而不是进程本身)产生一个指示其异常终止原因的终止状态。在任意一种情况下,该终止进程的父进程都能用wait或waitpid函数去的终止状态。注意:这里使用了“退出状态”和“终止状态”两个术语,以表示有所区别。在最后调用_exit时,内核将退出状态转换成终止状态。
关于进程的终止及退出状态,要注意一下几点:
1、子进程在父进程调用fork后生成,子进程将其终止状态返回给父进程。但如果父进程在子进程之前终止,则,对于父进程已经终止的所有进程,他们的父进程都改变为init进城。称这些进程被init进程领养。操作过程如下:在一个进程终止时,内核逐个检查所有活动进程,以判断它是否是正要终止进程的子进程,如果是,则将该进程的父进程ID更改为1。
2、如果子进程在父进程之前终止,那么父进程又如何能在做相应检查时得到子进程的终止状态呢?答:内核为每个终止进程保存了一定量的信息,所以当终止进程的父进程调用wait或waitpid时,可以得到这些信息。这些信息至少包括进程ID、该进程的终止状态、以及该进程使用的CPU时间总量。内核可以释放终止进程所使用的所有存储区,关闭其所有打开文件。一个已经终止、但是其父进程尚未对其进行善后处理的进程称为僵死进程。
3、一个由init进程领养的进程终止时会发生什么?它会不会变成一个僵死进程?不会。因为init被编写成无论何时只要有一个子进程终止,init就会调用一个wait函数去的其终止状态。当提及“一个init的子进”时,这指的可能是init直接产生的进程,也可能是其父进程已终止,由init领养的进程。
(三)
当一个进程正常或异常终止时,内核就向其父进程发送SIGCHLD信号。因为子进程终止是个异步事件,所以这种信号也是内核向父进程发的异步通知。父进程可以选择忽略该信号,或者提供一个该信号发生时即被调用执行的寒素(信号处理程序)。系统默认是忽略它。现在需要知道的是调用wait或waitpid的进程可能发生什么情况:
1、如果其所有子进程都还在运行,则阻塞。
2、如果一个子进程已终止,正等待父进程获取其终止状态,则取得该子进程的终止状态立即返回。
3、如果它没有任何子进程,则立即出错返回。
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options); // 返回值:若成功则返回进程ID,0,若出错则返回-1
两个函数的区别是:
1、在一个子进程终止前,wait使其调用者阻塞,而waitpid有一个选项,可使调用者不阻塞。
2、waitpid并不等待在其调用之后的第一个终止子进程,它由若干选项,可以控制它所等待的进程。
函数的参数status是一个整型指针。如果status不是一个空指针,则终止进程的终止状态就存放在它所指向的单元内。如果不关心终止状态,则可将该参数指定为空指针。检查wait或waitpid所返回的终止状态的宏有4个:WIFEXITED(status)、WIFSIGNALED(status)、WIFSTOPPED(status)、WIFONTINUED(stauts)。
waitpid函数提供了wait函数没有提供的三个功能:
1、waitpid可等待一个特定的进程,而wait则返回任一终止子进程的状态。
2、waitpid提供了一个wait的非阻塞版本。有时用户希望取得一个子进程的状态,但不想阻塞。
3、waitpid支持作业控制。