学科分类
目录
Linux编程

进程同步

在多道程序环境中,进程是并行执行的,父进程与子进程可能没有交集,各自独立执行;但也有可能,子进程的执行结果是父进程的下一步操作的先决条件,此时,父进程就必须等待子进程的执行。我们把异步环境下的一组并发进程,因相互制约而互相发送消息、进行互相合作、互相等待,使得各进程按一定的速度执行的过程称为进程间的同步。

前文中使用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()函数获取子进程状态,获取已退出的子进程发送的状态信息。孤儿进程永远不会成为僵尸进程。

点击此处
隐藏目录