在多道程序环境中,进程是并行执行的,父进程与子进程可能没有交集,各自独立执行;但也有可能,子进程的执行结果是父进程的下一步操作的先决条件,此时父进程就必须等待子进程的执行。
我们把异步环境下的一组并发进程因相互制约而相互发送消息、互相合作、互相等待,使得各进程按一定速度和顺序执行称为进程间的同步。
使用sleep()函数来控制进程的执行顺序,但这种方法只是一种权宜之计。系统中进程的执行顺序是由内核决定的,使用这种方法很难做到对进程的精确控制。
Linux系统中提供了wait()函数和waitpid()函数来获取进程状态,实现进程同步。
1.wait()函数存在系统库函数sys/wait.h中,函数声明如下:
pid_t wait(int *status);
调用wait()函数的进程会被挂起,进入·阻塞状态,直到子进程变为僵尸态,wait()函数捕获到该子进程的退出信息时才会转为运行态,回收子进程资源并返回。
若没有变为僵尸态的子进程,wait函数会让进程一直阻塞。若当前进程有多个子进程,只要捕获到一个变为僵尸态的子进程的信息,wait()函数就会返回并使进程恢复执行。
函数中的参数status是一个int*类型的指针,它用来保存子进程退出时的状态信息。但通常,我们只想消灭僵尸进程,不在意子进程如何终止,因此一般将该参数设为NULL。
若wait()函数调用成功,wait()会返回子进程的进程id;若调用失败,wait()返回-1,errno被设为
置为ECHILD
案例6-4:若子进程p1是其父进程p的先决进程,使用wait()函数使进程同步。
#include <stdio.h> #include <stdlib.h> #include <sys/wait.h> int main() { pid_t pid,w; pid = fork(); if(pid == -1) { perror("fork error"); exit(1); } else if(pid == 0) { sleep(3); printf("Child process : pid =%d\n",getpid()); } else if(pid > 0) { //若wait()函数调用成功,wait()会返回子进程的进程id w=wait(NULL); printf("Catched a child process,pid=%d\n",w); } return 0; } ~
执行结果如下:
以上结果在执行程序3秒后输出,因为代码第13行使用sleep()函数使子进程沉睡3秒后才执行。观察程序的执行情况:子进程在程序执行3秒后完成并输出子进程pid;因为父进程中执行的操作只是回收子进程,所以父进程在子进程终止后立即输出。由执行情况可以知道,父进程在子进程结束后才结束,父进程成功捕获了子进程。
当然,wait()函数中的参数可以不为空。若status不为空,wait()函数会获取子进程的退出状态,退出状态被存放在exit()函数参数status的低八位。使用常规方法读取比较麻烦,因此Linux系统中定义了一组用于判断进程退出状态的宏函数,其中最基础的是WIFEXITED()和WEXITSTATUS(),它们
的参数与wait()函数相同,都是一个整型的status。宏函数的功能分别如下:
①WIFEXITED(status):用于判断子程序是否正常退出,若是,则返回非0值;否则返回0
②WEXITSTATUS(status):WEXITSTATUS()通常与WIFEXITED()结合使用,若WIFEXITED返回非0值(即正常退出),则使用该宏可以提取子进程的返回值。
案例6-5:使用wait()函数同步进程,并使用宏获取子进程的返回值。
#include <stdio.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
int status;
pid_t pid,w;
pid = fork();
if(pid == -1)
{
perror("fork error");
exit(1);
}
else if(pid == 0)
{
sleep(3);
printf("Child process:pid = %d\n ",getpid());
exit(5);
}
else if(pid>0)
{
w=wait(&status);
if(WIFEXITED(status))
{
printf("Child process pid=%d exit normally.\n",w);
printf("Return Code:%d\n",WEXITSTATUS(status));
}
else
printf("Child process pid =%d exit abnormally.\n",w);
}
return 0;
}
执行结果如下:
定义了一个整型变量status,该变量在wait()函数中获取子进程的退出码。之后通过宏WIFEXITED判断返回值是否为0,当不为0时,使用宏WEXITSTATUS将返回码转换成一个整数数据
2.waitpid()函数
wait()函数具有一定局限性,若当前进程有多个子进程,那么该函数就无法确保作为先决条件的子进程在父进程之前执行,此时可使用waitpid()函数实现进程同步。
waitpid()函数同样位于系统库函数sys/wait.h中,它的函数声明如下:
pid_t waitpid(pid_t pid,int *status,int options);
waitpid()函数比wait()函数多两个参数:pid 和options。
参数pid一般是进程的pid,但也会有其他取值。参数pid的取值及其意义分别如下:
①pid=-1时,waitpid()函数与wait()作用相同,将阻塞等待并回收一个子进程
②pid>0时,只等待pid与该参数相同的子进程,若该子进程退出,waitpid()函数就会返回;若
该子进程仍未结束,waitpid()函数就一直等待该进程。
③pid=0时,会等待同一个进程组的所有子进程,若子进程加入了其他进程组,waitpid()将不再关心它的状态。
④pid<-1时,会等待同一个进程组中的任何子进程,进程组的id等于pid的绝对值。
参数options提供控制waitpid()选项,该选项是一个常量或由 | 连接的两个常量。该参数支持的选项如下:
①WNOHANG。即使子进程没有终止,waitpid()也会立即返回,即不会使父进程阻塞。
②WUNTRACED.即使子进程暂停执行,则waitpid()立刻返回。
另外若不想使用该参数,可以将其值设置为0.
waitpid()函数的返回值会出现3种情况:
①正常返回时,waitpid()返回捕捉到的子进程的pid
②若options的值为WNOHANG,但调用waitpid()时发现没有已退出的子进程可收集,则返回0.
③若调用过程出错,则返回-1,errno会被设置成相应的值以指示错误位置。
waitpid()函数可以等待指定的子进程,也可以在父进程不阻塞的情况下获取子进程状态,相对于
wait函数来说,它的使用更为灵活。下面通过两个案例来学习waitpid()函数的用法。
案例6-6:使父进程等待进程组中某个指定的进程,若该进程不退出,则让父进程一直阻塞。
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
pid_t pid,p,w;
pid = fork();
if(pid == -1)
{
perror("fork error");
exit(1);
}
else if(pid == 0)
{
sleep(5);
printf("First child process:pid=%d\n",getpid());
}
else if(pid > 0)
{
int i;
p=pid;
for(i=0;i<3;i++)
{
if((pid = fork())==0)
break;
}
if(pid == -1)
{
perror("fork error");
exit(2);
}
else if(pid == 0)
{
printf("Child process : pid=%d\n",getpid());
exit(0);
}
else if(pid > 0)
{
w=waitpid(p,NULL,0);//传入子进程的pid,第二个参数不设置,不使用第三个参数可设置为0
if(w==p)//正常返回时,waitpid()返回捕捉到的子进程的pid
printf("Catch a child process:pid=%d\n",w);
else
printf("waitpid error\n");
}
}
return 0;
}
执行结果如下:
CPU执行速度极高,执行程序后可看到执行结果中的前3行会立刻被输出;结果中的第4行在5秒后输出,因为第13行代码要求第一次创建子进程时睡眠5秒;执行结果的第五行为父进程执行的操作,当第一个子进程终止后,此行立刻被输出。
案例6-7:使用waitpid()函数不断获取某进程中子进程的状态
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
pid_t pid,w;
pid = fork();
if(pid == -1)
{
perror("fork error");
exit(1);
}
else if(pid == 0)
{
sleep(3);
printf("Child process:pid =%d\n",getpid());
exit(0);
}
else if(pid > 0)
{
do{
w=waitpid(pid,NULL,WNOHANG);
if(w==0)
{
printf("No child exited\n");
sleep(1);
}
}while(w==0);
if(w==pid)
printf("Catch a child process:pid=%d\n",w);
else
printf("waitpid error\n");
}
return 0;
}
执行结果如下:
在父进程的代码段中设置了一个循环,在循环中调用waitpid()函数,并使用sleep()函数控制waitpid(),使其每隔1秒捕捉一次子进程信息;同时使子进程沉睡3秒,因此父进程会输出3次No
child exited。3秒后子进程终止,waitpid()成功捕获到子进程的退出信息并使父进程继续运行,从而输出捕捉到的子进程id。
多学一招:
僵尸进程不能再次被运行,但是却会占据一定的内存空间:当系统中僵尸进程的数量很多时,不光会占用进程id。若僵尸进程一直存在,新的进程可能会因内存不足或一直无法获取pid而无法被创建。因此,应尽量避免僵尸进程的产生,使用wait()和waitpid()可以有效避免僵尸进程。
若僵尸进程已经产生,就应该想办法终止僵尸进程。通常情况下,解决僵尸进程的方法是终止其父进程。当僵尸进程的父进程被终止后,僵尸进程作为孤儿进程被init接收,init会不断调用wait()函数获取子进程状态,收集已退出的子进程发送的状态信息。孤儿进程永远不会成为僵尸进程。