• 操作系统——程序地址空间



    前言:学过内存管理的同志,应该都看过下图。我们一般认为下图就是真正的物理内存。但是这并不是真正的内存,听到这有的人懵了,内存就是这样的呀,学的就是如此。说一句网络都是虚拟的,水太深,把握不住。一样,以下是虚拟内存。程序是如何和物理内存打交道的,本篇娓娓道来。
    在这里插入图片描述


    1. 一段令人困惑的程序

    #include 
    #include 
    #include 
    int g_val = 0;
    int main()
    {
     pid_t id = fork();
     if(id < 0){
     perror("fork");
     return 0;
     }
     else if(id == 0)
     {
      g_val=1000;
      printf("child:change-> g_val:%d,%p\n",g_val,&g_val);
     }
     else 
     {
       printf("father:my->g_val:%d,%p\n",g_val,&g_val);
       sleep(3);
     }
     sleep(1);
     return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    我们知道,子进程会继承父进程的代码和数据。但是我在子进程中修改数据会发生写时拷贝。写时拷贝后面具体讲。上面的代码非常明显:我在子进程中将g_val修改为1000,父进程的g_val不受影响,每个进程都有独立性,这个大家都懂。那么g_val的地址应该会不同,因为同一个地址只能有一个值,我们来看看子进程和父进程的g_val地址是否相同。
    在这里插入图片描述
    惊奇的发现,g_val的地址尽然相同。这是打破以往常识的。

    • 每个地址只能有一个值
    • 以上的地址绝对不是物理地址,而是虚拟地址
    • os负责将虚拟地址和物理地址产生联系

    2. 虚拟地址空间

    每个进程都有自己的代码和空间,进程可能需要对物理地址进行操作。但是如何管理进程代码在内存中的存储呢?让每个进程都可以直接和物理内存打交道,是威胁的行为,而且管理起来很复杂。
    比如:我在内存中有两个进程,如果直接让进程操作物理地址,那么它有没有可能会占用另一个进程的物理地址,是有可能的,会造成很大的危害。所以引入虚拟地址。


    为了方便管理所有的进程,每个进程都由struct mm_struct这一虚拟地址空间来管理。每个进程都认为自己拥有os的所有内存。

    在这里插入图片描述
    进程是由PCB来管理的,PCB中就有一个指针指向了mm_struct。mm_struct就存储了进程的代码和数据。

    2.1 虚拟地址空间mm_struct如何实现空间分段

    可以看到上图中,mm_struct被分成多段,如何实现的呢?其实比较简单。
    我们来假设实现,不是源代码哦。

    struct mm_struct
    {
     unsignde int code_start;
     unsignde int code_end;
     unsignde int date_start;
     unsignde int date_end;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在32位下,内存是4GB,每个进程都按照4GB来规划虚拟地址空间。就是如上那样 [_start ,_end]为一个
    段。那么就是从0X00000000……000~0Xfffffffff……fffff这样的来规划虚拟地址空间的。

    2.2 虚拟地址和物理地址如何产生关联

    虚拟地址是按照操作系统所有的内存,来规划的。那么该如何真正的使用物理地址呢?那就是构建映射关系:页表。
    利用页表我们可以使虚拟地址和物理地址产生映射关系。
    在这里插入图片描述

    2.3 子进程如何继承父进程的代码和数据

    子进程会继承父进程的代码和数据,子进程的PCB是以父进程的PCB为模板来创建的,当然不是完全拷贝父进程的PCB,如PID,PRI等就不一样。那代码和数据是如何共享的呢?
    在这里插入图片描述
    嗯,就是这样的继承的。

    2.4 写时拷贝

    页表中有一个权限,不知道大家注意到没有,每次操作都会对于页表中的权限,如果子进程一直都是读代码,那很简单,啥呀不用管;要是子进程要进行写入操作呢?就比如一上来的代码要进行修改变量的值,该怎么办呢?->写时拷贝。

    假如子进程要修改变量,g_val。
    (1)一开始是这样的,
    在这里插入图片描述
    (2)但是子进程,要修改g_val的值了。页表上的权限也表示你可以修改,但是在此之前你先稍等,这就是断页中断,你的先拷贝一下,物理地址中的g_val需要拷贝一份来供你修改,这就是写时拷贝。
    在这里插入图片描述
    (3)可以看到,发生写时拷贝后,g_val在物理地址上多了一份,这就保证了父子进程的独立性。在页表中断时,父子进程什么影响都没有,子进程只不过是要修改数据,页表中断,发生写时拷贝,这都是操作系统干的事。

    总结:子进程会默认和父进程代码数据指向同一个物理地址,如果只读那么就相安无事,若要修改数据,那么会发生页表中断来完成写时拷贝,供给子进程来修改。所以默认情况下,只读的代码和数据,操作系统只维护一份;如果要求写入操作,那么会根据具体情况,发生写时拷贝。

    3. 解释那令人困惑的程序

    有了以上内容的了解,基本上我们都懂了,g_val的虚拟地址是一样的,但是物理地址是分开的。物理地址不同,所以其实本质上是两个变量了。
    昂,虚拟的确实不可信哟,但是虚拟的确实很香,香在哪里呢?

    4.虚拟地址空间的好处

    • 保护了每个进程的物理内存空间,使得进程的代码和数据风险大大降低
    • 提升了操作系统管理的效率,每个虚拟地址空间都是按照4GB的方式规划内存
    • 内存申请和内存使用,时间,空间效率都提升
  • 相关阅读:
    startUML设计
    股票价格预测项目
    Kubernetes: kube-controller-manager 源码分析
    goerli 测试网资源整理
    clustershell
    【MATLAB】羽状图
    python中pickle向redis中存储数据
    SpringCloud(二) 用Eureka做服务注册中心认证
    监控脱岗离岗行为检测系统
    Spring5源码-事务的创建、回滚、提交
  • 原文地址:https://blog.csdn.net/lyzzs222/article/details/126661174