8.3 fork 函数
在 UNIX 系统中,一个现有进程可以调用 fork 函数创建一个新进程。调用 fork 函数的进程称为父进程,由 fork 创建的新进程被称为子进程(child process)。fork函数被调用一次但返回两次,两次返回的唯一区别是子进程中返回0值而父进程中返回子进程ID。首先看下fork函数的原型;
/* 创建进程 */
/* fork 函数 */
/*
* 函数功能:创建一个新的进程;
* 返回值:
* (1)在父进程中,返回新创建子进程的进程ID;
* (2)在子进程中,返回0;
* (3)若出错,则返回-1;
* 函数原型:
*/
#include <unistd.h>
pid_t fork(void);
fork 函数调用一次,返回两个值,在父进程中,返回新建子进程的进程ID,因为一个父进程可能有多个子进程,没有获取子进程ID的函数,所以返回子进程的进程ID;在子进程中,返回0,因为子进程的父进程ID可以通过函数 getppid 获取;子进程是父进程的副本,它将获得父进程数据空间、堆、栈等资源的副本(UNIX 系统是采用写时复制),意味着父子进程间不共享这些存储空间。UNIX将复制父进程的地址空间内容给子进程,因此,子进程有了独立的地址空间。fork 之后父进程和子进程的执行顺序是不确定的,根据内核所使用的进程调度算法来执行。
下面先看一个比较简单的例子:
#include "apue.h"
#include <unistd.h>
int main(void)
{
pid_t pid;
int count = 0;
pid = fork();//创建一个子进程;
if(pid < 0)
{
err_sys("Created child process error.\n");
exit(-1);
}
else if(0 == pid)//在子进程中,返回0
{
printf("I am back in the child process, my ID: %d and my parent ID: %d\n",getpid(),getppid());
printf("the count is: %d\n",++count);
}
else //在父进程中,返回新建子进程的ID
{
printf("I am back in the parent process, my ID: %d and my parent ID: %d\n",pid,getpid());
printf("the count is: %d\n",++count);
}
exit(1);
}
输出结果:
I am back in the parent process, my ID: 10684 and my parent ID: 10683
the count is: 1
I am back in the child process, my ID: 10684 and my parent ID: 10683
the count is: 1
从输出结果我们可以知道,调用一次fork函数,会返回两次,根据程序的输出,返回之后是先执行父进程,在父进程中,返回值是新建子进程的进程ID,所以此时pid的值即为新建子进程的进程ID,count的值增加1;当父进程结束后,调用子进程,在子进程中返回值是 pid=0,所以想要获取新建子进程的进程ID需要使用 getpid函数(相当于获取当前进程的进程ID),注意:count的值依然为1,因为父进程和子进程的数据空间是独立的,所以父子进程的数据不相互共享,count的初始值都还是0;
接着看第二个例子:
#include <unistd.h>
#include <stdlib.h>
#include "apue.h"
int main(void)
{
pid_t pid;
int count = 0;
printf("before fork,enter\n"); //有换行符只输出一次
printf("before fork,no enter:pid=%d",getpid()); //没有换行符,输出两次
pid = fork();
if(pid < 0)
{
err_sys("Created child process error.\n");
exit(-1);
}
else if(0 == pid)//在子进程中,返回0
{
printf("\n");
printf("I am back in the child process, my ID: %d and my parent ID: %d\n",getpid(),getppid());
printf("the count is: %d\n",++count);
printf("\n");
}
else //在父进程中,返回新建子进程的ID
{
printf("\n");
printf("I am back in the parent process, my ID: %d and my parent ID: %d\n",pid,getpid());
printf("the count is: %d\n",++count);
printf("\n");
}
exit(1);
}
输出结果:
before fork,enter
before fork,no enter:pid=11384
I am back in the parent process, my ID: 11385 and my parent ID: 11384
the count is: 1
before fork,no enter:pid=11384
I am back in the child process, my ID: 11385 and my parent ID: 11384
the count is: 1
从结果可以看到,printf("before fork,enter\n"); 有换行符的 printf语句输出一次,就是说只在一个进程中输出;
printf("before fork,no enter:pid=%d",getpid()); 没有换行符的 printf 语句输出二次,就是说在每一个进程都输出,这里只有两个进程,所以输出两次,多个进程会输出多次;
出现以上 printf 输出不同的原因很简单,因为 printf 是把数据存储在缓冲区中,在缓冲队列等待输出,若遇到换行符(行缓冲)或者刷新缓冲区时,会直接打印到屏幕。所以第一个语句带有换行符时是直接把数据打印到屏幕,不再缓冲队列等待输出,因此,子进程复制父进程的数据段时,stdout输出缓冲区并不存在数据,则只在父进程输出一次;没有换行符的 printf 函数把数据保存在缓冲区队列中,子进程把该数据复制,因此,父、子进程各自输出一次。
8.4 vfork 函数
vfork 函数也是在现有的进程上创建子进程,而该新进程的目的是exec一个新程序。基本操作和fork 函数类似,与 fork 的区别如下:
(1)fork 的子进程的数据是复制父进程的数据,即数据独立;
(2)vfork 的子进程和父进程共享数据;
(3)fork 子进程和父进程的执行顺序根据内核进程调度算法决定;
(4)vfork 的执行顺序是,先执行子进程,再执行父进程;
例如例子1使用 vfork 创建子进程时,输出结果跟上面的不一样;
#include "apue.h"
#include <unistd.h>
int main(void)
{
pid_t pid;
int count = 0;
pid = vfork();//创建一个子进程;
if(pid < 0)
{
err_sys("Created child process error.\n");
exit(-1);
}
else if(0 == pid)//在子进程中,返回0
{
printf("I am back in the child process, my ID: %d and my parent ID: %d\n",getpid(),getppid());
printf("the count is: %d\n",++count);
}
else //在父进程中,返回新建子进程的ID
{
printf("I am back in the parent process, my ID: %d and my parent ID: %d\n",pid,getpid());
printf("the count is: %d\n",++count);
}
exit(1);
}
输出结果:
I am back in the child process, my ID: 12171 and my parent ID: 12170
the count is: 1
I am back in the parent process, my ID: 12171 and my parent ID: 12170
the count is: 2
从结果中可以知道, vfork 函数创建的子进程是和父进程共享数据的,看 count 值的输出就知道,并且是先执行子进程,再执行父进程。
8.5 exit函数
在7.3节中介绍了进程退出的八种方式,分别为:
1:从 main 函数返回
2:调用 exit 函数
3:调用 _exit 函数或者是 _Exit 函数
4:从最后一个线程中返回
5:从最后一个线程中调用 pthread_exit
还有三种非正常的结束方式:
6:调用 abort
7:接收到信号
8:应答最后一个线程的取消请求
以下就各个函数做比较详细的解释:
1:执行return从main函数返回等同于调用exit函数。
2:调用exit函数,这个函数有ISO C所定义,包括调用调用所有的被atexit注册过的退出处理程序和关闭所有的标准I/O流。因为ISO C不会处理文件描述符、多进程、作业控制,所以在Unix系统中,这个函数的定义有些不完整。
3:ISO C定义了一个_Exit函数来提供进程结束时,不用执行退出处理程序和信号处理 程序。在Unix系统中,_Exit和_exit同义,都不用 刷新标准I/O流 。 _exit函数由POSIX.1所定义。
在大多数 Unix系统中exit函数是标准的C库函数,_exit是系统调用。
4:线程的返回值不作为进程的返回值,当最后一个线程从例程返回时,进程结束时的结束状态值为0。
5:和4中的一样,这种情况下同样返回0,与传递到pthread_exit函数中的参数无关。
不管一个进程如何结束,内核都会为这个结束的进程关闭所有的打开的件描述符,释放所利用的内存等。
如果我们想要结束的进程通知父进程自己是如何结束的,对于exit 、 _Exit和_exit ,通过传递退出状态值,作为函数的参数来实现。
对于非正常的结束方式,内核(而不是结束进程),产生一个指示其异常终止原因的终止状态。在任意一种情况下,该进程的父进程可以通过 wait和waitpid函数 来获取结束进程的终止状态值。
二:为什么任何一个进程都有父进程
当调用一个fork函数后,子进程就会有一个父进程。当父进程先于子进程结束时,init进程会成为这个子进程的父进程。
具体的实现如下,通常情况下,在一个进程结束时,内核会遍历所有的活动进程,看是否有结束进程的子进程。如果有子进程,则把该子进程的父进程的ID修改为1(init进程的PID),这样保证了所以的进程都有父进程。
三:僵死进程的产生
在子进程先于父进程结束的情况下,当父进程要检查一个子进程是否结束时,子进程完全消失了,这时父进程不能取得结束子进程的结束状态信息。
在一个进程结束时,内核会保存保存每个结束进程的一定量信息。这些信息最少包含,进程ID,终止状态,进程使用的CPU时间。当父进程调用wait或者是waitpid时,可以得到这些结束信息。在Unix系统术语中,如果一个进程结束了,但是其父进程尚未对其进行善后处理(获取终止子进程的有关信息,释放它仍占用的资源)的进程被称为僵死进程。
如果写一个长时间运行的程序,在这个程序中fork了很多子进程,除非父进程等待取得子进程的终止状态,否则这些子进程就会变成僵死进程。
如果一个被init 进程领养的进程终止时他是否会成为僵死进程,答案是否定的。因为init的任何一个子进程结束时,init会调用wait函数来取得结束状态值。从而避免僵死进程的产生。从上可以看出,在 子进程先于父进程 结束,而父进程没有wait这个子进程才会产生僵死进程。
8.6 wait 函数和 waitpid 函数
当一个进程正常或异常终止时,内核会向其父进程发送 SIGCHLD 信号。父进程可以为这个信号提供一个信号处理程序,也可以选择忽略,系统对这种信号默认是忽略。当进程调用 wait 或 waitpid 函数可能会发生以下的情况:
(1)如果其所有子进程都还在运行,则发生阻塞。
(2)如果一个子进程已经终止,正在等待父进程获取其终止状态,则取得该子进程的终止状态立即返回。
(3)如果没有任何子进程,则立即出错返回。
注意:如果进程是由于接收到 SIGCHLD信号而调用 wait 函数,则可期望 wait 立即返回;但是如果是在任意时刻调用 wait,则进程可能会发生阻塞。
如果一个子进程已经终止,并且是一个僵尸进程,则wait立即返回并取得该子进程的状态,否则wait使其调用者阻塞直到一个子进程终止。如果调用者阻塞而且它有多个子进程,则在其一个子进程终止时,wait就立即返回。因为wait返回终止子进程的进程ID,所以它总能了解是哪一个子进程终止了。
/* 等待进程终止 */
/*
* 函数功能:等待一个进程终止;
* 返回值:若成功则返回进程ID、0;若出错则返回-1;
* 函数原型:
*/
#include <sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int option);
/*
* 说明:
* 参数statloc是指向存放终止进程的终止状态单元的指针;如果不在意这些信息,则可设为NULL;
* pid和option参数是控制waitpid操作;
*/
/*
* pid参数及作用如下
* pid == -1; 等待任一子进程,此时waitpid和wait功能一样;
* pid > 0; 等待其进程ID与pid相等的子进程;
* pid == 0; 等待其组ID与调用进程组ID相等的任一子进程;
* pid < -1; 等待其组ID与pid绝对值相等的任一子进程;
*/
/*
* 参数option可以是0,也可以是以下参数按位"或"组成:
* WCONTINUED 若实现支持作业控制,由pid指定的任一子进程在暂停后已经继续,但其状态尚未报告,则返回其状态;
* WNOHANG 若由pid指定的子进程并不是立即可用的,则waitpid不阻塞,此时其返回值0;
* WUNTRACED 若某实现支持作业控制,而由pid指定的任一子进程已处于暂停状态,并且其状态自暂停以来还未报告,则返回其状态;
*/
这两个函数的区别如下:
(1)在一个子进程终止之前,wait函数使其调用者阻塞,而 waitpid 有一个选项 option,根据选项的不同值可使其调用者不发生阻塞。
(2)waitpid 并不一定要等待在其调用后的第一个子进程终止,根据不同选项,可以控制自己感兴趣要等待的进程。
waitpid 提供了 wait 没有提供的三个功能:
(1)waitpid 可以等待一个特定的子进程,而wait 则返回任一终止子进程的状态;
(2)waitpid 提供一个非阻塞的 wait 版本;
(3)waitpid 支持作业控制;
可以用以下宏查看进程的终止状态:
/* 检查 wait 和 waitpid 所返回的终止状态的宏.....
* WIFEXITED(status) 若为正常终止子进程返回的状态,则为真;对于这种情况可执行WEXITSTATUS(status), 取子进程传给exit,_exit或_Exit参数的低8位;
*
* WIFSIGNALED(status) 若为异常终止子进程返回的状态,则为真;对于这种情况可以执行WTERMSIG(status),取使子进程终止的信号编号;另外,有些实现定义宏WCONREDUMP(status),若已产生终止进程的core文件,则返回真;
*
* WIFSTOPPED(status) 若为当前暂停子进程的返回状态,则为真;对于这种情况可执行WSTOPSIG(status),取使子进程暂停的信号编号;
*
* WIFCONTINUED(status) 若在作业控制暂停后已经继续的子进程返回的状态,则为真;
*
*/
测试程序:
#include <sys/wait.h>
#include "apue.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),
#ifdef WCOREDUMP
WCOREDUMP(status) ? "(core file generated)" : " ");
#else
" ");
#endif
else if(WIFSTOPPED(status))
printf("child stoped, signal number = %d\n", WSTOPSIG(status));
}
int main(void)
{
pid_t pid;
int status;
if((pid = fork()) < 0)
err_sys("fork error");
else if(0 == pid) /* in child process */
exit(7);
if(wait(&status) != pid) /* wait for child process */
err_sys("wait error");
pr_exit(status); /* print its status */
if((pid = fork()) < 0)
err_sys("fork error");
else if(0 == pid) /* in child process */
abort(); /* generated SIGABRT */
if(wait(&status) != pid) /* wait for child process */
err_sys("wait error");
pr_exit(status); /* print its status */
if((pid = fork()) < 0)
err_sys("fork error");
else if(0 == pid) /* in child process */
status /= 0; /* divide by 0 generated SIGFPE */
if(wait(&status) != pid) /* wait for child process */
err_sys("wait error");
pr_exit(status); /* print its status */
exit(0);
}
输出结果:
normal termination, exit status = 7
abnormal termination, signal number = 6(core file generated)
abnormal termination, signal number = 8(core file generated)
输出的结果只是对不同状态的打印,即在正常终止子进程、异常终止子进程、当前暂停子进程的状态。从打印信息可以看到,该子进程发生正常终止和异常终止。
程序2:
#include <sys/wait.h>
#include "apue.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),
#ifdef WCOREDUMP
WCOREDUMP(status) ? "(core file generated)" : " ");
#else
" ");
#endif
else if(WIFSTOPPED(status))
printf("child stoped, signal number = %d\n", WSTOPSIG(status));
}
int main(void)
{
pid_t pid,fpid;
int status;
if((pid = fork()) < 0)
err_sys("fork error");
else if(0 == pid) /* in child process */
{
printf("child process, sleep 5s.\n");
sleep(5);
printf("child process, normal exit.\n");
exit(0);
}
else
{
fpid = wait(&status);/* wait for child process */
if(fpid < 0)
err_sys("wait error");
printf("father process, child process ID: %d\n",fpid);
pr_exit(status); /* print its status */
}
exit(0);
}
以上程序是在现有进程中fork 创建一个子进程,在 fork 返回的子进程中先睡眠5秒,然后正常退出;在 fork 返回的父进程中,wait 等待子进程终止,然后打印子进程终止状态信息,最后正常退出。输出结果如下:
child process, sleep 5s.
child process, normal exit.
father process, child process ID: 6065
normal termination, exit status = 0
8.7 waitid 函数
该函数类似于上面的 waitpid 函数功能,也是允许一个进程指定要等待的子进程,但是 waitid 使用单独的参数表示要等待的子进程的类型,而不是将此与进程 ID 或进程组 ID 组合成一个参数。
程序测试:
#include "apue.h"
#include <sys/wait.h>
#include <unistd.h>
int main(void)
{
pid_t pid, fpid;
siginfo_t info;
if((pid = fork()) < 0)
err_sys("fork error.");
else if(pid == 0)
{
sleep(5);
_exit(0);
}
while((fpid = waitid(P_PID,pid,&info,WEXITED)) == 0)
{
printf("terminated process success exit,and process ID: %d.\n",pid);
sleep(1);
}
exit(0);
}
该程序是在现有进程使用 fork 创建一个子进程,在fork 返回的子进程中睡眠 5s 后正常退出;在 fork 返回的父进程调用 waitid 函数等待与子进程的进程 ID 与 pid 相等子进程退出状态,waiti 成功则返回0,打印一条信息;输出结果如下:
terminated process success exit,and process ID: 6589.
8.8 wait3 和 wait4 函数
函数 wait3 和 wait4 提供的功能比函数 wait,waitpid 和 waitid 所提供的功能要多一个,这与附加参数 rusage 有关,该参数要求内核返回由终止进程及其所有子进程使用的资源汇总。
/*
* wait3 和 wait4 函数
* 函数功能:等待一个进程终止;
* 返回值:若成功则返回进程ID,若出错则返回-1;
* 函数原型:
*/
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <sys/resource.h>
pid_t wait3(int *statloc, int options, struct rusage *rusage);
pid_t wait4(pid_t pid, int *statloc, int options, struct rusage *rusage);