linux下C 编程学习之多进程编程(一)(转)

一、进程概念

进程是操作系统中资源分配的最小单位,而线程是调度的最小单位。

一个进程,主要包含三个元素:

 

a)        一个可以执行的程序;

b)        和该进程相关联的全部数据(包括变量,内存空间,缓冲区等等);

c)        程序的执行上下文(execution context)。

不妨简单理解为,一个进程表示的,就是一个可执行程序的一次执行过程中的一个状态。操作系统对进程的管理,典型的情况,是通过进程表完成的。进程表中的每一个表项,记录的是当前操作系统中一个进程的情况。对于单 CPU的情况而言,每一特定时刻只有一个进程占用CPU,但是系统中可能同时存在多个活动的(等待执行或继续执行的)进程。

 

二、进程标识符

每一个进程都有一个非负整形表示的唯一进程ID。虽然进程ID总是唯一的,但是可以重用。当一个进程终止,其之前被分配的进程ID就可以再次被使用。

 

三、创建进程  fork() 函数

fork()是进程的核心函数,由fork创建的新进程被成为子进程。

eg:

有一个现有的进程可以调用fork函数创建一个新的进程:

 

#include <unistd.h>

pid_t fork(void);

 

fork函数被调用一次,但返回两次。两次返回的唯一区别是子进程的返回值是0,而父进程的返回值则是新子进程的进程ID。

将子进程ID返回给父进程的理由是:因为一个进程的子进程可以有多个,但是没有一个函数能使一个进程可以获得其所有子进程的进程ID。

fork使子进程获得返回值为0的原因:一个进程只会有一个父进程,所以子进程总是可以调用getppid用来获得其父进程的进程ID。

子进程和父进程继续执行fork调用之后的指令(原因在文章的后面部分会解释)。子进程是父进程的副本。子进程可以获得父进程的数据空间、堆和栈的副本,但是父子进程并不共享这些存储空间,父子进程共享这些正文段。

 

 

fork函数简单示例:

 

#include "apue.h"

int     glob = 6;
char    buf[] = "a write to stdout\n";

int main(void)
{
        int     var;
        pid_t   pid;

        var = 88;
        if (write(STDOUT_FILENO, buf, sizeof(buf)-1) != sizeof(buf)-1)//write函数将buf中的内容写到标准输出流中,此处制作输出使用。
        err_sys("write error");
        printf("before fork\n");

        if ((pid = fork()) < 0) {
                err_sys("fork error");
        } else if (pid == 0) {
                glob++;
                var++;
        } else {
                sleep(2);  //学过Linux的都知道,在fork()之后,是父进程先执行还是子进程先执行取决于内核的调度算法。所以在这里为了让子进程先执行,我们
                             先让父进程sleep 2秒,但是2秒钟不一定够,所以不一定能够保证子进程先执行。
 }

编译、运行 上述代码段之后可得如下结果:

wangye@wangye:~$ gcc -g test1.c -o test1 libapue.a   //编译 
wangye@wangye:~$ ./test1               //运行
a write to stdout
before fork
pid = 20266, glob = 7, var = 89
pid = 20265, glob = 6, var = 88


代码分析:

 

a)、当你看到fork的时候,你可以把fork理解成“分叉”,在分叉的同时,生成的一个子进程复制了父进程的基本所有的东西,包括代码、数据和分配给进程的资源。也就是子进程几乎是和父进程是一模一样的。但是子进程可能会根据不同情况调用其他函数。比如exec函数。

b)、在语句pid=fork()之前,只有一个进程在执行这段代码,但在这条语句之后,就变成两个进程在执行了,这两个进程的几乎完全相同,将要执行的下一条语句都是if(pid<0)……

  为什么两个进程的pid不同呢,这与fork函数的特性有关。fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:

  1)在父进程中,fork返回新创建子进程的进程ID;

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

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

  在fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。解释一下pid的值为什么在父子进程中不同。“其实就相当于链表,进程形成了链表,父进程的pid(p 意味point)指向子进程的进程id, 因为子进程没有子进程,所以其pid为0.

c)、fork出错可能有两种原因:

  1)当前的进程数已经达到了系统规定的上限,这时errno的值被设置为EAGAIN。

  2)系统内存不足,这时errno的值被设置为ENOMEM。

