fork函数详解

 

1、fork入门知识html

     一个进程,包括代码、数据和分配给进程的资源。fork()函数经过系统调用建立一个与原来进程几乎彻底相同的进程,也就是两个进程能够作彻底相同的事,但若是初始参数或者传入的变量不一样,两个进程也能够作不一样的事。
    一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。而后把原来的进程的全部值都复制到新的新进程中,只有少数值与原来的进程的值不一样。至关于克隆了一个本身。linux

     咱们来看一个例子:缓存

/* 
 *  fork_test.c 
 *  version 1 
 *  Created on: 2010-5-29 
 *      Author: wangth 
 */  
#include <unistd.h>  
#include <stdio.h>   
int main ()   
{   
    pid_t fpid; //fpid表示fork函数返回的值  
    int count=0;  
    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());   
        printf("我是爹的儿子/n");//对某些人来讲中文看着更直白。  
        count++;  
    }  
    else {  
        printf("i am the parent process, my process id is %d/n",getpid());   
        printf("我是孩子他爹/n");  
        count++;  
    }  
    printf("统计结果是: %d/n",count);  
    return 0;  
} 

运行结果是:
    i am the child process, my process id is 5574
    我是爹的儿子
    统计结果是: 1
    i am the parent process, my process id is 5573
    我是孩子他爹
    统计结果是: 1
    在语句fpid=fork()以前,只有一个进程在执行这段代码,但在这条语句以后,就变成两个进程在执行了,这两个进程的几乎彻底相同,将要执行的下一条语句都是if(fpid<0)……
    为何两个进程的fpid不一样呢,这与fork函数的特性有关。fork调用的一个奇妙之处就是它仅仅被调用一次,却可以返回两次,它可能有三种不一样的返回值:
    1)在父进程中,fork返回新建立子进程的进程ID;
    2)在子进程中,fork返回0;
    3)若是出现错误,fork返回一个负值;

    在fork函数执行完毕后,若是建立新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新建立子进程的进程ID。咱们能够经过fork返回的值来判断当前进程是子进程仍是父进程。
    fork出错可能有两种缘由:
    1)当前的进程数已经达到了系统规定的上限,这时errno的值被设置为EAGAIN。
    2)系统内存不足,这时errno的值被设置为ENOMEM。
    建立新进程成功后,系统中出现两个基本彻底相同的进程,这两个进程执行没有固定的前后顺序,哪一个进程先执行要看系统的进程调度策略。
    每一个进程都有一个独特(互不相同)的进程标识符(process ID),能够经过getpid()函数得到,还有一个记录父进程pid的变量,能够经过getppid()函数得到变量的值。
    fork执行完毕后,出现两个进程,

    执行完fork后,进程1的变量为count=0,fpid!=0(父进程)。进程2的变量为count=0,fpid=0(子进程),这两个进程的变量都是独立的,存在不一样的地址中,不是共用的,这点要注意。能够说,咱们就是经过fpid来识别和操做父子进程的。
    有人可能疑惑为何不是从#include处开始复制代码的,这是由于fork是把进程当前的状况拷贝一份,执行fork时,进程已经执行完了int count=0;fork只拷贝下一个要执行的代码到新的进程。
2、fork进阶知识

    1.先看一份代码:并发

/* 
 *  fork_test.c 
 *  version 2 
 *  Created on: 2010-5-29 
 *      Author: wangth 
 */  
#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("%d child  %4d %4d %4d/n",i,getppid(),getpid(),fpid);  
       else  
           printf("%d parent %4d %4d %4d/n",i,getppid(),getpid(),fpid);  
   }  
   return 0;  
} 

运行结果是:
    i son/pa ppid pid  fpid
    0 parent 2043 3224 3225
    0 child  3224 3225    0
    1 parent 2043 3224 3226
    1 parent 3224 3225 3227
    1 child     1 3227    0
    1 child     1 3226    0
    这份代码比较有意思,咱们来认真分析一下:
    第一步:在父进程中,指令执行到for循环中,i=0,接着执行fork,fork执行完后,系统中出现两个进程,分别是p3224和p3225(后面我都用pxxxx表示进程id为xxxx的进程)。能够看到父进程p3224的父进程是p2043,子进程p3225的父进程正好是p3224。咱们用一个链表来表示这个关系:
    p2043->p3224->p3225
 第一次fork后,p3224(父进程)的变量为i=0,fpid=3225(fork函数在父进程中返向子进程id),代码内容为:函数

