首页 最新 热门 推荐

  • 首页
  • 最新
  • 热门
  • 推荐

[Linux]——进程(4)

  • 25-02-16 06:40
  • 4107
  • 10140
blog.csdn.net

 

目录

 

一、前言

二、正文

1.地址空间的概念

2.地址空间的意义

3.页表

4.总结和思考

三、结语


 

一、前言

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

二、正文

1.地址空间的概念

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

        上面的栈区,堆区,代码区,常量区,相信小伙伴们一定耳熟能详了,对于栈是向下生长,堆是向上生长,并且图中这几个区域的地址分布也是有规律的,从最下面的正文代码,初始化数据,未初始化的数据一直到栈,命令行参数,他们的地址分布是从低到高的,但是我们只是听闻是这样,下面让我们来验证一下。

  1. 1 #include
  2. 2 #include
  3. 3 #include
  4. 4 #include
  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
  2. 2 #include
  3. 3 #include
  4. 4 #include
  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博客
        大家的「关注❤️ + 点赞? + 收藏⭐」就是我创作的最大动力!谢谢大家的支持,我们下期见!

 

 

一枚积极学习,乐于分享的苏苏子
QQ名片
注:本文转载自blog.csdn.net的_麦麦_的文章"https://blog.csdn.net/m0_73953114/article/details/145306765"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接
复制链接
相关推荐
发表评论
登录后才能发表评论和回复 注册

/ 登录

评论记录:

未查询到任何数据!
回复评论:

分类栏目

后端 (14832) 前端 (14280) 移动开发 (3760) 编程语言 (3851) Java (3904) Python (3298) 人工智能 (10119) AIGC (2810) 大数据 (3499) 数据库 (3945) 数据结构与算法 (3757) 音视频 (2669) 云原生 (3145) 云平台 (2965) 前沿技术 (2993) 开源 (2160) 小程序 (2860) 运维 (2533) 服务器 (2698) 操作系统 (2325) 硬件开发 (2491) 嵌入式 (2955) 微软技术 (2769) 软件工程 (2056) 测试 (2865) 网络空间安全 (2948) 网络与通信 (2797) 用户体验设计 (2592) 学习和成长 (2593) 搜索 (2744) 开发工具 (7108) 游戏 (2829) HarmonyOS (2935) 区块链 (2782) 数学 (3112) 3C硬件 (2759) 资讯 (2909) Android (4709) iOS (1850) 代码人生 (3043) 阅读 (2841)

热门文章

122
操作系统
关于我们 隐私政策 免责声明 联系我们
Copyright © 2020-2025 蚁人论坛 (iYenn.com) All Rights Reserved.
Scroll to Top