• 【Linux】第六章 进程地址空间(程序在内存中存储+虚拟地址+页表+mm_struct+写实拷贝+解释fork返回值)


    🏆个人主页企鹅不叫的博客

    ​ 🌈专栏

    ⭐️ 博主码云gitee链接:代码仓库地址

    ⚡若有帮助可以【关注+点赞+收藏】,大家一起进步!

    💙系列文章💙


    【Linux】第一章环境搭建和配置

    【Linux】第二章常见指令和权限理解

    【Linux】第三章Linux环境基础开发工具使用(yum+rzsz+vim+g++和gcc+gdb+make和Makefile+进度条+git)

    【Linux】第四章 进程(冯诺依曼体系+操作系统+进程概念+PID和PPID+fork+运行状态和描述+进程优先级)

    【Linux】第五章 环境变量(概念补充+作用+命令+main三个参数+environ+getenv())



    💎一、程序地址空间

    🏆1.验证程序地址空间

    下面是内存区域图片,程序/进程地址空间是操作系统上的概念,和我们物理内存本身不是一个东西

    在这里插入图片描述

    用代码验证不同区域区别

    #include
    #include
    
    int un_global_val;//未初始化全局变量
    int global_val=100;//已初始化全局变量
    //main函数的参数
    int main(int argc, char *argv[], char *env[])
    {
     printf("code addr         : %p\n", main);
     printf("init global addr  : %p\n", &global_val);
     printf("uninit global addr: %p\n", &un_global_val);
     char *m1 = (char*)malloc(100);
     char *m2 = (char*)malloc(100);
     char *m3 = (char*)malloc(100);
     char *m4 = (char*)malloc(100);
     int a = 100;
     static int s = 100;
     printf("heap addr         : %p\n", m1);
     printf("heap addr         : %p\n", m2);
     printf("heap addr         : %p\n", m3);
     printf("heap addr         : %p\n", m4);
    
     printf("stack addr        : %p\n", &m1);
     printf("stack addr        : %p\n", &m2);
     printf("stack addr        : %p\n", &m3);
     printf("stack addr        : %p\n", &m4);
     printf("stack addr a      : %p\n", &a);
     printf("stack addr s      : %p\n", &s);
     printf("\n");
     for(int i = 0; i < argc; i++)
     {
         printf("argv addr         : %p\n", argv[i]);
     }
     printf("\n");
     for(int i =0 ; env[i];i++)
     {
         printf("env addr          : %p\n", env[i]);
     }
     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
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    结果如下:

    在这里插入图片描述

    🏆2.通过fork验证进程地址空间

    我们运行以下代码

    #include 
    #include 
    #include 
    #include 
    
    int main()
    {
     int test = 10;
     int ret = fork();
     int count = 0;
     if(ret == 0)
     {
         while(1)
         {
             printf("我是子进程%d,ppid:%d,test:%d,&test: %p\n\n",getpid(),getppid(),test,&test);
             sleep(1);
             count++;
             if(count == 5)
             {
                 test = 20;
                 printf("数据被修改,注意查看!\n");
             }
         }
     }
     else
     {    
         while(1)
         {
             printf("我是父进程%d,ppid:%d,test:%d,&test: %p\n\n",getpid(),getppid(),test,&test);
             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
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    结果:

    在这里插入图片描述

    子进程和父进程打印的test值不一样,但是其地址却完全相同!

    💎二、进程地址空间

    🏆1.简述进程地址空间

    每一个进程启动的时候,都会让操作系统给他创建一个地址空间,这个空间就是进程地址空间。

    • 先描述再组织的理念,进程地址空间其实是操作系统内核的一个数据结构struct mm_struct
    • 之前提到过进程具有独立性,在多进程运行的时候,数据和代码是独立的,需要独享各种资源。而进程地址空间的作用,就是让进程认为自己是独占操作系统中的所有资源。

    进程地址空间的最大意义就是维护进程独立性

    虚拟地址

    在 C/C++ 中使用的地址绝对不是物理地址,叫虚拟地址,又称为线性地址。操作系统不会让我直接看到物理地址,他怕你修改坏坏,但是内存是一个硬件他并不能阻止你去访问,但是他会被动的进行读取和写入。

    页表

    当磁盘中数据传输到物理内存时,我们需要将虚拟地址空间和物理内存之间建立映射关系,程序加载到内存变成进程后,操作系统会给每一个进程构建一个页表,地址空间区域其实存的是对应的虚拟地址,虚拟地址传过来在页表中转换成物理地址然后才能找到物理内存地址。

    mm_struct

    当进程需要申请内存的时候,本质就是操作系统在mm_strcut中修改不同区域的end

    在这里插入图片描述

    如果范围需要调整可以在 end 或者 strat 加上特定调整值即可。由此每个区域范围,都是可以有对应编号的,再进行分区,将每个区域以链表形式组织起来,就可以抽象出 mm_struct 结构:

    struct mm_struct
    {
    long init_start;//全局变量
    long init_end;
    
    long uninit_start;//未初始化变量
    long uninit_end;
    
    long heap_start;//堆区
    long heap_end;
    
    long stack_start;//栈区
    long stack_end;
    ……
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    🏆2.程序地址空间编译过程-程序变进程

    • 程序编译出来,没有被加载的时候,程序内部有地址(如果没有地址,无法进行链接)
    • 程序编译出来,没有被加载的时候,程序内部有区域readelf -s 可执行文件可以查看区域,程序内部的地址和内存中的地址是没有关系,磁盘中的代码每一部分在什么区域是已经确定好了的,地址编好加载到内存,起始偏移量从 0 开始由 0000~FFFF 进行编址,磁盘中的虚拟地址转换内存中是不变的,至于加载到了那个地方就由页表进行映射,当程序被加载到内存当中时,假设系统将该程序的代码从内存0x100开始加载,就可以依照程序编址的数据加上这个偏移量,从而存放在内存中

    🏆3.写时拷贝

    当父子进程其中有人发生修改时,就会发生写时拷贝去构建新的物理内存,实质虚拟地址是不发生变化的,是改变页表右侧的映射关系,也就能解释内容不一样但是地址一样的情况,通过页表将父子进程以写时拷贝方式进行分离。

    最先开始父子进程指向同一个物理内存

    在这里插入图片描述

    当子进程尝试修改test变量的时候,操作系统就会开始一个写时拷贝,开辟一个新的空间,将对应的值考入该空间,再重新映射页表,虽然页表左侧的虚拟地址没有变化,但是映射的物理地址已经不一样了。

    在这里插入图片描述

    不管是写入还是回收,写时拷贝都保证了进程的独立性原则。

    🏆4.解释fork两个返回值

    fork 的 pid_t id,这个变量是属于父进程栈空间中定义的变量, fork 内部 return 会执行两次,return的本质是通过寄存器将返回值写入到接收变量中。当 id = fork() 时,谁先返回谁就要发生写时拷贝,所以同一个变量会有不同值,本质是因为大家的虚拟地址是一样的,但是物理地址是不一样的

    🏆5.进程地址空间的意义

    内存作为一个硬件,不阻止你读写

    • 保护内存,程序地址空间让访问内存时添加了一层软硬件层,可以对转化过程进行审核,拦截非法的访问
    • 功能模块解耦,程序地址空间还可以延迟用户的内存使用。比如我们现在malloc了100个字节的空间,实际上操作系统并不会立马给你申请空间,而是操作你的mm_struct让进程以为自己已经申请成功了。当程序真正使用这个空间的时候,操作系统才会去物理内存中进行映射,提高运行效率
    • 统一进程/程序,以统一的方式编译加载所有可执行程序,简化程序本身的设计和实现,内存的区域都是自下而上的预留区,代码段,堆区,栈区等等。编译器就可以以统一的方式编址,我们也方便去寻址

  • 相关阅读:
    计算机网络——应用层重点协议【HTTP协议】
    内耗自救指南|5招停止内耗让你逆风翻盘
    Blockchain for Internet of Energy management: Review, solutions, and challenges
    【Go语言】Go项目工程管理
    2018-Adversarial Learning for Semi-Supervised Semantic Segmentation
    SpringBoot 使用过滤器修改请求参数
    如何在 Windows 11/10 中重命名或删除 SoftwareDistribution 文件夹
    vue2 生命周期钩子函数
    判断是不是二叉搜索树
    SpringCloud之服务发现
  • 原文地址:https://blog.csdn.net/YQ20210216/article/details/127434024