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