zoukankan      html  css  js  c++  java
  • 父子进程之间的数据拷贝关系

    [cpp] view plain copy
     
    1. #include<stdio.h>  
    2. #include<string.h>  
    3. #include<stdlib.h>  
    4. #include<unistd.h>  
    5.   
    6. void main()  
    7. {  
    8.     char str[6]="hello";  
    9.   
    10.     pid_t pid=fork();  
    11.   
    12.     if(pid==0)  
    13.     {  
    14.         str[0]='b';  
    15.         printf("子进程中str=%s ",str);  
    16.         printf("子进程中str指向的首地址:%x ",(unsigned int)str);  
    17.     }  
    18.     else  
    19.     {  
    20.         sleep(1);  
    21.         printf("父进程中str=%s ",str);  
    22.         printf("父进程中str指向的首地址:%x ",(unsigned int)str);  
    23.     }  
    24. }  

    子进程中str=bello
    子进程中str指向的首地址:bfdbfc06
    父进程中str=hello

    父进程中str指向的首地址:bfdbfc06

    这里就涉及到物理地址和逻辑地址(或称虚拟地址)的概念。

    从逻辑地址到物理地址的映射称为地址重定向。分为:

    静态重定向--在程序装入主存时已经完成了逻辑地址到物理地址和变换,在程序执行期间不会再发生改变。

    动态重定向--程序执行期间完成,其实现依赖于硬件地址变换机构,如基址寄存器。

    逻辑地址:CPU所生成的地址。CPU产生的逻辑地址被分为 :p (页号) 它包含每个页在物理内存中的基址,用来作为页表的索引;d (页偏移),同基址相结合,用来确定送入内存设备的物理内存地址。

    物理地址:内存单元所看到的地址。

    用户程序看不见真正的物理地址。用户只生成逻辑地址,且认为进程的地址空间为0到max。物理地址范围从R+0到R+max,R为基地址,地址映射-将程序地址空间中使用的逻辑地址变换成内存中的物理地址的过程。由内存管理单元(MMU)来完成。

    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后,这两个虚拟地址实际上指向相同的物理地址(内存页),只有任何一个进程试图修改这个虚拟地址里的内容前,两个虚拟地址才会指向不同的物理地址(新的物理地址的内容从原物理地址中复制得到))


    2、exec家族

    exec家族一共有六个函数,分别是:

    [cpp] view plain copy
     
    1. <span style="font-family:Microsoft YaHei;font-size:12px;">(1)int execl(const char *path, const char *arg, ......);  
    2. (2)int execle(const char *path, const char *arg, ...... , char * const envp[]);  
    3. (3)int execv(const char *path, char *const argv[]);  
    4. (4)int execve(const char *filename, char *const argv[], char *const envp[]);  
    5. (5)int execvp(const char *file, char * const argv[]);  
    6. (6)int execlp(const char *file, const char *arg, ......);</span>  

    其中只有execve是真正意义上的系统调用,其它都是在此基础上经过包装的库函数。

        exec函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件。这里的可执行文件既可以是二进制文件,也可以是任何linux下可执行的脚本文件。

    与一般情况不同,exec函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段,数据段和堆栈等都已经被新的内容取代,只留下进程ID等一些表面上的信息仍保持原样,颇有些神似"三十六计"中的"金蝉脱壳"。看上去还是旧的躯壳,却已经注入了新的灵魂。只有调用失败了,它们才会返回一个-1,从原程序的调用点接着往下执行。

  • 相关阅读:
    ZOJ 3818 Pretty Poem
    HDU 4597 Play Game
    HDU 4497 GCD and LCM
    CSU 1335 高桥和低桥
    UVA 10791 Minimum Sum LCM
    CSU 1119 Collecting Coins
    CSU 1120 病毒
    UVA 12169 Disgruntled Judge
    HDU 1301 Jungle Roads
    POJ 1258 Agri-Net
  • 原文地址:https://www.cnblogs.com/daimadebanyungong/p/7472435.html
Copyright © 2011-2022 走看看