• 进程地址空间--Linux


    目录

    🚩前言

    🚩程序(虚拟)地址空间

    图解

    程序地址空间验证

    🚩页表

    页表的结构

    页表的作用 

    挨个访问4G虚拟地址会怎样?

    🚩父子进程的地址空间理解 

    对进程进行写入操作

    父子进程地址图解

    🚩总结


    🚩前言

    谈及进程,我们对进程的概念并不陌生,但对于进程所在的地方,我们得搞清楚。那么我们平常写的程序运行起来之后,所在的进程是直接放在了内存上吗?其实并不是,我们平常所看到的进程地址空间是程序地址空间,也叫虚拟地址空间。

    这种地址空间处理实际上是对内存空间的高效利用和保护,我们后面再讲。

    🚩程序(虚拟)地址空间

    为什么叫虚拟地址空间呢?

    原因在于每个进程不可能完全占有整个内存空间,往往只需要一部分就够了。为了保证内存空间的高效利用,操作系统每次只会给进程一部分内存空间,并让进程以为自己拥有了完整的内存空间--虚拟地址空间诞生。

    图解

    程序地址空间验证

    代码: 

    1. #include
    2. #include
    3. #include
    4. int init_val=100;
    5. int uninit_val;
    6. int main(int argc, char* argv[],char* env[])
    7. {
    8. const char* c_str="abcdef";
    9. printf("code addr :%p\n",main);//代码区
    10. printf("read_only addr :%p\n",c_str);//常量区
    11. printf("init_val addr :%p\n",&init_val);//初始化变量
    12. printf("uninit_val addr :%p\n",&uninit_val);//未初始化变量
    13. int* p1=(int*)malloc(sizeof(int)*10);
    14. int* p2=(int*)malloc(sizeof(int)*10);
    15. int* p3=(int*)malloc(sizeof(int)*10);
    16. int* p4=(int*)malloc(sizeof(int)*10);
    17. printf("heap addr :%p\n",p1);//堆区
    18. printf("heap addr :%p\n",p2);
    19. printf("heap addr :%p\n",p3);
    20. printf("heap addr :%p\n",p4);
    21. printf("stack addr :%p\n",&p1);//栈区
    22. printf("stack addr :%p\n",&p2);
    23. printf("stack addr :%p\n",&p3);
    24. printf("stack addr :%p\n",&p4);
    25. int i=0;
    26. for(i=0;i//命令行参数
    27. {
    28. printf("argv addr :%p\n",argv[i]);
    29. }
    30. for(i=0;env[i];++i)//环境变量
    31. {
    32. printf("env addr :%p\n",env[i]);
    33. }
    34. return 0;
    35. }

     运行结果:

    从上面的图片可以看出:我们自己写的程序的地址空间,刚好和程序地址空间的布局相同。

    注:每个进程都有属于自己独有的地址空间!并且在用户看来,每个进程的地址空间都是4G。这显然是不可能的,具体的处理方式是什么呢?答案在于页表这个特殊的结构里!

    🚩页表

    既然我们知道了以往的地址空间是虚拟的 ,那么它是怎么和真实存在的物理空间连接起来的呢?

    这里我们就要引入页表这个概念了。

    将程序加载到内存中并转换为进程时,操作系统会自动生成一个对应进程的页表。

    页表的结构

    页表的作用 

    当调度器把相应的进程加载到cpu时,cpu会访问该进程中的地址来运行和计算,但是虚拟的地址是不可能被直接访问的,被访问的还是真实存在的物理空间。因此这中间的连接转化工作就交给了页表。

    页表其实保存了两种数据:虚拟地址和物理地址。并且这两个地址严格左右对照。每次cpu访问进程的地址时,都是通过页表先找到虚拟地址,再访问对应着的物理地址。

    挨个访问4G虚拟地址会怎样?

    🔺非法访问!

    我们在写代码的时候,经常会出现非法指针访问的现象。原因就在于,我们此时访问的地址页表里面很有可能没有,也就是没有真实的物理空间对应着。这样肯定是会报错的。

    同时也可以看出,页表相当于是一个检查站,只有合格的地址访问,才会被允许。否则内存随便访问的风险就太大了一点。

    🚩父子进程的地址空间理解 

    我们知道,父进程和子进程是共用一套代码的。那我们是不是可以合理地推测父子进程的地址空间也同样是共用的呢?

    大部分情况下,上述猜测是成立的。但有一种情况比较特殊,就是涉及到对程序进行写入操作时,父子进程的地址就会稍微有点出入了。

    对进程进行写入操作

    1. #include
    2. #include
    3. #include
    4. int main()
    5. {
    6. int g_val=10;
    7. pid_t id=fork();
    8. if(id==0)//子进程
    9. {
    10. while(1)
    11. {
    12. printf("子进程:pid:%d ppid:%d g_val:%d &g_val:%p\n",getpid(),getppid(),g_val,&g_val);
    13. g_val+=10;//相当于写入改变数据
    14. sleep(1);
    15. }
    16. }
    17. else//父进程
    18. {
    19. while(1)
    20. {
    21. printf("父进程:pid:%d ppid:%d g_val:%d &g_val:%p\n",getpid(),getppid(),g_val,&g_val);
    22. sleep(1);
    23. }
    24. }
    25. return 0;
    26. }

    运行结果:

    可以看出g_val的地址一样,但是却出现了不一样的值。是不是似曾相识?这不是和fork()函数返回值一样吗?

    原因其实很简单,在发生写入操作时,为了避免写入部分造成未写入进程的逻辑发生变化(例:子进程的一个变量在父进程中充当条件语句的判别值),写入时会进行写时拷贝,将被写入的数据再另开辟一个。

    因为是在物理空间上直接开辟,反映到虚拟地址上时,仍是和原来一样的地址空间,因此在我们看来,一样的地址空间里存了两个不同的值。

    父子进程地址图解

    🚩总结

    进程之间的独立性

    由于写时拷贝的存在,虽然父子进程共用一套地址空间,但只要一方写入,就会自动进行拷贝分离,这样就不会出现地址冲突的问题。

    虚拟地址设计思路

    让进程或者程序可以以种统一的视角看待内存!方便以统一的方式来编译和加载所有的可执行程序。

    虚拟地址的作用

    1.以页表为隔离层,将物理内存保护起来。

    2.使内存申请和内存使用分离,通过虚拟地址空间来屏蔽底层内存申请的过程(进程也不再关心该过程),从而达到进程和操作系统进行内存管理操作,将进程调度和内存管理进行解耦

    3.由设计思路可知,虚拟地址可以提高cpu的运行效率,每个进程在cpu看来都是一样的地址空间和处理方式。

    自我感受

    虚拟地址是学习进程地址空间的重点,要理解虚拟地址的高效内存空间利用率。内容就到这里了,拜拜~😋

  • 相关阅读:
    统计Excel单元格中某个字符出现的次数
    【文末赠书】SRE求职必会 —— 可观测性平台&可观测性工程(Observability Engineering)
    【zabbix】MySQL模板创建与监控
    【C语言趣味教程】(6) 作用域:局部变量 | 全局变量 | 局部变量优先原则 | 利用大括号限制作用域 | 变量的生命周期
    c++实现Json配置数据序列化和反序列化
    linux 限制带宽命令,Linux服务器限制网络带宽流量速率(限制应用程序和指定网卡的流量)
    ChatGPT规模化服务的经验与教训
    【linux】倒计时小程序
    OpenVPN 介绍
    Postman for Mac(HTTP请求发送调试工具)v10.18.10官方版
  • 原文地址:https://blog.csdn.net/qq_63412763/article/details/127129619