d)、创建新进程成功后,系统中出现两个基本完全相同的进程,这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的进程调度策略。

  每个进程都有一个独特(互不相同)的进程标识符(process ID),可以通过getpid()函数获得,还有一个记录父进程pid的变量,可以通过getppid()函数获得变量的值。

e)、fork调用执行完毕后,出现两个进程,

  或许有人问两个进程的内容完全一样,为什么打印的结果不一样啊,那是因为判断条件的原因,上面列举的只是进程的代码和指令,还有变量。

  执行完fork后,进程1的变量为var=88,pid!=0(父进程)。进程2的变量为var=88,pid=0(子进程),这两个进程的变量都是独立的,存在不同的地址中,不是共用的,这点要注意。可以说,我们就是通过pid来识别和操作父子进程的。

f)、还有人可能问为什么不是从#include处开始复制代码的

     这是因为fork是把进程当前的情况拷贝一份,执行fork时,进程已经执行完了int var=88;fork只拷贝下一个要执行的代码到新的进程。(因为FORK是复制产生一个新的进程,因此新的进程与旧的进程之间的上下文,如寄存器上下文等是一致的,也就是说两个进程的变量值,PC指针值也是一样的在这里:PC是指寄存器PC,它里边的值总是指向当前程序的运行点的地址,因此两个进程都是在同一个位置开始运行)。

 

 

 

四、fork难度++

a)、首先看一段代码:

 

#include "apue.h"
 
int main(void)
 
{
      int i=0;
      pid_t pid;
 
      for(i=0;i<2;i++)
 
       {
 
                if((pid=fork())<0){
 
                        printf("forkerror\n");
 
                }else if(pid==0){
 
                       printf("%d,childself's pid=%d,parent's pid=%d,returnid=%d\n",i,getpid(),getppid(),pid);
 
               }else{
 
                       printf("%d,parentself's pid=%d,parent's father's pid=%d,returnid=%d\n",i,getpid(),getppid(),pid);
 
                        sleep(2);//为了确保(1)、在i=0时子进程先于父进程执行fork调用;
                                           (2)、父进程在子进程之后退出,这样可以保证子进程不会变成孤儿进程而过继给init进程;
                  }
 
       }
 
       exit(0);
 
}

  

 

 

wangye@wangye:~$ gcc -g test32.c -o test32 libapue.a 
wangye@wangye:~$ ./test32
0,parentself's pid=21352,parent's father's pid=31055,returnid=21353
0,childself's pid=21353,parent's pid=21352,returnid=0
1,parentself's pid=21353,parent's father's pid=21352,returnid=21354
1,childself's pid=21354,parent's pid=21353,returnid=0
1,parentself's pid=21352,parent's father's pid=31055,returnid=21355
1,childself's pid=21355,parent's pid=21352,returnid=0


分析:

 

第一步:在父进程中,当指令执行for循环时,i=0,接着执行fork,fork执行完后,系统中出现两个进程,分别是21352和21353。可以看到父进程21352的父进程是31055,子进程21353的父进程正好是21352。我们用一个链表来表示这个关系:

  31055->21352->21353

第一次fork后,21352(父进程)的返回值 returnid=21353,而子进程21353的返回值rturnid=0。原因是:fork函数被调用一次产生两个返回值,父进程得到的返回值是它所产生的子进程的进程ID,而子进程得到的返回值是0。

所以会有如下打印结果:

 

0,parentself's pid=21352,parent's father's pid=31055,returnid=21353
0,childself's pid=21353,parent's pid=21352,returnid=0

 

 

第二步:有上面的结果可知,当i=1时,子进程21353先执行,接着执行fork,系统中又新增一个新进程21354,对于此时进程链为:21352(当前进程的父进程)->21353(当前进程)->21354(被创建的子进程)。从输出可以看到21353原来是21352的子进程,现在变成21354的父进程。父子进程是相对的,这个大家应该容易理解。只要当前进程执行了fork,该进程就变成了父进程了,就打印出了parent。

  对于进程21352,当i=1时,接着执行fork,该进程创建一个进程号为21355的子进程,对于此进程进程链为:31055-(当前进程的父进程)>21352(当前进程)->21355(被创建的子进程)。

