多进程编程之概述

1、进程简介
    每一个进程都有一个非负整型表示的唯一进程ID。ID为0的进程通常是调度进程,被称为交换进ß程(swapper),也被称为系统进程。进程id为1通常是init进程。

2、函数fork

    一个进程,包括代码、数据和分配给进程的资源。fork()函数通过系统调用创建一个与原来进程几乎完全相同(pid ppid 和某些资源量和统计量等不同)的进程。

    fork创建子进程时继承了父进程的数据段、代码段、栈、堆,注意从父进程继承来的是虚拟地址空间,同时页复制了页表(没有复制物理块)。因此,此时父子进程拥有相同的虚拟地址,映射的物理内存也是一致的。由于父进程和子进程共享物理页面,内核将其标记为“只读”,父子双方均无法对其修改。无论父进程还是子进程试图对一个共享页面执行写操作,就会产生一个错误,这时内核就把这个页复制到一个新的页面给这个进程,并标记“可写”,同时修改页表,把原来的只读页面标记为“可写”,留给另外一个进程使用-------写时拷贝技术。注意:内核在为子进程分配物理内存时,并没有将代码段对应的数据另外复制一份给子进程,最终父子进程代码段映射的是同一块物理内存(代码段在单个进程内部本来就是只读的)。fork()的实际开销就是复制父进程的也表以及给子进程创建唯一的进程描述符。

    fork()函数调用一次,返回两次,它有三种不同的返回值:通过返回值来判断当前进程是子进程还是父进程。

         1)在父进程中,fork函数返回新建子进程的id;

         2)在子进程中,fork返回0;

         3)如果出现错误,fork返回一个负值。  

1 #include <unistd>
2 
3 pid_t fork(void);
4                 返回值:子进程返回0,父进程返回子进程ID;若出错,返回负1。
    fork出错可能有两种原因:
        1)当前的进程数已经达到了系统规定的上限;(PID的最大值默认设置为32768,这就是系统中允许同时存在的进程的最大数目,可以通过修改/proc/sys/kernel/pid_max来提高上限)
        2)系统内存不足;
   
 1 #include <stdio.h>
 2 #include <unistd.h>
 3 
 4 int main(int argc, char * argv)
 5 {
 6     fork();
 7     fork() && fork() || fork();
 8     fork();
 9     return 0;
10 }
 
     fork函数的用途:
        1)一个进程希望复制自身,从而父子进程能同时执行不同段的代码;

        2)进程想执行另外一个程序。

 1 /*知识点*/
 2   
 3 #include <unistd.h>
 4 #include <stdio.h>
 5   
 6 int main()
 7 {
 8         int var;
 9         pid_t pid;
10         var = 88;
11         char buf[] = "this is a test\n";
12   
13         write(STDOUT_FILENO, buf, sizeof(buf) - 1);
14         printf("1111111111111111111\n");
15         if ((pid = fork()) < 0) {
16                 printf("fork error");
17         }else if (pid == 0){
18                 var++;
19         }else {
20                 sleep(2);
21         }
22         printf("--------------pid = %d, var = %d\n", pid, var);
23         return 0;
24 }
25   
26 执行./a.out的输出结果是:
27 this is a test
28 1111111111111111111
29 --------------pid = 0, var = 89
30 --------------pid = 878, var = 88
31   
32 执行./a.out > test,cat test的结果是:
33 this is a test
34 1111111111111111111
35 --------------pid = 0, var = 89
36 1111111111111111111
37 --------------pid = 895, var = 88
38   
39 /*总结:write函数是不带缓冲的,fork之前调用,只写到标准输出一次;而标准I/O库是带缓冲的,如果标准输出连到终端设备,则它是行缓冲,否则它是全缓冲。
当(./a.out)运行该程序时,只得到printf输出一次,其原因是标准输出缓冲区由换行符冲洗。当将标准输出重定向到一个文件时,得到printf输出两次。
其原因是在fork之前调用了printf一次,但当调用fork时,该数据仍在缓冲区中,然后在将父进程数据空间复制到子进程中时,该缓冲区数据也被复制到子进程中。
*/

 

