进程有下面5种正常终止方式:
(1) 在main函数内执行return语句。这等效于调用exit。
(2) 调用exit函数。此函数由ISO C定义,其操作包括调用各种终止处理程序(终止处理程序包在调用atexit函数时登记),然后关闭所有标准I/O流等。因为ISO C并不处理文件描述符、多进程以及作业控制,所以这一定义对UNIX系统是不完整的。
(3) 调用_exit或_Exit函数。ISO C定义_Exit,其目的是为进程提供一种无需运行终止处理程序或信号处理程序而终止的方法。对标准I/O流是否冲洗,这取决于实现。在UNIX系统中,_Exit和_exit是同义的,并不冲洗标准I/O流。_exit函由exit调用,它处理UNIX特定的细节。_exit是有POSIX.1说明的。
(4) 进程的最后一个线程在启动例程中执行返回语句。但是,该线程的返回值不会用作进程的返回值。当最后一个线程从启动例程返回时,该进程以终止状态0返回。
(5) 进程的最后一个线程调用pthread_exit函数。如果前面一样,在这种情况中,进程终止状态总是0,这与传送给pthread_exit的参数无关。
三种异常终止方式如下:
(1) 调用abort。它产生SIGABRT信号,这是下一种异常终止的一种特例。
(2) 当进程接收到某些信号时。信号可由进程自身(例如调用abort函数)、其他进程或内核产生。例如,若进程越出其地址空间访问存储单元或者除以0,内核会为该进程产生相应的信号。
(3) 最后一个线程对“取消”请求做出响应。按系统默认,“取消”以延迟方式发生:一个线程要求取消另一个线程,一段时间后,目标线程终止。
不管进程如何终止,最后都会执行内核中的同一段代码。这段代码为相应进程关闭所有打开描述符,释放它所使用的存储器等。
对上述任意一种终止情形,我们都希望终止进程能够通知其父进程它是如何终止的。对于三个终止函数(exit、_exit和_Exit),实现这一点的方法是,将其退出状态(exit status)作为参数传送给函数。在异常终止情况下,内核产生一个指示其异常终止原因的终止状态(termination status)。在任意一种情况下,该终止进程的父进程都能用wait或waitpid函数取得其终止状态。
这里使用了“退出状态”(它是传向exit或_exit的参数,或main的返回值)和“终止状态”两个术语。在最后调用_exit时,内核将退出状态转换成终止状态。如果子进程正常终止,则父进程可以获得子进程的退出状态。
如果父进程在子进程之前终止。对于父进程已经终止的所有进程,它们的父进程都改变为init进程。我们这里称这些进程由init进程领养。其操作过程大致如下:在一个进程终止时,内核逐个检查所有活动进程,以判断它是否是正要终止进程的子进程,如果是,则将进程的父进程ID更改为1(init进程的ID)。这种处理方式保证了每个进程都有一个父进程。
如果子进程在父进程之前终止。父进程如何在做相应检查时得到子进程的终止状态呢?内核为每个终止子进程保存了一定量的信息,所以当终止进程的父进程调用wait或waitpid时,可以得到这些信息。这些信息至少包括进程ID、该进程的终止状态、以及该进程使用的CPU时间总量。内核可以释放终止进程所使用的所有存储区,关闭其所有打开文件。
在UNIX术语中,一个已经终止的、但是其父进程尚未对其进行善后处理(获取终止子进程的有关信息,释放它仍占用的资源)的进程被称为僵死进程(zombie)。ps(1)命令将僵死进程的状态打印为Z。如果编写一个长期运行的程序,它调用fork产生了很多子进程,那么除非父进程等待取得子进程的终止状态,否则这些子进程终止后就会编程僵死进程。
init被编写成无论何时只要有一个子进程终止,init就会调用一个wait函数取得其终止状态。“一个init的子进程”,可能是init直接产生的进程,也可能是其父进程已终止,由init领养的进程。
当进程正常或异常终止时,内核就向其父进程发送SIGCHLD信号。因为子进程终止是个异步事件(这可以在父进程运行的任何时候发生),所以这种信号也是内核向父进程发的异步通知。父进程可以选择忽略该信号,或者提供一个该信号发生时被调用执行的函数(信号处理程序)。对于这种信号的系统默认动作是忽略它。
调用wait或waitpid的进程可能发生的情况:
(1) 如果其所有子进程都还在运行,则阻塞。
(2) 如果一个子进程已终止,正等待父进程获取其终止状态,则取得该子进程的终止状态立即返回。
(3) 如果它没有任何子进程,则立即出错返回。
如果进程由于接收到SIGCHLD信号而调用wait,则可期望wait会立即返回。但是如果在任意时刻调用wait,则进程可能会阻塞。
#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并不等待在调用之后的第一个终止子进程,它有若干个选项,可以控制它所等待的进程。
如果一个子进程已经终止,并且是一个僵死进程,则wait立即返回并取得该子进程的状态,否则wait使其调用者阻塞直到一个子进程终止。如果调用者阻塞而且他有多个子进程,则其在一个子进程终止时,wait就立即返回。因为wait返回终止子进程的进程ID,所以它总能了解是哪一个子进程终止了。
这两个函数的参数status是一个整型指针。如果status不是一个空指针,则终止进程的终止状态就存放在它所指向的单元内。如果不关心终止状态,则可将该参数指定为空指针。
依据传统,这两个函数返回的整型状态字是由实现定义的。其中某些位表示退出状态(正常返回),其他位则指示信号编号(异常返回),有一位指示是否产生了一个core文件等。POSIX.1规定终止状态用定义在<sys/wait.h>中的各个宏来查看。有四个互斥的宏可用来取得进程的终止状态、信号编号等。
WIFEXITED(status) 若为正常终止子进程返回的状态,则为真。对于这种情况可执行WEXITSTATUS(status),取子进程传送给exit、_exit或_Exit参数的低8位。
WIFSIGNALED(status) 若为异常终止子进程返回的状态,则为真(接到一个不捕捉的信号)。对于这种情况,可执行WTERMSIG(status),使取进程终止的信号编号。另外,有些实现定义宏WCOREDUMP(status),若已产生终止进程的core,则它返回真。
WIFSTOPPED(status) 若为当前暂停子进程的返回状态,则为真。对于这种情况,可执行WSTOPSIG(status),使取子进程暂停的信号编号。
WIFCONTINUED(status) 若在作业控制暂停后已经继续的子进程返回了状态,则为真。(POSIX.1的XSI扩展;仅用于waitpid)
《UNIX环境高级编程》P180:程序清单8-3 打印exit状态的说明
#include <stdio.h> #include <sys/wait.h> void pr_exit(int status) { if (WIFEXITED(status)) // 正常终止子进程 printf("normal termination exit status = %d\n", WEXITSTATUS(status)); else if (WIFSIGNALED(status)) // 异常终止子进程 printf("abnormal termination, signal number = %d%s\n", WTERMSIG(status), WCOREDUMP(status) ? " (core file generated)" : ""); else if (WIFSTOPPED(status)) // 当前暂停子进程 printf("child stopped, signal number = %d\n", WSTOPSIG(status)); }
《UNIX环境高级编程》P181:程序清单8-4 演示不同的exit值(有改动)
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/wait.h> void pr_exit(int status); int main(void) { pid_t pid; int status; // 1 if ((pid = fork()) < 0) fprintf(stderr, "fork error\n"); else if (pid == 0) // 子进程 exit(7); // 正常终止 if (wait(&status) != pid) // 等待子进程终止 fprintf(stderr, "wait error\n"); pr_exit(status); // 2 if ((pid = fork()) < 0) fprintf(stderr, "fork error\n"); else if (pid == 0) // 子进程 abort(); // 产生SIGABRT if (wait(&status) != pid) // 等待子进程终止 fprintf(stderr, "wait error\n"); pr_exit(status); // 3 if ((pid = fork()) < 0) fprintf(stderr, "fork error\n"); else if (pid == 0) // 子进程 status /= 0; // 除以0 if (wait(&status) != pid) // 等待子进程终止 fprintf(stderr, "wait error\n"); pr_exit(status); exit(0); } void pr_exit(int status) { /* 程序清单8-3代码 */ }
运行程序:
$ ./04 normal termination exit status = 7 abnormal termination, signal number = 6 (core file generated) abnormal termination, signal number = 8 (core file generated)
不幸的是,没有一种可移植的方法将WTERMSIG得到的信号编号映射为说明性的名字,必须查看<signal.h>头文件才能知道。
对于waitpid函数中pid参数的作用解释如下:
pid == -1 等待任一子进程。就这方面而言,waitpid和wait等效。
pid > 0 等待其进程ID与pid相等的子进程。
pid == 0 等待其组ID等于调用进程组ID的任一子进程。
pid < -1 等待进程组ID等于pid绝对值的任一子进程。
waitpid函数返回终止子进程的进程ID,并将该子进程的终止状态存放在由status指向的存储单元中。如果指定的进程或进程组不存在,或者参数pid指定的进程不是调用进程的子进程都将出错。
options参数使我们能进一步控制waitpid的操作。此参数可以是0,或者是如下常量按位“或”运算的结果。
WCONTINUED 若实现支持作业控制,那么由pid指定的任一子进程在暂停后已经继续,但其状态尚未报告,则返回其状态。
WNOHANG 若由pid指定的子进程并不是立即可用的,则waitpid不阻塞,此时其返回值为0。
WUNTRACED 若某实现支持作业控制,而有pid指定的任一子进程已处于暂停状态,并且其状态自暂停以来还未报告过,则返回其状态。WIFSTOPPED宏确定返回值是否对应于一个暂停子进程。
waitpid函数提供了wait函数没有提供的三个功能:
(1) waitpid可等待一个特定的进程,而wait则返回任一终止子进程的状态。
(2) waitpid提供了一个wait的非阻塞版本。有时用户希望取得一个子进程的状态,但不像阻塞。
(3) waitpid支持作业控制(利用WUNTRACED和WCONTINUED选项)。
《UNIX环境高级编程》P183:程序清单8-5 调用fork两次以避免僵死进程
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/wait.h> int main(void) { pid_t pid; if ((pid = fork()) < 0) { // 进程A fork,产生子进程B fprintf(stderr, "fork error\n"); } else if (pid == 0) { // 子进程B if ((pid = fork()) < 0) // 子进程B fork,产生孙子进程C fprintf(stderr, "fork error\n"); else if (pid > 0) // 子进程B在孙子进程C前终止 exit(0); // 孙子进程C /* * 由于父进程B在子进程C之前终止,该子进程C被init领养 * init会对该子进程C进行善后处理 */ sleep(2); // 使子孙子进程C休眠,让爷A、父B进程先走 printf("second child, parent pid = %d\n", getppid()); exit(0); } // 进程A执行区域 if (waitpid(pid, NULL, 0) != pid) // 进程A等待子进程B终止 fprintf(stderr, "waitpid error\n"); exit(0); // 进程A终止 }
如果一个进程fork一个子进程,但不要它等待子进程终止,也不希望子进程处于僵死状态直到父进程终止,实现这一要求的技巧时调用fork两次。(如果父进程终止了,其僵死的子进程也被清理了吗?)
在Ubuntu14.04 上执行该程序:
$ ./05 $ second child, parent pid = 1668
注意,当原先进程终止时,shell打印其提示符,这在第二个子进程打印其父进程ID之前。结果和书上所写不一致!!检查了好几次源代码,没发现问题。进程ID为1668为何许进程?
$ ps -aux | grep 1668 user 1668 0.0 0.1 40424 2544 ? Ss 19:37 0:00 init --user
难道是因为Ubuntu用Upstart替换了传统的init?搜到了两片相关文章:
维基百科:http://zh.wikipedia.org/wiki/Upstart;
某一网站:http://myip.tw/itsmw/index.php?title=Upstart
#include <sys/wait.h> int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options); // 返回值:若成则返回0,若出错则返回-1
waitid允许一个进程指定要等待的子进程。它使用单独的参数表示要等待的子进程类型。id参数的作用与idtype的值有关。该函数支持的idtype如下所示:
P_PID 等待一个特定的进程:id包含了要等待子进程的进程ID。
P_PGID 等待一个特定进程组中的任一子进程:id包含了要等待子进程的进程组ID。
P_ALL 等待任一子进程:忽略id
options参数是下列常量按位“或”。这些标志指示调用者关注哪些状态变化。
WCONTINUED 等待一个进程,它以前曾被暂停,此后又已继续,但其状态尚未报告。
WEXITED 等待已退出的进程。
WNOHANG 如无可用的子进程退出状态,立即返回而非阻塞。
WNOWAIT 不破坏子进程退出状态。该子进程退出状态可由后续的wait、waitid或waitpid调用取得。
WSTOPPED 等待一个进程,它已经暂停,但其状态尚未报告。
infop参数是指向siginfo结构的指针。该结构包含了有关引起子进程状态改变的生成信号的详细信息。
#include <sys/types.h> #include <sys/time.h> #include <sys/resource.h> #include <sys/wait.h> pid_t wait3(int *status, int options, struct rusage *rusage); pid_t wait4(pid_t pid, int *status, int options, struct rusage *rusage); // 两个函数的返回值:若成功则返回进程ID,若出错则返回-1
它们提供的功能比POSIX.1函数wait、waitpid和waitid所提供的功能要多一个,这与附加参数rusage有关。该参数要求内核返回由终止进程及其所有子进程使用的资源汇总。
资源统计信息包括用于CPU时间总量、系统CPU时间总量、页面出错次数、接收到信号的次数等。有关细节请参阅getrusage(2)手册页。