进程——父子进程共享

时间:2024-01-27 08:32:04

一、fork()

  1. 在谈fork之前,先简单说一下进程的相关知识点。

  (1)进程不同于程序是动态运行在内存中的实体,占用系统资源(CPU、内存等),而程序则是存放在磁盘中的静态的资源,占用磁盘空间而不占用系统资源。进程在内存中运行,由CPU分配资源。

  (2)与进程相关的两个内存:虚拟内存和物理内存。所谓虚拟内存就是我们程序员视角下的内存,比如int a = 10; &a 所得的值就是虚拟内存,是给我们程序员看的连续的地址空间。(当我们在代码中连续定义几个local object时,通过&可以观察到它们的地址是连续的)相对的,物理内存才是实实在在的存在于计算机硬件中的内存(比如买内存条时我们可以参考的4G、8G等容量参数),当执行 a = 20这条语句时,操作系统就会将 a 的虚拟地址送入 CPU的地址转换单元(MMU),如果a还没有实际的物理单元,则为a分配物理内存,写入20,反之直接将20写入a物理内存单元。

  (3)为什么会有虚拟内存? 虚拟内存的产生源自物理内存的稀缺,买过SSD或者内存条的伙伴都知道,250G的SSD也就是250块左右,而仅仅8G的内存条就要250块,内存的小容量与高价格的反差促使猿们必须节省内存的开销。由此产生了虚拟内存技术,32位系统下,CPU会为每个进程分配4G的虚拟地址单元(地址编号为0-4G),分为用户空间(通常为0-3G)和内核(kernel)空间(3-4G),用户空间存放该进程的堆栈变量、全局变量等,kernel里存放该进程的进程控制块(PCB,唯一区分每一个进程)。虚拟内存单元只有在被进程访问后才会映射为物理内存单元(见(2)a=20的执行过程)。

  (4)内存使用的一大机制(虚拟内存能实现的原因):缺页中断(见文章最后)。

  2. fork()函数用于创建进程,fork执行完后系统就产生了一个新的进程,成为调用fork的进程的子进程,此时系统中有两个进程,父进程和子进程。

  fork()返回值:

    成功:父进程——返回子进程的pid; 子进程——返回0

    失败:返回-1

  3. fork()是如何产生子进程的?  上面说了每一个进程都有自己0-4G的虚拟地址空间,因此,fork所做的动作就是在当前进程的基础上产生一个新进程:(1)复制父进程的0-3G的用户空间(2)创建新进程(子进程)的PCB

二、父子进程共享

  共享的定义是二者可以共同使用一件东西,前提是一件东西,而不是一个东西的两个副本。

  fork()完成的动作是子进程拿到了父进程0-3G用户空间的副本而不是原件,因此严格来说父子进程之间是不共享的,那为什么这里还要说到共享呢?前面说到,父进程的0-3G用户空间是虚拟内存空间,只有我们访问了某个单元时,才会真正在物理内存上分配一份地址空间,也就是说,这些虚拟内存里总会有一部分被真正的映射到了物理内存中,但是为了节省内存开销,fork在复制0-3G的虚拟内存空间时并不会将父进程中已经映射到物理内存的内存单元再复制一份到物理内存中,而是遵循“读时共享,写时复制”的原则,即仅复制虚拟地址空间。

  因此这里说的“共享”更大意义上存在于 父子进程共同访问一块内存空间的过程0,比如fork产生的子进程要输出父进程中变量a的值,那么子进程只需要共享父进程提供的变量a的物理单元,取出20这个值输出即可,如果子进程不单要输出a,还要对a执行+1操作,这个时候子进程才会将父进程中变量a的物理内存单元复制一份并执行+1操作,执行完后父进程中的变量a值仍为20,子进程a值为21。

  因此可以说fork后父子进程的用户空间是相同的,kernel空间是不同的。

  但是fork后父子进程的用户空间遵循的是“读时共享,写时复制”的原则,类似于“父子进程共享栈变量、全局变量”的说法都是不准确的。

  看一个例子吧:

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
//global variable
int globalVar = 78;

int main(){
    pid_t pid = getpid();
    int k = 0;
    int i;
    for(i = 0; i < 5; ++i){
        pid = fork();   // 循环生成5个子进程
        if(-1 == pid){
            perror("FORK");
            exit(1);
        }else if(0 == pid){
            break;
        }
    }
    sleep(i); // 保证父进程最后退出
    if(i < 5){
        ++k;  // 写时复制
        --globalVar; //写时复制
        printf("I'm %dth child, pid = %d, parent is %d, k = %d, globalVar = %d\n", i, getpid(), getppid(), k, globalVar);
    }else{
        ++k;
        --globalVar;
        printf("I'm parent, pid = %d, k = %d, globalVar = %d\n", getpid(), k, globalVar);
    }
}

 

 三、小福利

  缺页中断:缺页中断的过程其实已经在1.(2)中描述了,缺页中断简单来说就是:先对 待访问数据的虚拟地址进行段表和页表的映射,试图访问其对应的物理内存单元, 然后——待访问的数据在物理内存中吗 ? (在)访问/修改该物理内存单元 : (不在)申请一段物理内存存放该数据并完成与虚拟地址的映射,然后访问/修改该内存单元。 (段表和页表是段页式内存管理中记录虚拟地址与物理地址映射关系的表)