fork的意思是复制进程,就是把当前的程序再加载一次,加载后,所有的状态和当前进程是一样的(包括变量)。fork不象线程需提供一个函数做为入口, fork后,新进程的入口就在 fork的下一条语句。
返回值为pid_t,实际是unsigned int
:子进程中为0,父进程中为子进程ID,出错则为-1。
开始写了几个都不成功,最后终于成功了:
pid_t p = getpid(); // 需要 #include
printf("before fork(), pid = %d\n\n", p);
int n=0;
pid_t pid=-1;
while(n<5 && pid!=0)
{
pid = fork();
if(getpid()!=p) //是创建的子进程,不是原进程
{
printf("child pid:%d ",getpid());
printf("parent pid:%d\n",getppid());
printf("\n\n");
//sleep(2);
}
waitpid(pid,NULL,0); //父进程等待子进程执行后,才能继续fork其他子进程
n++;
}
网上抄来抄去的一个程序是这样的,逻辑大致相同:
pid_t p1;
for(i=0;i<4;i++)
{
if((p1=fork())==0)
{
printf("child pid %d ",getpid());
return 0; //非常关键
}
waitpid(p1,NULL,0);
printf("parent pid %d \n\n\n",getpid());
}
两种进程都属于子进程回收的范畴。
代码里不加waitpid
这句,导致出现了这样的结果:
孤儿进程:pid为1305
的进程是systemd
,因为如果父进程早于子进程终结,子进程就会成为一个孤儿进程。孤儿进程会被过继给systemd
进程,systemd进程也就成了该进程的父进程。systemd进程负责该子进程终结时调用wait函数。
如果代码修改如下:
while(n<5 && pid!=0)
{
pid = fork();
if(getpid()!=p) //子进程
{
printf("child pid:%d ",getpid());
sleep(5);
printf("parent pid:%d\n",getppid());
printf("\n\n");
}
else{
while(1)
sleep(2);
}
n++;
}
运行之后产生的子进程ID为2514,用ps命令查看的结果:
其中的Z+
表示进程为僵尸进程。
僵尸进程:正常情况下,子进程死后,系统会发送SIGCHLD
信号给父进程,父进程对其默认处理是忽略。如果想响应这个消息,父进程通常使用wait函数来响应子进程的终止。
如果子进程在其父进程还没有调用wait()或waitpid()的情况下退出,导致父进程无法对其进行回收,子进程就会成为僵尸进程,其残留资源(PCB)存放于内核,不能用kill命令终结。僵尸进程会占用系统资源,如果很多,则会严重影响服务器的性能。如果父进程到死也没有执行wait
,那么子进程最终过继给init
进程,init进程周期执行wait系统调用收割其收养的所有僵尸进程。
两个函数的原型:
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);
当子进程终结时,它会通知父进程,并清空自己所占据的内存,并在内核里留下自己的退出信息(exit code,正常则为0;如果有错误或异常状况,为大于0的整数)。在这个信息里,会解释该进程为什么退出。父进程在得知子进程终结时,应当对该子进程使用wait函数调用,然后就会阻塞自己,由wait分析子进程是否已经退出,如果子进程成为僵尸进程,wait收集其信息并将其销毁。如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。
如果wait执行成功则返回子进程PID,如果有错误发生则返回-1,失败原因存于errno 中。wait的参数为int指针status,用来获取子进程的退出信息,如果我们只要销毁僵尸进程而不管退出信息,就将指针设定为NULL。获取退出状态不能用常规方法,需要用到几种宏,常用的一种方法如下:
int status;
ret = wait(&status);
if(WIFEXITED(status)) /* 如果WIFEXITED 返回非零值 */
{
printf("the child process %d exit normally.\n", ret);
printf("the return code is %d.\n",WEXITSTATUS(status));
}
else /* 如果WIFEXITED返回零 */
printf("the child process %d exit abnormally.\n", ret);
如果参数status的值不是NULL,wait就会把子进程退出时的状态exit code
取出并存入其中。
WIFEXITED(status)
的意思是Wait if exited
,用来指出子进程是否为正常退出的,如果是,它会返回一个非零值。
当WIFEXITED
返回非零值时,可以用宏WEXITSTATUS(status)
来提取子进程的返回值,意思是wait exit status
,如果子进程调用exit(5)退出,WEXITSTATUS(status) 就会返回5。
所以说,wait和waitpid有三个作用:
1. 阻塞自身等待子进程退出
2. 回收子进程残余资源
3. 获取子进程退出信息
除了wait
函数外,还有多种方法避免僵尸进程,但常用的是两种。
原理:当子进程成为僵尸进程后,如果父进程结束,那么僵尸进程会过继给init
进程,最终被init
进程回收。所以可以人为地让父进程结束,这样子进程就不会变为僵尸,说白了就是让僵尸进程先成为孤儿进程,然后交给init
。
APUE的代码:
pid_t pid;
printf("before fork, pid is %d\n",getpid());
sleep(2);
if ( (pid = fork()) < 0)
perror("fork error");
else if (pid == 0) /* first child */
{
if ( (pid = fork()) < 0)
perror("fork error");
else if (pid > 0) /* parent from second fork == first child */
{
printf("first child pid is %d\n",getpid());
exit(0);
}
/* We're the second child; our parent becomes init as soon
as our real parent calls exit() in the statement above.
Here's where we'd continue executing, knowing that when
we're done, init will reap our status. */
sleep(2);
printf("second child, pid = %d, parent pid = %d\n", getpid(),getppid());
exit(0);
}
if (waitpid(pid, NULL, 0) != pid) /* wait for first child */
perror("waitpid error");
/* We're the parent (the original process); we continue executing,
knowing that we're not the parent of the second child. */
exit(0);
人为地创建一个子进程,再创建一个孙子进程,子进程退出,实际处理事务的是孙子进程,这个时候孙子进程成为孤儿进程,被系统进程init回收,而init进程的子进程不会成为僵尸进程。