for(i=0;i<2;i++){  
    pid_t fpid=fork();//执行完毕,i=0,fpid=3225  
    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;  

  p3225(子进程)的变量为i=0,fpid=0(fork函数在子进程中返回0),代码内容为:测试

for(i=0;i<2;i++){  
    pid_t fpid=fork();//执行完毕,i=0,fpid=0  
    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;  

  因此打印出结果:
    0 parent 2043 3224 3225
    0 child  3224 3225    0
    第二步:假设父进程p3224先执行,当进入下一个循环时,i=1,接着执行fork,系统中又新增一个进程p3226,对于此时的父进程,p2043->p3224(当前进程)->p3226(被建立的子进程)。
    对于子进程p3225,执行完第一次循环后,i=1,接着执行fork,系统中新增一个进程p3227,对于此进程,p3224->p3225(当前进程)->p3227(被建立的子进程)。从输出能够看到p3225原来是p3224的子进程,如今变成p3227的父进程。父子是相对的,这个你们应该容易理解。只要当前进程执行了fork,该进程就变成了父进程了,就打印出了parent。
    因此打印出结果是:
    1 parent 2043 3224 3226
    1 parent 3224 3225 3227 
    第三步:第二步建立了两个进程p3226,p3227,这两个进程执行完printf函数后就结束了,由于这两个进程没法进入第三次循环,没法fork,该执行return 0;了,其余进程也是如此。
    如下是p3226,p3227打印出的结果:
    1 child     1 3227    0
    1 child     1 3226    0
    细心的读者可能注意到p3226,p3227的父进程难道不应是p3224和p3225吗,怎么会是1呢?这里得讲到进程的建立和死亡的过程,在p3224和p3225执行完第二个循环后,main函数就该退出了,也即进程该死亡了,由于它已经作完全部事情了。p3224和p3225死亡后,p3226,p3227就没有父进程了,这在操做系统是不被容许的,因此p3226,p3227的父进程就被置为p1了,p1是永远不会死亡的,至于为何,这里先不介绍,留到“3、fork高阶知识”讲。
    总结一下,这个程序执行的流程以下:
spa

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

2.咱们再来看一份代码:操作系统

/* 
 *  fork_test.c 
 *  version 3 
 *  Created on: 2010-5-29 
 *      Author: wangth 
 */  
#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;  
  
}  

它的执行结果是:
    father
    son
    father
    father
    father
    father
    son
    son
    father
    son
    son
    son
    father
    son
    这里就不作详细解释了,只作一个大概的分析。
    for        i=0         1           2
              father     father     father
                                        son
                            son       father
                                        son
               son       father     father
                                        son
                            son       father
                                        son
    其中每一行分别表明一个进程的运行打印结果。
    总结一下规律,对于这种N次循环的状况,执行printf函数的次数为2*(1+2+4+……+2N-1)次,建立的子进程数为1+2+4+……+2N-1个。.net

3.最后,对printf的缓冲机制作一个简单分析,代码以下:unix

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
 
int main()
{
    pid_t pid;
    
    printf("parent\n");
    pid = fork();
    if (0 == pid)
    {
        printf("child\n");
    }
    else if (pid > 0)
    {
        printf("parent\n");
    }
    else if (pid < 0)
    {
        printf("error\n");
    }        
    return 0;
}

输出结果为:
parent
parent
child

我把第一个printf里的'\n'去掉后,测试的输出结果是:

parentparent
parentchild

为何两种状况的输出结果差一个parent呢,由于prient函数存在缓冲机制,在详细介绍以前,先对缓冲作简要了解:

缓冲区又称为缓存,它是内存空间的一部分。也就是说,在内存空间中预留了必定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫作缓冲区。

缓冲区根据其对应的是输入设备仍是输出设备,分为输入缓冲区和输出缓冲区。

为何要引入缓冲区

好比咱们从磁盘里取信息,咱们先把读出的数据放在缓冲区,计算机再直接从缓冲区中取数据,等缓冲区的数据取完后再去磁盘中读取,这样就能够减小磁盘的读写次数,再加上计算机对缓冲区的操做大大快于对磁盘的操做,故应用缓冲区可大大提升计算机的运行速度。

又好比,咱们使用打印机打印文档,因为打印机的打印速度相对较慢,咱们先把文档输出到打印机相应的缓冲区,打印机再自行逐步打印,这时咱们的CPU能够处理别的事情。

如今您基本明白了吧,缓冲区就是一块内存区,它用在输入输出设备和CPU之间,用来缓存数据。它使得低速的输入输出设备和高速的CPU可以协调工做,避免低速的输入输出设备占用CPU,解放出CPU,使其可以高效率工做。

缓冲区的类型

缓冲区 分为三种类型:全缓冲、行缓冲和不带缓冲。

1) 全缓冲