第三步:第二步的时候创建了两个进程21354和21355,,这两个进程执行完printf函数后就结束了,所以这两个进程无法进入第三次循环,无法执行fork,故returnid= 0;其他进程也是如此。
    以下是进程21354和21355打印出的结果:
 

1,childself's pid=21354,parent's pid=21353,returnid=0
1,childself's pid=21355,parent's pid=21352,returnid=0


       总结一下,这个程序执行的流程如下:

 

这个程序最终产生了3个子进程,执行过6次printf()函数。

b)、接下来再看一段代码:

#include <unistd.h>
#include <stdio.h>
int main(void)
{
   int i=0;
   printf("i son/pa ppid pid  fpid\n");//ppid指当前进程的父进程pid,pid指当前进程的pid,fpid指fork返回给当前进程的值

   for(i=0;i<2;i++){
       pid_t fpid=fork();
        if(fpid<0)
                printf("error\n");
       if(fpid==0)
           printf("%d child  %4d %4d %4d\n",i,getppid(),getpid(),fpid);
       else
        {   printf("%d parent %4d %4d %4d\n",i,getppid(),getpid(),fpid);
}
   }
   return 0;
}


编译运行结果如下:

 

wangye@wangye:~$ gcc -g fork1.c -o fork1 
wangye@wangye:~$ ./fork1
i son/pa ppid pid  fpid
0 parent 31055 29375 29376
0 child  29375 29376    0
1 parent 31055 29375 29377
1 parent 29375 29376 29378
1 child     1 29378    0
1 child     1 29377    0

该程序和上面的程序类似但是细心的读者会发现, 进程29378和进程29377的父进程难道不该是29376和29375吗,怎么会是1呢?在这里得涉及到进程的创建和死亡过程,在29376和29375执行完第二个循环后,main函数就该退出了,也即进程该死亡了,因为它已经做完所有事情了。29376和29375死亡后,29378和29377就没有父进程了就变成了孤儿进程,这在操作系统中是不被允许的,所以进程29378和进程29377的父进程就被置为1了,相当于把这两个孤儿进程过继给PID=1的init进程(init进程是系统专用进程),是永远不会死亡的。

 

 

我们在来看一份代码:

#include <unistd.h>
#include <stdio.h>
int main(void)
{
   int i=0;
   for(i=0;i<3;i++){
       pid_t fpid=fork();
       if(fpid==0)
    	   printf("son/n");
       else
    	   printf("father/n");
   }
   return 0;
}

 

编译运行结果:

 

wangye@wangye:~$ gcc -g fork2.c -o fork2 
wangye@wangye:~$ ./fork2
father
son
father
father
father
father
son
son
son
father
son
father
son
son


针对上述结果进行一下详细分析,如图所示:

 

 

 

    总结一下规律,对于这种N次循环的情况,执行printf函数的次数为2*(1+2+4+……+2N-1)次,创建的子进程数为1+2+4+……+2N-1个。 网上有人说N次循环产生2*(1+2+4+……+2N)个进程,这个说法是不对的,希望大家需要注意。

    如果想测试一个程序中创建的子进程数,最好的方法就是调用printf函数打印该进程的pid,也即调用printf("%d \n",getpid());或者通过printf("+ \n");来判断产生了几个进程。有人想直接通过调用printf("+");来统计创建了几个进程,这是不妥当的。具体原因如下:

我们重新来看一下本篇文章的第一段代码:

1):我们编译完之后执行结果如下:

 

 

wangye@wangye:~$ ./test1
a write to stdout
before fork
pid = 31332, glob = 7, var = 89
pid = 31331, glob = 6, var = 88

 

 

2):接下来我们用第二种方式运行该程序,执行命令./test1 > tmp.out,然后cat tmp.out(./test1 > temp.out是将执行结果输出到文件tmp.out中;cat tmp.out是获取该文件的内容)得到结果如下:

 

<span style="font-size:12px">w</span><span style="font-size:12px">angye@wangye:~$ ./test1 > tmp.out
wangye@wangye:~$ cat tmp.out 
a write to stdout
before fork
pid = 31335, glob = 7, var = 89
before fork
pid = 31334, glob = 6, var = 88</span>

 

