1. fork
      fork创建一个进程时,子进程只是完全复制父进程的资源,复制出来的子进程有自己的task_struct结构和pid,但却复制父进程其它所有的资源。例如,要是父进程打开了五个文件,那么子进程也有五个打开的文件,而且这些文件的当前读写指针也停在相同的地方。所以,这一步所做的是复制。这样得到的子进程独立于父进程, 具有良好的并发性,但是二者之间的通讯需要通过专门的通讯机制,如:pipe,共享内存等机制, 另外通过fork创建子进程,需要将上面描述的每种资源都复制一个副本。这样看来,fork是一个开销十分大的系统调用,这些开销并不是所有的情况下都是必须的,比如某进程fork出一个子进程后,其子进程仅仅是为了调用exec执行另一个可执行文件,那么在fork过程中对于虚存空间的复制将是一个多余的过程。但由于现在Linux中是采取了copy-on-write(COW写时复制)技术,为了降低开销,fork最初并不会真的产生两个不同的拷贝,因为在那个时候,大量的数据其实完全是一样的。写时复制是在推迟真正的数据拷贝。若后来确实发生了写入,那意味着parent和child的数据不一致了,于是产生复制动作,每个进程拿到属于自己的那一份,这样就可以降低系统调用的开销。所以有了写时复制后呢,vfork其实现意义就不大了。

  fork()调用执行一次返回两个值,对于父进程,fork函数返回子程序的进程号,而对于子程序,fork函数则返回零,这就是一个函数返回两次的本质。
  在fork之后,子进程和父进程都会继续执行fork调用之后的指令。子进程是父进程的副本。它将获得父进程的数据空间,堆和栈的副本,这些都是副本,父子进程并不共享这部分的内存。也就是说,子进程对父进程中的同名变量进行修改并不会影响其在父进程中的值。但是父子进程又共享一些东西,简单说来就是程序的正文段。正文段存放着由cpu执行的机器指令,通常是read-only的。

  1. vfork
      vfork系统调用不同于fork,用vfork创建的子进程与父进程共享地址空间,也就是说子进程完全运行在父进程的地址空间上,如果这时子进程修改了某个变量,这将影响到父进程。

因此,上面的例子如果改用vfork()的话,那么两次打印a,b的值是相同的,所在地址也是相同的。
但此处有一点要注意的是用vfork()创建的子进程必须显示调用exit()来结束,否则子进程将不能结束,而fork()则不存在这个情况。
  Vfork也是在父进程中返回子进程的进程号,在子进程中返回0。
  用 vfork创建子进程后,父进程会被阻塞直到子进程调用exec(exec,将一个新的可执行文件载入到地址空间并执行之。)或exit。vfork的好处是在子进程被创建后往往仅仅是为了调用exec执行另一个程序,因为它就不会对父进程的地址空间有任何引用,所以对地址空间的复制是多余的 ,因此通过vfork共享内存可以减少不必要的开销。

  1. clone
      系统调用fork()和vfork()是无参数的,而clone()则带有参数。fork()是全部复制,vfork()是共享内存,而clone()是则可以将父进程资源有选择地复制给子进程,而没有复制的数据结构则通过指针的复制让子进程共享,具体要复制哪些资源给子进程,由参数列表中的clone_flags来决定。另外,clone()返回的是子进程的pid。
  2. fork与vfork区别
      vfork()用法与fork()相似.但是也有区别,具体区别归结为以下3点:

 (1)fork():子进程拷贝父进程的数据段,代码段. vfork():子进程与父进程共享数据段.
 (2)fork():父子进程的执行次序不确定.
   vfork():保证子进程先运行,在调用exec或_exit之前与父进程数据是共享的,在它调用exec
或_exit之后父进程才可能被调度运行。
 (3)vfork()保证子进程先运行,在她调用exec或_exit之后父进程才可能被调度运行。如果在
调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。
 (4)当需要改变共享数据段中变量的值,则拷贝父进程。

  1. 关于fork之后父子进程的内容

        先看一段代码
        #include<stdio.h>
        #include<string.h>
        #include<stdlib.h>
        #include<unistd.h>
    
        main()
        {

    char str[4]="asd";
    pid_t pid=fork();
    if(pid==0)
    {

    str[0]='b';
    printf("子进程中str=%s\n",str);
    printf("子进程中str指向的首地址:%x\n",(unsigned int)str);

    }
    else{

    sleep(1);
    printf("父进程中str=%s\n",str);
    printf("父进程中str指向的首地址:%x\n",(unsigned int)str);

    }

        }
    
        输出:
        子进程中str=bsd
        子进程中str指向的首地址:bfc224dc
        父进程中str=asd
        父进程中str指向的首地址:bfc224dc
    

这里我们发现出现父子进程变量的地址一样,但值不一样。为什么会这样呢?

  这里就涉及到物理地址和逻辑地址(或称虚拟地址)的概念。
(1)逻辑地址:CPU所生成的地址。CPU产生的逻辑地址被分为 :p (页号) 它包含每个页在物理内存中的基址,用来作为页表的索引;d (页偏移),同基址相结合,用来确定送入内存设备的物理内存地址。
(2)物理地址:内存单元所看到的地址。
一般从逻辑地址到物理地址的映射称为地址重定向。分为静态重定向和动态重定向:
(1)静态重定向--在程序装入主存时已经完成了逻辑地址到物理地址和变换,在程序执行期间不会再发生改变。
(2)动态重定向--程序执行期间完成,其实现依赖于硬件地址变换机构,如基址寄存器。

  用户程序看不见真正的物理地址。用户只生成逻辑地址,且认为进程的地址空间为0到max。物理地址范围从R+0到R+max,R为基地址,地址映射-将程序地址空间中使用的逻辑地址变换成内存中的物理地址的过程。由内存管理单元(MMU)来完成。fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,linux中引入了“写时复制“技术,也就是只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。在fork之后exec之前两个进程用的是相同的物理空间(内存区),子进程的代码段、数据段、堆栈都是指向父进程的物理空间,也就是说,两者的虚拟空间不同,但其对应的物理空间是同一个。当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间,如果不是因为exec,内核会给子进程的数据段、堆栈段分配相应的物理空间(至此两者有各自的进程空间,互不影响),而代码段继续共享父进程的物理空间(两者的代码完全相同)。而如果是因为exec,由于两者执行的代码不同,子进程的代码段也会分配单独的物理空间。fork之后内核会通过将子进程放在队列的前面,以让子进程先执行,以免父进程执行导致写时复制,而后子进程执行exec系统调用,因无意义的复制而造成效率的下降。

  上述结论总结起来就是fork时子进程获得父进程数据空间、堆和栈的复制,所以变量的地址(当然是虚拟地址)也是一样的。