[Linux]——进程(4)

打印 上一主题 下一主题

主题 919|帖子 919|积分 2757




 
目录
 
一、媒介
二、正文
1.地址空间的概念
2.地址空间的意义
3.页表
4.总结和思考
三、结语

 
一、媒介

           本文我们将对进程中的地址空间和页表举行详细的讲解!
  二、正文

1.地址空间的概念

           在C/C++语言的学习中,我们常常会听到有人谈论起内存中地址的相干概念,其实在Linux中确切的概念叫做进程地址空间,对于每一个进程而言,都有其对应的进程地址空间,其内大抵空间分布如下图:
  

           上面的栈区,堆区,代码区,常量区,相信小伙伴们肯定耳熟能详了,对于栈是向下生长,堆是向上生长,而且图中这几个地域的地址分布也是有规律的,从最下面的正文代码,初始化数据,未初始化的数据一直到栈,命令行参数,他们的地址分布是从低到高的,但是我们只是听闻是这样,下面让我们来验证一下。
  1.   1 #include<stdio.h>
  2.   2 #include<string.h>
  3.   3 #include<stdlib.h>
  4.   4 #include<unistd.h>
  5.   5      
  6.   6 int g_val1=100;
  7.   7 int g_val2;
  8.   8 int main()
  9.   9 {   
  10. 10     printf("code addr:%p\n",main);
  11. 11     const char *str="hello world";
  12. 12     printf("read only string  addr:%p\n",str);
  13. 13     printf("init global value addr:%p\n",&g_val1);
  14. 14     printf("uninit global value addr:%p\n",&g_val2);
  15. 15     char *mem1=(char*)malloc(100);
  16. 16     char *mem2=(char*)malloc(100);
  17. 17     char *mem3=(char*)malloc(100);
  18. 18     printf("m1 heap addr:%p\n",mem1);
  19. 19     printf("m2 heap addr:%p\n",mem2);
  20. 20     printf("m3 heap addr:%p\n",mem3);
  21. 21     printf("stack addr:%p\n",&str);
  22. 22     int a;
  23. 23     int b;
  24. 24     int c;
  25. 25     printf("a stack addr:%p\n",&a);
  26. 26     printf("b stack addr:%p\n",&b);
  27. 27     printf("c stack addr:%p\n",&c);
  28. 28     return 0;                                               
  29. 29 }   
  30. 30      
复制代码

           上面只是地址空间的大概划分,那么什么叫做地址空间所谓的地址空间,本质是一个猫叔进程可视范围的大小,地址空间内肯定要存在各种地域划分,即对线性地址举行start和end即可
  2.地址空间的意义

           那么为什么要有地址空间呢,也就是它存在的意义是什么?
          其一是让进程以统一的视角对待内存,因为有了地址空间的存在,进程无需再管现实分配的物理地址在物理内存是怎样的一个分布,在进程看来都只有一个CPU分配的地址空间,进程直接使用它就可以了。
          其二是增加进程虚拟地址空间可以让我们访问内存的时候,增加一个转换的过程,在这个转换的过程中,可以对我们的寻址请求举行审查,以是一旦非常访问,直接拦截,该请求不会到达物理内存,保护物理内存。就说我们是小孩子的时候,父母总会要求我们将压岁钱交给他们管理,当我们想要买东西的时候找他们拿即可,如果我们买一个10元的文具盒,父母会给我们10元,但是我们想买一个200元的游戏机,父母可能就会以好好学习为由制止我们的请求,与这个道理是类似的。
          其三就是有了地址空间和页表的存在,就可以将进程管理模块和内存管理模块举行解耦合,因为在进程看来它只必要处理虚拟地址即可,无需关注内存是如何存放和处理代码和数据的。
   
  3.页表

           在讲解页表之前,我们先来回顾之前创建子进程的一个小尾巴,就是当我们在folk创建一个子进程时,当时我们发现falk函数的返回值在父子进程的值是不一样的,子进程为0,父进程为创建子进程的id,那么这到底是怎么实现的,让我们我们先来看看下面这几段代码的结果
  1.   1 #include<stdio.h>
  2.   2 #include<string.h>
  3.   3 #include<stdlib.h>
  4.   4 #include<unistd.h>
  5.   5               
  6.   6 int g_val=100;
  7.   7 int main()     
  8.   8 {              
  9.   9       pid_t id=fork();                                                                                                                    
  10. 10       if(id==0)
  11. 11       {        
  12. 12           int cnt=5;
  13. 13           //子进程
  14. 14           while(1)
  15. 15           {   
  16. 16               printf("I'm a child, pid: %d, ppid: %d, g_val: %d, &g_val: %p\n",getpid(),getppid(),g_val,&g_val);
  17. 17               sleep(1);
  18. 18           }   
  19. 19       }        
  20. 20       else     
  21. 21       {        
  22. 22           //父进程
  23. 23           while(1)
  24. 24           {   
  25. 25               printf("I'm a parent, pid: %d, ppid: %d, g_val: %d, &g_val: %p\n",getpid(),getppid(),g_val,&g_val);
  26. 26               sleep(1);
  27. 27           }   
  28. 28       }      
  29. 29    return 0;
  30. 30 }