3、函数vfork
    用vfork创建的进程主要目的是用exec函数执行另外的程序,与fork的第二个用途相同。
    vfork与fork的区别:
    1)fork要拷贝父进程的数据段;而vfork则不需要完全拷贝父进程的数据段,在子进程没有调用exec和exit之前,子进程与父进程共享数据段;
    2)fork不对父子进程的执行次序进行任何限制;而在vfork调用中,子进程先运行,父进程挂起,直到子进程调用了exec或exit之后,父子进程的执行次序不再有限制。
 1 #include <stdio.h>
 2 #include <unistd.h>
 3 #include <stdlib.h>
 4 int main()
 5 {
 6      int var = 8;
 7     pid_t pid;
 8     printf("before vfork\n");
 9     if((pid = vfork()) < 0){
10         err_sys("vfork error");
11     }else if (pid == 0){
12          var++;
13          _exit(0);
14     }
15     printf("pid = %d  var = %d\n",getpid(), var);
16     exit(0);
17 }
4、函数exit
    exit()通常是用在子进程中用来结束程序的,使程序自动结束,跳回操作系统。
    exit(0)表示程序正常退出,exit(1)/exit(-1)表示程序异常退出。
    如果程序以main函数返回一个值结束,那么其效果相当于用这个值作为参数调用exit()函数。return和exit的区别就是即使在除main()函数之外的函数调用exit(),也能终止程序。
 
    (1)进程正常终止:

            a. 在main函数中执行return,等效于在main函数中调用exit()函数;

            b.调用exit()函数。此函数由ISO C定义,其操作包括调用各种终止处理程序,然后关闭所有的I/O流等。

            c.调用_exit或_Exit函数。ISOC定义_Exit,其目的是提供一种无需运行终止处理程序或信号处理程序而终止的方法。在UNIX系统中,这两个函数并不冲洗I/O流。_exit函数是由exit调用。

            d.进程的最后一个线程在其启动例程中执行return语句。但是该线程的返回值不用作进程的返回值。当最后一个线程从其启动例程返回时,该进程以终止状态0返回。

            e.进程的最后一个线程调用pthread_exit函数。同前面一样,该线程的返回值不用作进程的返回值。当最后一个线程从其启动例程返回时,该进程以终止状态0返回。

  (2)进程异常终止:

                a.调用abort函数;(abort函数是一个比较严重的函数,当调用它时,会导致程序异常终止,而不会进行一些常规的清除工作,比如释放内存等,所有的流会被关闭和清洗)
                b.当进程接收到某些信号时。信号可由进程自身、其他进程或内核产生。
                c.最后一个线程对“取消”请求作出响应。默认情况下,“取消”以延时方式发生:一个线程要求取消另一个线程,若干时间之后,目标线程终止。
 
    总结:不管进程如何终止,最后都会执行内核中的同一段代码。这段代码为相应进程关闭所有打开的描述符,释放它所使用的存储器。
 
    exit()和_exit()的异同:
        1)当程序执行到exit和_exit时,系统无条件的停止剩下所有的操作,清除包括PCB(进程控制块,在linux的具体实现是task_struct数据结构)在内的各种数据结构,并终止本进程的运行。
        2) exit在头文件stdlib.h中声明,而_exit在画unistd.h中声明。exit中的参数0代表进程正常终止,若为其他值表示程序执行过程中有错误发生。
        3)调用_exit函数时,其会关闭进程所有的文件描述符,清理内存以及其他一些内核清理函数,但不会刷新流(stdout,stdin,stderr……),exit函数是在_exit函数之上的一个封装,其会调用_exit,并在调用之前先刷新流
    
 1 #include <stdio.h>
 2 #include <stdlib.h> //exit
 3 #include <unistd.h> //_exit
 4 
 5 int main()
 6 {
 7         printf("this is a test\n");
 8         printf("again test");
 9         _exit(0);
10 }

 

    在一个进程终止时,内核逐个检查所有活动进程,以判断它是否是将要终止进程的子进程,如果是,则该进程的父进程ID就更改为1(init进程的ID),这种处理方法保证了每个进程都有一个父进程。内核为每个终止子进程保存了一定量的信息,所以当终止进程的父进程调用wait或waitpid时,可以得到这些信息,这些信息至少包含进程ID、该进程的终止状态以及该进程使用的cpu时间总量。内核可以释放终止进程所使用的所有存储区,关闭其所有打开文件。在UNIX术语中,一个已经终止、但是其父进程尚未对其进行善后处理(获取终止子进程的有关信息、释放它占用的资源)的进程被称为僵死进程。僵死进程与孤儿进程详细介绍:http://www.cnblogs.com/Anker/p/3271773.html#undefined
 
