一、C/C++ 下的地址空间

1、回忆 C/C++ 下的内存分布

进程地址空间

我们在学习 C/C++ 的时候常说全局变量和局部变量存在数据段,只读常量的数据存在代码段,自己也可以为变量的申请空间我们称为堆区,还有一个区域就做栈区主要存放函数的返回值 / 函数的参数 / 非静态的成员变量。但是我们常常说这就是计算机内的内存分布,但他们真的就是指磁盘中的内存分布吗?我们要内存是直接在磁盘在申请的吗?

在回答这些问题之前我们先在 Linux 下跑一段代码,见见一个难以理解的现象。

2、看一个现象

我们写一个代码,用 fork 创建子进程,并循环打印子进程和父进程,让子进程和父进程都先打印出

global_value 的值和 global_value 的地址,当 cet==5 的时候,子进程改变 global_value 的值为 300,父进程不变,观察子进程和父进程的变化。


#include 
#include 

int global_value = 100;

int main()
{
    pid_t id = fork();
    if (id < 0) {
        printf("fork error\n");
        return 1;
    } else if (id == 0) {
        int cnt = 0;
        while (1) {
            printf("我是子进程, pid: %d, ppid: %d | global_value: %d, &global_value: %p\n", getpid(), getppid(),
                   global_value, &global_value);
            sleep(1);
            cnt++;
            if (cnt == 5) {
                global_value = 300;
                printf("子进程已经更改了全局的变量啦..........\n");
            }
        }
    } else {
        while (1) {
            printf("我是父进程, pid: %d, ppid: %d | global_value: %d, &global_value: %p\n", getpid(), getppid(),
                   global_value, &global_value);
            sleep(2);
        }
    }
    sleep(1);
}


vxbus@vxworks:~/github/code$ ./process 
我是父进程, pid: 581796, ppid: 90198 | global_value: 100, &global_value: 0x55fc3b48e010
我是子进程, pid: 581797, ppid: 581796 | global_value: 100, &global_value: 0x55fc3b48e010
我是子进程, pid: 581797, ppid: 581796 | global_value: 100, &global_value: 0x55fc3b48e010
我是父进程, pid: 581796, ppid: 90198 | global_value: 100, &global_value: 0x55fc3b48e010
我是子进程, pid: 581797, ppid: 581796 | global_value: 100, &global_value: 0x55fc3b48e010
我是子进程, pid: 581797, ppid: 581796 | global_value: 100, &global_value: 0x55fc3b48e010
我是父进程, pid: 581796, ppid: 90198 | global_value: 100, &global_value: 0x55fc3b48e010
我是子进程, pid: 581797, ppid: 581796 | global_value: 100, &global_value: 0x55fc3b48e010
子进程已经更改了全局的变量啦..........
我是子进程, pid: 581797, ppid: 581796 | global_value: 300, &global_value: 0x55fc3b48e010
我是父进程, pid: 581796, ppid: 90198 | global_value: 100, &global_value: 0x55fc3b48e010
我是子进程, pid: 581797, ppid: 581796 | global_value: 300, &global_value: 0x55fc3b48e010
我是子进程, pid: 581797, ppid: 581796 | global_value: 300, &global_value: 0x55fc3b48e010
我是父进程, pid: 581796, ppid: 90198 | global_value: 100, &global_value: 0x55fc3b48e010
我是子进程, pid: 581797, ppid: 581796 | global_value: 300, &global_value: 0x55fc3b48e010
我是子进程, pid: 581797, ppid: 581796 | global_value: 300, &global_value: 0x55fc3b48e010
我是父进程, pid: 581796, ppid: 90198 | global_value: 100, &global_value: 0x55fc3b48e010
我是子进程, pid: 581797, ppid: 581796 | global_value: 300, &global_value: 0x55fc3b48e010
我是子进程, pid: 581797, ppid: 581796 | global_value: 300, &global_value: 0x55fc3b48e010
我是父进程, pid: 581796, ppid: 90198 | global_value: 100, &global_value: 0x55fc3b48e010
我是子进程, pid: 581797, ppid: 581796 | global_value: 300, &global_value: 0x55fc3b48e010
我是子进程, pid: 581797, ppid: 581796 | global_value: 300, &global_value: 0x55fc3b48e010
我是父进程, pid: 581796, ppid: 90198 | global_value: 100, &global_value: 0x55fc3b48e010
我是子进程, pid: 581797, ppid: 581796 | global_value: 300, &global_value: 0x55fc3b48e010

这里我们发现一个很奇怪的现象为什么我们子进程 global_value 的值明明变为了 300 但是他的地址确实和 100 的地址是一样的。这是为什么?

如果我们继续按照在 C/C++ 上进行理解,指针指向的地址空间是唯一的。这就显然和这里的结果相矛盾一个相同的地址这么可能存放相同的值。这说明这里的指针绝对指向的不是[物理地址]要解释这个现象我们就不得不继续往下学习虚拟地址空间。