在第一种情况下:由于write函数是不带缓冲的,因为在fork()之前调用的write,所以其数据写到标准输出一次。但是标准I/O库是带缓冲的,当我们用以上这种方式运行的时候,before fork只会输出一次,其原因是我们是用的交互式方式运行的该程序,且标准输出连到终端设备,它是行缓冲,标准缓冲区由换行符冲洗掉了,所以只得到printf一次。

在第二种情况下我们得到两次before fork:这是因为我们把标准输出重定向到了tmp.out,它是全缓冲的,那么该行数据不会被换行符冲洗掉,然后在将父进程的数据空间复制到子进程中时,该缓冲区也被复制到了子进程中。于是父子进程都拥有该行内容的标准I/O缓冲区。

进一步的解释请看如下代码是:

 

#include <unistd.h>
#include <stdio.h>
int main() {
        pid_t fpid;//fpid表示fork函数返回的值
        //printf("fork!");
        //printf("fork!\n");
        fpid = fork();
        if (fpid < 0)
                printf("error in fork!");
        else if (fpid == 0)
                printf("I am the child process, my process id is %d\n", getpid());
        else
                printf("I am the parent process, my process id is %d\n", getpid());
        return 0;
}

 

注释第一个printf输出,保留第二个printf输出的输出结果:

 

wangye@wangye:~$ gcc -g fork3.c -o fork3
wangye@wangye:~$ ./fork3
fork!
I am the parent process, my process id is 31459
I am the child process, my process id is 31460


注释第二个printf输出,保留第一个printf输出的输出结果:

 

wangye@wangye:~$ gcc -g fork3.c -o fork3
wangye@wangye:~$ ./fork3
fork!I am the parent process, my process id is 31506
fork!I am the child process, my process id is 31507


分析:之所以会有 上面的情况是跟printf的缓冲机制有关,即:printf某些内容时,操作系统仅仅是把该内容放到了stdout(标准输出流)的缓冲队列里了,并没有实际的写到屏幕上。但是,只要看到有/n 则会立即刷新stdout,因此就马上能够打印了。   运行了printf("fork!")后,“fork!”仅仅被放到了缓冲里,程序运行到fork时,缓冲里面的“fork!” 被子进程复制过去了。因此在子进程度stdout缓冲里面就也有了fork! 。所以,你最终看到的会是fork!  被printf了2次!!!!
    而运行printf("fork! /n")后,“fork!”被立即打印到了屏幕上,之后fork到的子进程里的stdout缓冲里不会有fork! 内容(前面说过了子进程的执行不是从#include开始执行的而是从代码段的fork处开始往下执行)。因此你看到的结果会是fork! 被printf了1次!!!!这就是为什么说printf("+");不能正确地反应进程的数量的原因。

 

为了测试子进程是否复制了父进程缓冲区里的内容,对本次文章第一次讲解的代码作如下修改:

#include "apue.h"

int glob=6;
char buf[]="a write to stdout\n";

int main(void)
{
  int var;
  pid_t pid;
  var=88;
  if(write(STDOUT_FILENO,buf,sizeof(buf)-1)!=sizeof(buf)-1)
  err_sys("write error");
  printf("before fork %d\n",getpid());
  if((pid=fork())<0)
  err_sys("fork error");
  else if(pid==0)
  {
    glob++;
    var++;
  }
  else   sleep(2);
  printf("pid= %d,glod=%d, var=%d \n",getpid(),glob,var);
  exit(0);
}

编译连接产生如下结果:

 

 

wangye@wangye:~$ gcc -g fork4.c -o fork4 libapue.a 
wangye@wangye:~$ ./fork4 >tmp.out
wangye@wangye:~$ cat tmp.out 
a write to stdout
before fork 1976
pid= 1977,glod=7, var=89 
before fork 1976
pid= 1976,glod=6, var=88

两次输出的都是before fork 1976,而1976是父进程的pid.所以可知子进程确实复制了父进程缓冲区中的内容。

 

转自:linux下C 编程学习之多进程编程(一)_wangyezi19930928的专栏-CSDN博客_多进程编程

posted @ 2020-11-26 09:37  学习随笔记  阅读(325)  评论(0编辑  收藏  举报