5、函数wait和waitpid
    当一个进程正常或异常终止时,内核就向其父进程发送SIGCHLD信号。因为子进程终止对于父进程来说是个异步事件,所以这种信号也是内核向父进程发的异步通知,父进程可以选择忽略该信号,或者提供一个该信号发生时即被调用执行的函数(信号处理程序),僵死进程可以采用信号的方法来避免,也可以采用两次fork的方法来避免。
    父进程调用wait或waitpid:
        1)如果其所有子进程都还在运行,则阻塞;
        2)如果子进程已终止,则取得该子进程的终止状态立即返回;
        3)如果它没有任何子进程,则立即返回出错。
    wait和waitpid的区别:
        1)在一个子进程终止前,wait使其调用者阻塞,而waitpid有一个选项,可使调用者不阻塞;
        2)waitpid并不等待在其调用之后的第一个终止子进程,它有若干个选项,可以控制它所等待的进程;而wait只等待第一个子进程终止立即返回;
1 #include <sys/wait.h>
2 
3 pid_t wait (int *status);
4 pid_t waitpid(pid_t pid, int *status, int options);
5 
6 /*两个函数返回值:若成功,返回进程id,并将子进程的终止状态存放在由status指向的存储单元中;若出错,返回 0 或 -1*/
      这两个函数的参数status是一个整型指针,如果status不是一个空指针,则终止进程的终止状态就存放在它所指向的单元内,指出子进程是否为正常退出以及正常结束时的返回值,或被哪一个信号结束等信息,由于这些信息被存放在一个整数的不同二进制中,所以就设计了一套专门的宏来完成这项工作。如果不关心终止状态,则可将该参数指定为空指针。
  检查wait和waitpid所返回的终止状态的宏:

说明

         WIFEXITED(status)     若为正常终止子进程返回的状态,则为真。对于这种情况可执行WEXITSTATUS(status),取子进程传送给exit、_exit或_Exit参数的低8位
         WIFSIGNALED(status)    若为异常终止子进程返回的状态,则为真(接到一个不捕捉的信号)。对于这种情况,可执行WTERMSIG(status),取使子进程终止的信号编号。另外,有些实现定义宏WCOREDUMP(status),若已产生终止进程的core文件,则它返回真
        WIFSTOPPED(status)    若为当前暂停子进程的返回状态,则为真。对于这种情况,可执行WSTOPSIG(status),取使子进程暂停的信号编号
        WIFCONTINUED(status)    若在作业控制暂停后已经继续的子进程返回了状态,则为真。(POSIX.1的XSI扩展;仅用于waitpid。)

     对于waitpid函数中pid参数的作用解释如下:
        1)pid == -1,等待任一子进程,此种情况下,waitpid和wait等效;
        2)pid > 0,等待进程id与pid相等的子进程;
        3)pid == 0,等待组id等于调用进程组ID的任一子进程;
        4)pid < -1,等待组id等于pid绝对值的任一子进程。
  如果一个进程希望等待一个子进程终止,则它必须调用wait函数中的一个;如果一个进程要等待其父进程终止,则可使用下列循环的形式:
1 while (getppid() != 1)
2     sleep(1);
3 /*这种形式的循环称为轮询,它的问题是浪费了CPU的时间,因为调用者每隔1s都被唤醒,然后进行条件测试*/
   对于waitpid函数中的option参数解释如下:
  options提供了一些额外的选项来控制waitpid,目前在Linux中只支持WNOHANG和WUNTRACED两个选项,这是两个常数,可以用"|"运算符把它们连接起来使用,比如:
  
1 ret = waitpid(-1, NULL, WNOHANG | WUNTRACED);
2 /*如果不想使用它们,option可以设置为0*/
3 ret = waitpid(-1, NULL, 0);

  如果使用了WNOHANG(wait no hung)参数调用waitpid,即使没有子进程退出,它也会立即返回,不会像wait那样永远等下去。WUNTRACED不经常用到,这里不做解释。