二、[虚拟地址]空间

1、浅谈虚拟地址空间

什么是虚拟地址空间呢?我相信有许多人都会有疑惑为什么会出现虚拟地址空间怎么个东西,这个问题下面会回答。但在此时我们要形成一个共识:一个进程会认为他独占系统资源 (实际上并不是如此)。

我们都知道在 C/C++ 的理解下,我们都认为有一块空间里面存放我们要执行的代码,但是在一个系统中不可能仅仅只有一个进程都在运行,那么也就说进程可能都把他们的要存储的内容分配到同一个空间中 (因为进程们都认为,独占空间),那怎么显然会出错,但是在同一个系统中仍然存在多个进程同时在运行。那系统是怎么解决这个问题的呢?

其实我们由此也可得出每个进程都有自己的内存空间,但是真正的空间分布是由操作系统完成的,而那个进程自己的空间,我们称为进程地址空间 (也就是虚拟地址空间)。

那么问题来,这个进程空间是怎么规划空间的呢?

我们要管理一个进程都是:先描述,在组织。

一个进程的空间他的基本大小空间是字节,我们拿 32 位的机器来下,将能形成 2^32 的编码,每个编码都是一个地址,也就形成了 4G 的空间。对于 PCB 来说有存一个指针,指向一个结构体的 struct mm_struct, 而这个结构体内就规划了各个空间的起始地址和终止地址。


struct mm_struct {
    //代码区
    size_t code_start, code_end;
    //数据段
    size_t data_start, data_end;
    //堆区
    size_t heap_start, heap_end;
    //栈区
    size_t stack_start, stack_end;
};

这样各个内存的区域就描述好了,我们就可以通过 PCB 中的指针管理这个结构体从而管理进程地址空间。

2、图解进程空间

进程地址空间

从上面图我们看到了什么?

当我们执行 my.exe 文件的时候,首先是从磁盘中把要执行的代码导入到内存中,pcb 就会所到 my.exe 要执行的指令,就会通过 mm * 指针让进程地址空间为 my.exe 分配好空间,在通过一个叫页表的结构和物理内存沟通,在真正的物理内存开好相应的空间。

从上面我们就知道以前我们说指针指向的内存空间,其实是虚拟地址空间。

理解了什么是进程地址空间,下面我们就解释一下为什么会出现我们前面讲的这个现象。


vxbus@vxworks:~/github/code$ ./process 
我是父进程, pid: 581796, ppid: 90198 | global_value: 100, &global_value: 0x55fc3b48e010
我是子进程, pid: 581797, ppid: 581796 | global_value: 100, &global_value: 0x55fc3b48e010
我是子进程, pid: 581797, ppid: 581796 | global_value: 100, &global_value: 0x55fc3b48e010
我是父进程, pid: 581796, ppid: 90198 | global_value: 100, &global_value: 0x55fc3b48e010
我是子进程, pid: 581797, ppid: 581796 | global_value: 100, &global_value: 0x55fc3b48e010
我是子进程, pid: 581797, ppid: 581796 | global_value: 100, &global_value: 0x55fc3b48e010
我是父进程, pid: 581796, ppid: 90198 | global_value: 100, &global_value: 0x55fc3b48e010
我是子进程, pid: 581797, ppid: 581796 | global_value: 100, &global_value: 0x55fc3b48e010
子进程已经更改了全局的变量啦..........
我是子进程, pid: 581797, ppid: 581796 | global_value: 300, &global_value: 0x55fc3b48e010
我是父进程, pid: 581796, ppid: 90198 | global_value: 100, &global_value: 0x55fc3b48e010
我是子进程, pid: 581797, ppid: 581796 | global_value: 300, &global_value: 0x55fc3b48e010

我们子进程 global_value 的值明明变为了 300 但是他的地址确实和 100 的地址是一样的。

进程地址空间

在 global_value 为变为 300 的时候,进程物理空间的指向是上图,这里简单解释一下:子进程是以父进程的模板生成的,所以他最初进程空间通过页表和物理内存的联系都应该是一样的,这也就导致子进程和父进程的 global_value 的地址都是一样的,但是当我们通过子进程改变 global_value 的值而父进程不变时。就会发现下图的变化:

进程地址空间

这里我们明白:进程具有独立性。这就要求子进程的改变不能影响父进程。

这时就操作系统就会发生写时拷贝,为子进程的 global_value 重新在物理内存开辟一块空间给他进行存储,但是进程空间中的地址并没有发生改变,只是子进程的页表的映射改变了,指向了 300 的空间地址。

而我们对global_value 取地址仅仅是得到的是进程空间的地址,这也就导致了为什么二个不同的值存储的空间会一样。

写时拷贝: 父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本,后在让进程改变。

进程地址空间