[TOC]
每个进程都有一个非负整型表示的唯一进程ID。
虽然是唯一的,但是进程ID是可以复用的。当一个进程终止后,其进程ID久成为复用的候选者。大多数UNIX系统实现延迟复用算法,使得赋予新建进程的ID不同于最近终止进程所使用的ID。
系统中的一些专用进程:
- ID为0的进程通常是调度进程,常常被称为
交换进程(swapper)
。该进程是内核的一部分,它不执行任何磁盘上的程序,因此也被称为系统进程。 - 进程ID 1通常是init进程,在自举过程结束时由内核调用。该进程的程序文件在UNIX的早起版本中是
/etc/init
,在较新版本中是/sbin/init
。此进程负责在自举内核后启动一个UNIX系统。init通常读取与系统有关的初始化文件(/etc/rc*文件或/etc/inittab文件,以及在/etc/init.d中的文件),并将系统引导到一个状态(如多用户)。init进程绝不会终止。它是一个普通的用户进程(与交换文件不同,它不是内核中的系统进程),但它以超级用户特权运行。init会成为所有孤儿进程的父进程。
由fork创建的新进程被称为子进程(child process)。fork函数被调用一次,但返回两次。两次但会的区别是子进程的返回值是0,而父进程的返回值则是新建子进程的进程ID。
子进程是父进程的副本。例如,子进程获得父进程数据空间、堆和栈的副本。注意,这是子进程所拥有的副本。父进程和子进程并不共享这些储存空间部分。父进程和子进程共享正文段。
由于fork之后通跟着exec,所以现在很多实现并不执行一个父进程数据段、栈和堆的完全副本。作为替代,使用了**写时复制(Copy-On-Write,COW)**技术。这些区域由父进程和子进程共享,而且内核将它们的访问权限改变为只读。如果父进程和子进程中的任何一个试图修改这些区域,则内核只为修改区域的那块内存制作一个副本,通常是虚拟存储系统中的一“页”。
在fork之后父进程先执行还是子进程先执行是不确定的,这取决于内核所使用的调度算法。
fork的另一个特性是父进程的所有打开文件描述符都被复制到子进程中。父进程和子进程每个相同的打开描述符共享一个表项。重要的一点是,父进程和子进程共享同一个文件偏移量。
在fork之后处理文件描述符有以下两种常见的情况。
- 父进程等待子进程完成。这种情况下,父进程无需对其描述符做任何事情。当子进程终止后,它曾进行过读、写操作的任一共享描述符的文件偏移量已经做了相应的更新。
- 父进程和子进程各自执行不同的程序段。在这种情况下,在fork之后,父进程和子进程各自关闭他们不需使用的文件描述符。
除打开文件之外,父进程和很多属性也由子进程继承,包括:
- 实际用户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失败的两个主要原因是:
- 系统中已经有了太多进程
- 该用户ID的进程总数超过了系统限制。
vfork函数用于创建一个新的进程,而该进程的目的是exec一个新程序。
vfork与fork一样都创建一个子进程,但是它并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec(或exit),于是也就不会引用该地址空间。不过在子进程调用exec或exit之前,它在进程的空间中运行。
vfork和fork之间的另一个区别是:vfork保证子进程先运行,在它调用exec或exit之后父进程才可能被调度运行,当子进程调用这两个函数中的任意一个时,父进程会恢复运行。(如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。)
5种正常终止及3种异常终止方式。5种正常终止方式具体如下:
- 在main函数内执行return语句。等效于调用exit。
- 调用exit函数。此函数由ISO C定义,其操作包括调用各种终止处理程序(终止处理程序在调用atexit函数时登记),然后关闭所有标准I/O流等。
- 调用
_exit
或_Exit
函数。ISOC定义_Exit
,其目的是为进程提供一种无需运行终止处理程序或信号处理程序而终止的方法。在UNIX系统中,_Exit
和_exit
是同义的,并不冲洗标准I/O流。_exit
函数由exit调用,它处理UNIX系统特定的细节。 - 进程的最后一个线程在其启动例程中执行return语句。但是,该线程的返回值不用作进程的返回值。当最后一个线程从其启动例程返回时,该进程以终止状态0返回。
- 进程的最后一个线程调用
pthread_exit
函数。
3种异常终止如下:
- 调用abort。它产生SIGABRT信号。
- 当进程收到某些信号时。信号可能由进程自身、其他进程或内核产生。
- 最后一个线程对“取消”(cancellation)请求做出响应。默认情况下,“取消”以延迟方式发生:一个线程要求取消另一个线程,若干时间后,目标线程停止。
不管进程如何终止,都会执行内核中的同一段代码。这段代码为相应进程关闭所有打开描述符,释放它所使用的储存器等。
上述任何一种终止情形,进程都能通知到其父进程是如何终止的。对于3个终止函数(exit
, _exit
和 _Exit
),实现这一点的方法是,将其退出状态(exit status)作为参数传递给函数。在异常终止的情况,内核(不是进程本身)产生一个指示其异常终止原因的终止状态(termination status)。在任意一种情况下,该终止进程的父进程都能用wait或waitpid函数取得其终止状态。
这里用到了“退出状态”(传递给3个终止函数的参数,或main的返回值)和“终止状态”。在最后调用_exit
时,内核将退出状态转换成终止状态。如果子进程正常终止,则父进程可以获得子进程的退出状态。
对于父进程已经终止的所有进程,它们的父进程都改变为init进程。我们称这些进程由init进程收养。其操作过程大致是:在一个进程终止时,内核逐个检查所有活动进程,以判断它是否是正要终止进程的子进程。如果是,父进程的ID就更改为1(init进程的 ID)。这种处理 方法保证每个进程都有一个父进程。
如果子进程在父进程之前终止,内核为每个子进程保存了一定量的信息,所以当终止进程的父进程调用wait或waitpid时,可以得到这些信息。这些信息至少包括进程ID、该进程的终止状态以及该进程使用的CPU时间总量。内核可以释放终止进程所使用的所有储存区,关闭其所有打开文件。
在UNIX术语中,一个已经终止、但是其父进程尚未对其进行善后处理(获取终止子进程的有关信息、释放它仍占用的资源)的进程被称为僵死进程(zombie)
init进程收养的进程不会变成僵死进程,因为init被编写成无论何时只要有一个子进程终止,init就会调用一个wait函数取得其终止状态。
当一个进程正常或异常终止时,内核就向其父进程发送SIGCHLD信号。因为子进程终止是个异步事件(这可以在父进程运行的任何时候发生),所以这种信号也是内核向父进程发的异步通知。父进程可以选择忽略该信号,或者是提供一个该信号发生时即被调用执行的函数。对于这种信号的系统默认动作是忽略它。
调用wait或waitpid的进程会发生:
- 如果其所有子进程都还在运行,则阻塞。
- 如果一个子进程已终止,正带等待父进程获取其终止状态,则取得该子进程的终止状态立即返回。
- 如果它没有任何子进程,则立即出错返回。
#include <sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pit_t pid, int *statloc, int options);
// 两个函数返回值:若成功,返回进程id;若出错,返回0或-1
两个函数的区别如下:
- 在一个子进程终止前,wait使其调用者阻塞,而waitpid有一个选项,可使调用者不阻塞。
- waitpid并不等待在其调用之后的第一个终止子进程,它有若干选项,可以控制它所等待的进程。
如果子进程已经终止,并且是一个僵死进程,则wait立即返回并取得该子进程的状态;否则wait使其调用者阻塞,直到一个子进程终止。如果调用者阻塞并且它有多个子进程,则在其某一个子进程终止时,wait就立即返回。因为wait返回终止子进程的进程ID,所以总能了解那一个子进程终止了。
函数参数statloc是一个整数型指针。如果statloc不是一个空指针,则终止进程终止状态就放在它所指向的单元内。
POSIX.1规定,终止状态用定义在<sys/wait.h>中的各个宏来查看。有4个互斥的宏可以用来取得进程终止的原因,它们的名字都以WIF开始。基于这4个宏哪一个值为真,就可选用其他宏来取得退出状态、信号编号等。
宏 | 说明 |
---|---|
WIFEXITED (status) |
若为正常终止子进程返回状态,则为真。对于这种情况可执行WEXITSTATUS (status),获取子进程状态传递给eixt 或_exit 参数的低8位 |
WIFSIGNALED (status) |
若为异常终止子进程返回的状态,则为真(接到一个不捕捉的信号)。对于这种情况,可执行WTERMSIG (status),获取子进程终止状态的信号编号。另外,有些实现定义宏WCOREDUMP (status),若已产生终止进程的core文件,则它返回真 |
WIFSTOPPED (status) |
若为当前暂停子进程的返回状态,则为真。对于这种情况,可执行WSTOPSIG (status),获取子进程暂停的信号编号 |
WIFCONTINUED (status) |
若在作业控制暂停后已经继续的子进程返回了状态,则为真。 |
waitpid函数中pid参数的作用解释如下。
-
pid == -1: 等待任一子进程。这种情况下,wiatpid与wait等效。
-
pid > 0: 等待进程ID与pid相等的子进程。
-
pid == 0: 等待组ID等于调用进程组ID的任一子进程。
-
pid < -1: 等待组ID等于pid绝对值的任一子进程=
waitpid 函数返回终止子进程的进程ID,并将该子进程的终止状态存放在staloc指向的储存单元中。
optinos参数可以进一步控制waitpid的操作。此参数或者是0,或者是下表中常亮按位或运算的结果。
常亮 | 说明 |
---|---|
WCONTINUED | 若实现支持作业控制,那么由pid指定的任一子进程在停止后已经继续,但其状态尚未报告,则返回其状态 |
WNOHANG | 若由pid指定的子进程并不是立即可用的,则waitpid不阻塞,此时其返回值为0 |
WUNTRACED | 若某实现支持作业控制,而由pid指定的任一子进程已处于停止状态,并且其状态自停止以来还未报告过,则返回其状态。WIFSTOPPED宏确定返回值是否对应于一个停止的子进程 |
wiatpid函数提供了wait函数没有提供的3个功能:
- waitpid可等待一个特定的进程,而wait则返回任一终止子进程的状态。
- waitpid提供了一个wait的非阻塞版本。有时希望后去一个子进程的状态,但不想阻塞。
- waitpid通过WUNTRACED和WCONTINUED选项支持作业控制