在这种状况下,当填满标准I/O缓存后才进行实际I/O操做。全缓冲的典型表明是对磁盘文件的读写。

2) 行缓冲

在这种状况下,当在输入和输出中遇到换行符时,执行真正的I/O操做。这时,咱们输入的字符先存放在缓冲区,等按下回车键换行时才进行实际的I/O操做。典型表明是标准输入(stdin)和标准输出(stdout)。

3) 不带缓冲

也就是不进行缓冲,标准出错状况stderr是典型表明,这使得出错信息能够直接尽快地显示出来。

 

由此可知,由于printf函数其实调用的是全局宏stdout(标准输出),因此printf的缓冲属于行缓冲。

那什么状况下会刷新缓冲区?

  • 程序结束时调用 exit(0) .
  • 遇到 \n , \r 时会刷新缓冲区.
  • 手动刷新 fflush .
  • 缓冲区满时自动刷新.

咱们知道了以上内容后,回到刚才的代码

printf函数在执行输出内容时,操做系统仅仅是把该内容放到了stdout的缓冲队列里,并无实际的写到屏幕上。可是,只要看到有\n 则会当即刷新stdout,所以就立刻可以打印了。 

运行了printf("parent")后,“parent”仅仅被放到了缓冲里,程序运行到fork()时缓冲里面的“parent” 被子进程复制过去了。所以在子进程度stdout缓冲里面就也有了parent。因此,最终看到的会是parent 被printf了2次。

而运行printf("parent/n")后,,parent被当即打印到了屏幕上,以后fork()的子进程里的stdout缓冲里不会有“parent”。所以最终看到的结果parent只被printf了1次。

fork()会产生一个和父进程彻底相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,linux中引入了“写时复制“技术,也就是只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。在fork以后exec以前两个进程用的是相同的物理空间(内存区),子进程的代码段、数据段、堆栈都是指向父进程的物理空间,也就是说,二者的虚拟空间不一样,但其对应的物理空间是同一个。当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间,若是没有exec,内核会给子进程的数据段、堆栈段分配相应的物理空间(至此二者有各自的进程空间,互不影响),而代码段继续共享父进程的物理空间(二者的代码彻底相同)。而若是是由于exec,因为二者执行的代码不一样,子进程的代码段也会分配单独的物理空间。

fork时子进程得到父进程数据空间、堆和栈的复制,因此变量的地址(固然是虚拟地址)也是同样的。

每一个进程都有本身的虚拟地址空间,不一样进程的相同的虚拟地址显然能够对应不一样的物理地址。所以地址相同(虚拟地址)而值不一样没什么奇怪。 具体过程是这样的: fork子进程彻底复制父进程的栈空间,也复制了页表,但没有复制物理页面,因此这时虚拟地址相同,物理地址也相同,可是会把父子共享的页面标记为“只读”(相似mmap的private的方式),若是父子进程一直对这个页面是同一个页面,知道其中任何一个进程要对共享的页面“写操做”,这时内核会复制一个物理页面给这个进程使用,同时修改页表。而把原来的只读页面标记为“可写”,留给另一个进程使用。


这就是所谓的“写时复制”。正由于fork采用了这种写时复制的机制,因此fork出来子进程以后,父子进程哪一个先调度呢?内核通常会先调度子进程,由于不少状况下子进程是要立刻执行exec,会清空栈、堆。。这些和父进程共享的空间,加载新的代码段。。。,这就避免了“写时复制”拷贝共享页面的机会。若是父进程先调度极可能写共享页面,会产生“写时复制”的无用功。因此,通常是子进程先调度滴。

假定父进程malloc的指针指向0x12345678, fork 后,子进程中的指针也是指向0x12345678,可是这两个地址都是虚拟内存地址 (virtual memory),通过内存地址转换后所对应的 物理地址是不同的。因此两个进城中的这两个地址相互之间没有任何关系。

(注1:在理解时,你能够认为fork后,这两个相同的虚拟地址指向的是不一样的物理地址,这样方便理解父子进程之间的独立性) (注2:但实际上,linux为了提升 fork 的效率,采用了 copy-on-write 技术,fork后,这两个虚拟地址实际上指向相同的物理地址(内存页),只有任何一个进程试图修改这个虚拟地址里的内容前,两个虚拟地址才会指向不一样的物理地址(新的物理地址的内容从原物理地址中复制获得))

4.

*********父进程为何要建立子进程呢?*************