6、函数exec
   用fork函数创建新的子进程后,子进程往往调用exec函数以执行另一个程序。当进程调用一种exec函数时,该进程执行的程序完全替换为新程序,新程序可以是一个可执行的二进制文件,也可以是一个linux下任何可执行的脚本。因为调用exec并不创建新进程,所以前后进程的id并未改变。exec只是用磁盘上的一个新程序替换了当前进程的正文段、数据段、堆和栈。有7中不同的exec函数可供使用,它们常常被系统称为exec函数,可以使用7个函数中的任一个。这些exec函数使得unix系统进程控制原语更加完善(进程控制原语:进程创建,进程阻塞,唤醒进程和进程终止四个原语)。
    
 1 #include <unistd.h>
 2 
 3 int execl( const char *pathname, const char *arg0, ... /* (char *)0 */ );
 4 
 5 int execv( const char *pathname, char *const argv[] );
 6 
 7 int execle( const char *pathname, const char *arg0, ... /* (char *)0, char *const envp[] */ );
 8 
 9 int execve( const char *pathname, char *const argv[], char *const envp[] );
10 
11 int execlp( const char *filename, const char *arg0, ... /* (char *)0 */ );
12 
13 int execvp( const char *filename, char *const argv[] );
14 
15 int fexecve (int fd, char *const argv[], char *const envp[]);
16 
17 /*7个函数返回值:若出错则返回-1,若成功则不返回值*/
18 /*这些函数之间的第一个区别是前4个函数取路径名作为参数,后两个函数则取文件名作为参数,最后一个取文件描述符作为参数*/

  

  这7 个函数在函数名和使用语法的规则上都有细微的区别,下面就可执行文件查找方式、参数表传递方式及环境变量这几个方面进行比较说明。

  ①    查找方式:上表其中前4个函数的查找方式都是完整的文件目录路径,而后2个函数(也就是以p结尾的两个函数)可以只给出文件名,系统就会自动从环境变量“$PATH”所指出的路径中进行查找。

  ②    参数传递方式:exec函数族的参数传递有两种方式,一种是逐个列举的方式,而另一种则是将所有参数整体构造成指针数组进行传递。在这里参数传递方式是以函数名的第5位字母来区分的,字母为“l”(list)的表示逐个列举的方式,字母为“v”(vertor)的表示将所有参数整体构造成指针数组传递,然后将该数组的首地址当做参数传给它,数组中的最后一个指针要求是NULL。

  ③    环境变量:exec函数族使用了系统默认的环境变量,也可以传入指定的环境变量。这里以“e”(environment)结尾的两个函数execle、execve就可以在envp[]中指定当前进程所使用的环境变量替换掉该进程继承的所以环境变量。

 

 PATH环境变量

  PATH环境变量包含了一张目录表,系统通过PATH环境变量定义的路径搜索执行码;

  查看PATH环境变量:export $PATH;

mac_fan$ echo $PATH
/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin

 

例1:execlp()
1
#include <stdio.h> 2 #include <unistd.h> 3 int main() 4 { 5 if(fork()==0){ 6 if(execlp("/usr/bin/env","env",NULL)<0) 7 { 8 perror("execlp error!"); 9 return -1 ; 10 } 11 } 12 return 0 ; 13 }
输出结果:

mac-fandeMacBook-Pro:me mac_fan$ /usr/bin/env 

TERM_PROGRAM=Apple_Terminal

SHELL=/bin/bash

TERM=xterm-256color

TMPDIR=/var/folders/mx/48lcj5zn4cz8r61db79p5s1h0000gn/T/

例2:execle()
1
#include <unistd.h> 2 #include <stdio.h> 3 int main() 4 { 5 /*命令参数列表,必须以 NULL 结尾*/ 6 char *envp[]={"PATH=/tmp","USER=sun",NULL}; 7 if(fork()==0){ 8 /*调用 execle 函数,注意这里也要指出 env 的完整路径*/ 9 if(execle("/usr/bin/env","env",NULL,envp)<0) 10 { 11 perror("execle error!"); 12 return -1 ; 13 } 14 } 15 return 0 ; 16 }
输出结果:

mac_fan$ ./a.out

mac_fan$ PATH=/tmp

USER=sun

 

 

在exec函数族中,后缀l、v、p、e指定函数将具有某种操作能力:

