0


【Linux】进程地址空间

引入

🎃我们写一个这样的程序,运行并观察其输出结果。

#include <iostream>
#include <unistd.h>
#include <cstdio>
#include <cerrno>
#include <cstring>

using namespace std;

int main()
{
    int value = 10;   //定义一个变量
    pid_t id = fork();
    if (id == 0)   //子进程
    {
        while(1)
        {
            value++;    //改变value的值
            printf("我是子进程,我的pid是: %d,value是: %d,&value是: %p\n",getpid(),value,&value); //输出变量值
            sleep(1);
        }
    }
    else if (id > 0)  //父进程
    {
        while(1)
        {
            printf("我是父进程,我的pid是: %d,value是: %d,&value是: %p\n",getpid(),value,&value);  //输出变量值
            sleep(1);
        }
    }
    else  //fork出错
    {
        cout << errno << ": " << strerror(errno) << endl;
    }
    return 0;
}

🎃可以观察到,父子进程都有一个 value 值,且共用一个地址。而子进程改变 value 时父进程的 value 却不受影响。

🎃假设,我们使用的这个地址就是物理地址的话,可能会出现读取同一个地址而出现两个不同的值的情况吗?

🎃因此我们得出一个结论: 在语言层面用的地址并非物理地址,而是虚拟地址。

🎃我们在用C/C++语言所看到的地址,全部都是虚拟地址!而用户一概看不到物理地址,而是由OS统一管理

🎃我们使用的这个虚拟地址总的叫进程地址空间又叫 mm_struct ,是一种线性的结构,其通过页表的某种映射关系,从而找到物理内存。

进程地址空间

讲个故事吧:有个大富翁,他有很多的私生子。每个私生子不知道彼此的存在,因此都觉得大富翁的所有财产本质上也是自己的。因此,需要用钱的之后,只要找大富翁要就可以了,但也不能一下子要太多,否则大富翁会拒绝这种无礼的请求。而大富翁为了避免出现忘记给儿子们画的饼的尴尬情况,因此需要将他曾经画过的饼都管理起来。

🎃对号入座之后我们便能够发现,大富翁就是OS,私生子就是一个个进程。而其画的饼其实就是进程地址空间,本质上就是一个内核数据结构,struct mm_struct 。

虚拟地址与物理地址

如何理解虚拟地址的不同区域

🎃由于mm_struct本质上是一个线性结构,因此只要对线性区域进行指定 start 和 end 即可完成区域的划分,因此 mm_struct 内都是一段一段区域的划分,区域之间就叫做虚拟地址或线性地址。将来只需要修改区域边界的位置便可以修改区域的大小。

🎃之后 mm_struct 便可以借助页表与MMU(内存管理单元)与物理内存确立映射关系、建立联系。

🎃不仅如此,页表之中还存储了对于该空间的权限,正如一个常量区之中的字符串,我们可以对其读取却无法更改其内容。便是因为在页表之中,我们对该空间只有读权限而没有写权限。因此无法进行写入。

写时拷贝

🎃这时我们就可以回过头来讲讲,如何做到用一个地址却能得到两个值?

🎃在父进程创建子进程之前,申请了一个变量 value,因此在进程地址空间中存在一个地址,能够通过映射找到物理内存中的 value。

🎃之后父进程创建了子进程,子进程会继承父进程的进程地址空间,因此二者存储 value 的虚拟地址是相同的,并映射到同一块物理内存。若子进程未进行写入,则两个进程便会保持原有的映射关系。

🎃若子进程对值进行修改,便会触发写时拷贝,在物理内存的新地址中拷贝一份新的值进行修改,之后修改页表的映射,指向这块区域。

🎃值得注意的是,哪个进程先进行修改,哪个进程就触发写时拷贝。

动态开辟的细节

🎃不知道是否想过一个问题,动态开辟的空间是一申请就给我们呢?还是使用的时候再给?

🎃在动态开辟在申请时到使用前有个空窗期,这段时间内空间就被闲置了,而 OS 一般不允许任何的浪费和不高效的操作。

🎃因此动态开辟时值分配虚拟内存,当要使用该空间时再分配物理内存,完善页表之中的映射关系。

为什么存在进程地址空间

避免地址被随意访问

🎃若直接使用物理内存,则在访问空间时无法检测野指针问题,放任指针随意地修改,可能会使数据受损或导致其他进程崩溃出错。

进程管理和内存管理解耦合

🎃如此使用进程地址空间后,管理进程时并不关心内存是如何管理的。

🎃同理,管理内存是也不关心进程是如何管理的,实现了进程管理和内存管理的解耦合。再使用页表将二者关联起来。

使进程用统一的视角看待代码和数据

🎃原代码被编译的时候就是按照虚拟地址空间的方式,对代码和数据完成了对应的编制。

🎃这是由于虚拟地址这样的策略并不只影响 OS,编译器也要遵守。

🎃因此在内存中打开文件时,自然就有了对应的物理地址,之后在运行的时候若对函数进行调用,通过映射就会找到函数的虚拟地址(当前物理地址中存的是虚拟地址),再通过映射便可以找到函数体(再通过映射找到存函数体的物理地址)。

🎃因此 cpu 中读取的都是虚拟地址。 ---这样便实现使进程用统一的视角看待代码和数据。

🎃且进程的代码和数据并非一直都在内存中,而是用多少加载多少,不再使用就将其从内存之中移除


🎃好了,今天进程地址空间的讲解到这里就结束了,如果这篇文章对你有用的话还请留下你的三连加关注

标签: linux 运维 服务器

本文转载自: https://blog.csdn.net/Lin_Alpaca/article/details/130474102
版权归原作者 LinAlpaca 所有, 如有侵权,请联系我们删除。

“【Linux】进程地址空间”的评论:

还没有评论