复制代码

           上面这一段代码中,我们在父子进程中分别打印g_val的值和地址,我们发现两者是一模一样的,这也和我们对子进程会对父进程的资源举行继承的认知是相符合的,但是当我们对该值举行修改,结果又会如何呢? 
  1. pid_t id=fork();
  2. 10       if(id==0)
  3. 11       {        
  4. 12           int cnt=5;
  5. 13           //子进程
  6. 14           while(1)
  7. 15           {   
  8. 16               printf("I'm a child, pid: %d, ppid: %d, g_val: %d, &g_val: %p\n",getpid(),getppid(),g_val,&g_val);
  9. 17               sleep(1);
  10. 18               if(cnt) cnt--;
  11. 19               else
  12. 20               {
  13. 21                   g_val=200;
  14. 22                   printf("子进程chang:100->200\n");
  15. 23               }                                                                                                                 
  16. 24           }   
  17. 25       }        
  18. 26       else     
  19. 27       {        
  20. 28           //父进程
  21. 29           while(1)
  22. 30           {   
  23. 31               printf("I'm a parent, pid: %d, ppid: %d, g_val: %d, &g_val: %p\n",getpid(),getppid(),g_val,&g_val);
  24. 32               sleep(1);
  25. 33           }   
  26. 34       }        
复制代码

           当我们在子进程中修改g_val的值的时候,按照我们的认识,这时候子进程应该对其举行写实拷贝,即深拷贝,那么这时候打印出来g_val的地址在父子进程中应该是不雷同的,但是现实情况我们发现地址却仍旧是雷同的,但是他们的值确实不同的?
          这也就阐明白这个g_val的地址并不是现实的物理地址,而是一种线性地址,或者说常说的虚拟地址,那么到底是怎么做到虚拟地址雷同的情况下,值确不一样,这就是涉及到页表了。
   

           我们之前在讲解进程的时候,说到进程是由PCB和代码数据组成的,今天我们又可以进一步丰富进程的概念。进程是由PCB,程序地址空间,页表和存储在物理内存中的代码和数据组成的
          那么对于页表,首先我们先来了解什么是页表?简单来说,其由三部分组成,第一部分是虚拟地址,第二部分是虚拟地址对应到物理内存中的物理地址以及标志位,标志位即标定物理地址中存储数据是可读的还是可读写的,这也是为什么代码区和常量区的代码和数据我们不能修改的缘故起因
          其次,我们再来了解页表的周边知识。一个是CPU中的cr3寄存器,该寄存器中存放的是页表的地址,也属于进程在硬件的上下文,因此在举行进程切换的时候,cr3寄存器中页表的地址也会被所属进程打包带走,方便下次再次执行进程时上下文的恢复。
          再而是惰性加载,我们都知道对与一个很大的文件,操作体系有时候并不能一下子将这个文件的全部内容加载到磁盘上,往往是采取分批加载的方式,比方对于一个40G的文件,一次加载300M,或者是500M这样子。在了解加载这个过程后,那么什么是惰性加载呢,就是比如说操作体系在加载一个进程的时候,它可以一下子加载说500M的代码和数据,但是它并不会这样做,因为你进程自己可能一下子就跑5MB的代码或者用到的数据没那么多,如果对于每一个进程CPU都这样做的话就会极大的占据CPU的资源,因此它并不会一下子加载那么多,而是按需加载,即在必要时才加载资源或对象,而不是在程序启动时立即加载所有资源。这种技能可以有效减少初始开销,进步应用性能和用户体验。
          最后就是缺页制止,其定义是‌‌是指在执行指令时,所需访问的页面不在内存中,导致制止当前指令的执行,并从外存(如硬盘)中加载所需页面到内存的过程。缺页制止是分页存储中的一种常见现象,重要发生在虚拟内存体系中。那么在进程中,就是说当一个进程在创建的时候,其实是先创建内核的数据结构,即地址空间,页表等,这时候页表中可能会有虚拟地址,但是现实的物理地址还并没有分配,只有当你这个进程开始执行的时候,用到哪些代码和资源,再触发缺页制止,为页表分配物理地址。
  4.总结和思考

           通过对地址空间和页表的学习,我们更一步的深入的对进程的了解,进程是由内核数据结构(task_struct && mm_struct && 页表)+程序的代码和数组组成的
          最后,看完本文,小伙伴们能不能办理一下几个问题呢:
  ①地址空间的概念
  ②地址空间的意义
  ③如何做到让代码和字符常量区是只读的
  ④惰性加载的概念
  ⑤什么是缺页制止
  三、结语

           
          到此为止,本文有关地址空间和页表的讲解就到此结束了,如有不足之处,欢迎小伙伴们指出呀!
            关注我 _麦麦_分享更多干货:_麦麦_-CSDN博客
        各人的「关注❤️ + 点赞

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

张国伟

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表