前面咱们已经说过了Linux是一个多用户操做系统,在同一时间会有许多的用户在争夺系统的资源.有时进程为了早一点完成任务就建立子进程来争夺资源. 一旦子进程被建立,父子进程一块儿从fork处继续执行,相互竞争系统的资源.有时候咱们但愿子进程继续执行,而父进程阻塞,直到子进程完成任务.这个时候咱们能够调用wait或者waitpid系统调用.

,对子进程来讲,fork返回给它0,但它的pid绝对不会是0;之因此fork返回0给它,是由于它随时能够调用getpid()来获取本身的pid;

fork以后父子进程除非采用了同步手段,不然不能肯定谁先运行,也不能肯定谁先结束。认为子进程结束后父进程才从fork返回的,这是不对的,fork不是这样的,vfork才这样。

*****************************************为何返回0呢**************************************************

首先必须有一点要清楚,函数的返回值是储存在寄存器eax中的。
其次,当fork返回时,新进程会返回0是由于在初始化任务结构时,将eax设置为0;
在fork中,把子进程加入到可运行的队列中,由进程调度程序在适当的时机调度运行。也就是今后时开始,当前进程分裂为两个并发的进程。
不管哪一个进程被调度运行,都将继续执行fork函数的剩余代码,执行结束后返回各自的值。

***********************************************fork()以后的寄存器具体执行*************************************

【NOTE5】
对于fork来讲,父子进程共享同一段代码空间,因此给人的感受好像是有两次返回,其实对于调用fork的父进程来讲,若是fork出来的子进程没有获得调度,那么父进程从fork系统调用返回,同时分析sys_fork知道,fork返回的是子进程的id。再看fork出来的子进程,由 copy_process函数能够看出,子进程的返回地址为ret_from_fork(和父进程在同一个代码点上返回),返回值直接置为0。因此当子进程获得调度的时候,也从fork返回,返回值为0。
关键注意两点:

1.fork返回后,父进程或子进程的执行位置。(首先会将当前进程eax的值作为返回值)

2.两次返回的pid存放的位置。(eax中)


进程调用copy_process获得lastpid的值(放入eax中,fork正常返回后,父进程中返回的就是lastpid)
子进程任务状态段tss的eax被设置成0,
fork.c 中
p-&gt;tss.eax=0;(若是子进程要执行就须要进程切换,当发生切换时,子进程tss中的eax值就调入eax寄存器,子进程执行时首先会将eax的内容作为返回值)
当子进程开始执行时,copy_process返回eax的值。
fork()后,就是两个任务同时进行,父进程用他的tss,子进程用本身的tss,在切换时,各用各的eax中的值.
因此,“一次调用两次返回”是2个不一样的进程!
看这一句:pid=fork()
当执行这一句时,当前进程进入fork()运行,此时,fork()内会用一段嵌入式汇编进行系统调用:int 0x80(具体代码可参见内核版本0.11的unistd.h文件的133行_syscall0函数)。这时进入内核根据此前写入eax的系统调用功能号便会运行sys_fork系统调用。接着,sys_fork中首先会调用C函数find_empty_process产生一个新的进程,而后会调用C函数 copy_process将父进程的内容复制给子进程,可是子进程tss中的eax值赋值为0(这也是为何子进程中返回0的缘由),当赋值完成后, copy_process会返回新进程(该子进程)的pid,这个值会被保存到eax中。这时子进程就产生了,此时子进程与父进程拥有相同的代码空间,程序指针寄存器eip指向相同的下一条指令地址,当fork正常返回调用其的父进程后,由于eax中的值是新建立的子进程号,因此,fork()返回子进程号,执行else(pid&gt;0);当产生进程切换运行子进程时,首先会恢复子进程的运行环境即装入子进程的tss任务状态段,其中的eax 值(copy_process中置为0)也会被装入eax寄存器,因此,当子进程运行时,fork返回的是0执行if(pid==0)。



参考:

  https://blog.csdn.net/jason314/article/details/5640969?utm_source=copy

  http://blog.csdn.net/dog_in_yellow/archive/2008/01/13/2041079.aspx

  http://blog.chinaunix.net/u1/53053/showart_425189.html

  http://blog.csdn.net/saturnbj/archive/2009/06/19/4282639.aspx

  http://www.cppblog.com/zhangxu/archive/2007/12/02/37640.html

  http://www.qqread.com/linux/2010/03/y491043.html

  http://www.yuanma.org/data/2009/1103/article_3998.htm

  https://www.jb51.net/article/127400.htm

  https://blog.csdn.net/xy010902100449/article/details/44851453

  https://blog.csdn.net/shenwansangz/article/details/39184789

相关文章
相关标签/搜索