后缀 操作能力
l list希望接收以逗号分隔的参数列表,列表以NULL指针作为结束标志
v vertor希望接收到一个以NULL结尾的字符串数组的指针
p 是一个以NULL结尾的字符串数组指针,函数可以根据PATH变量查找子程序文件
e 函数传递指定参数envp,可以不继承父进程的环境变量,改变子进程的环境,无后缀e时,子进程使用当前程序的环境

 
  1 /*具体代码*/
  2 #ifdef HAVE_CONFIG_H
  3 #include <config.h>
  4 #endif
  5 
  6 #include <stdio.h>
  7 #include <stdlib.h>
  8 #include <unistd.h>
  9 #include <string.h>
 10 #include <errno.h>
 11 
 12 int main(int argc, char *argv[])
 13 {
 14   //以NULL结尾的字符串数组的指针,适合包含v的exec函数参数
 15   char *arg[] = {"ls", "-a", NULL};
 16   
 17   /**
 18    * 创建子进程并调用函数execl
 19    * execl 中希望接收以逗号分隔的参数列表,并以NULL指针为结束标志
 20    */
 21   if( fork() == 0 )
 22   {
 23     // in clild 
 24     printf( "1------------execl------------\n" );
 25     if( execl( "/bin/ls", "ls","-a", NULL ) == -1 )
 26     {
 27       perror( "execl error " );
 28       exit(1);
 29     }
 30   }
 31   
 32   /**
 33    *创建子进程并调用函数execv
 34    *execv中希望接收一个以NULL结尾的字符串数组的指针
 35    */
 36   if( fork() == 0 )
 37   {
 38     // in child 
 39     printf("2------------execv------------\n");
 40     if( execv( "/bin/ls",arg) < 0)
 41     {
 42       perror("execv error ");
 43       exit(1);
 44     }
 45   }
 46   
 47   /**
 48    *创建子进程并调用 execlp
 49    *execlp中
 50    *l希望接收以逗号分隔的参数列表,列表以NULL指针作为结束标志
 51    *p是一个以NULL结尾的字符串数组指针,函数可以DOS的PATH变量查找子程序文件
 52    */
 53   if( fork() == 0 )
 54   {
 55     // in clhild 
 56     printf("3------------execlp------------\n");
 57     if( execlp( "ls", "ls", "-a", NULL ) < 0 )
 58     {
 59       perror( "execlp error " );
 60       exit(1);
 61     }
 62   }
 63   
 64   /**
 65    *创建子里程并调用execvp
 66    *v 望接收到一个以NULL结尾的字符串数组的指针
 67    *p 是一个以NULL结尾的字符串数组指针,函数可以DOS的PATH变量查找子程序文件
 68    */
 69   if( fork() == 0 )
 70   {
 71     printf("4------------execvp------------\n");
 72     if( execvp( "ls", arg ) < 0 )
 73     {
 74       perror( "execvp error " );
 75       exit( 1 );
 76     }
 77   }
 78   
 79   /**
 80    *创建子进程并调用execle
 81    *l 希望接收以逗号分隔的参数列表,列表以NULL指针作为结束标志
 82    *e 函数传递指定参数envp,允许改变子进程的环境,无后缀e时,子进程使用当前程序的环境
 83    */
 84   if( fork() == 0 )
 85   {
 86     printf("5------------execle------------\n");
 87     if( execle("/bin/ls", "ls", "-a", NULL, NULL) == -1 )
 88     {
 89       perror("execle error ");
 90       exit(1);
 91     }
 92   }
 93   
 94   /**
 95    *创建子进程并调用execve
 96    * v 希望接收到一个以NULL结尾的字符串数组的指针
 97    * e 函数传递指定参数envp,允许改变子进程的环境,无后缀e时,子进程使用当前程序的环境
 98    */
 99   if( fork() == 0 )
100   {
101     printf("6------------execve-----------\n");
102     if( execve( "/bin/ls", arg, NULL ) == 0)
103     {
104       perror("execve error ");
105       exit(1);
106     }
107   }
108   return EXIT_SUCCESS;
109 }

   在执行exec后,进程ID没有改变。但是新程序从调用进程继承了的下列属性:

  进程ID和父进程ID、实际用户ID和实际组ID、附属组ID、进程组ID、会话ID、控制终端、闹钟尚余留的时间、当前工作目录、根目录、文件模式创建屏蔽字、文件锁、进程信号屏蔽、未处理信号、资源限制、nice值……

  在exec前后实际用户ID和实际组ID保持不变,而有效ID是否改变则取决于所执行程序文件的设置用户ID为和设置组ID位是否设置。如果新程序的设置用户ID位已设置,则有效用户ID变成程序文件所有者的ID,否则有效用户ID不变。对ID组的处理方式与此相同。

 
http://www.cnblogs.com/funblogs/                                                                                                                                                                  
posted @ 2017-09-08 15:36  请叫我阿强  阅读(403)  评论(0编辑  收藏  举报