进程地址空间如何映射至物理内存

举报
跳动的bit 发表于 2022/08/30 07:52:42 2022/08/30
【摘要】 💦 进程地址空间如何映射至物理内存 (页表的引出)假设存在三个进程 A B C,操作系统就会给每一个进程画一张大饼,叫做当前进程的虚拟地址空间,其中会通过指针将进程和虚拟地址空间关联起来。运行进程A,就要把进程A加载到物理内存中,其中操作系统会给每一个进程创建一张独立的页表结构,我们称之为用户级页表,当然后面还有内核级页表,而页表构建的就是从地址空间中出来的虚拟地址到物理地址中的映射,每...
💦 进程地址空间如何映射至物理内存 (页表的引出)

在这里插入图片描述

假设存在三个进程 A B C,操作系统就会给每一个进程画一张大饼,叫做当前进程的虚拟地址空间,其中会通过指针将进程和虚拟地址空间关联起来。运行进程A,就要把进程A加载到物理内存中,其中操作系统会给每一个进程创建一张独立的页表结构,我们称之为用户级页表,当然后面还有内核级页表,而页表构建的就是从地址空间中出来的虚拟地址到物理地址中的映射,每个进程都通过页表来维护进程地址空间和物理内存之间的关系,这是页表的核心工作,所以就进程就可以根据页表的映射访问物理内存。

能否把进程A中的代码和数据加载到物理内存中的任意位置 ❓

  在不考虑特殊情况下,是可以将进程对应的代码和数据加载到物理内存的任意位置的,因为最终我们能通过进程中虚拟地址与页表之间的映射,再通过页表与物理内存中代码和数据的映射进行访问。所以进程中的代码和数据是能够加载到物理内存中的任意位置的,其中本质是通过页表去完成的。

多个进程之间会互相干扰吗 ❓

不同的进程它们的虚拟地址可以一样吗 ???

  同样进程B也可以通过页表把代码和数据加载到物理内存的任意位置,这里就算不同的进程的虚拟地址完全一样也没问题,因为不同进程通过一样的虚拟地址查的是不同的页表,其中的工作细节是由页表去完成的。

如果物理地址重址呢 ???

  这是操作系统的代码,一般不可能重址。当然也存在这样的特殊情况,如果进程B进程C是父子关系,我们在创建子进程C的 PCB、地址空间、页表、建立各种映射关系,把代码区、数据区等区域映射时,只需要将子进程C映射到物理内存中父进程代码和数据处,但当子进程C修改数据时,操作系统就会重新申请内存,修改当前进程的指向关系,让子进程C指向新的空间,把旧数据拷贝至新数据,此时这就是写时拷贝。所以不同的页表,物理地址可以重址,只不过这种重址是刻意的。

💦 为什么要存在进程地址空间

在这里插入图片描述
 &emsp其实早期操作系统是没有所谓的虚拟地址空间的。如果进程直接访问物理内存,那么我们看到的地址就是物理地址,当我们认识过在 C语言中有一个概念叫做指针,那么就能理解有可能会出现:如果进程A出现了越界,那么就有可能直接访问到了另一个进程的代码和数据,所以进程的独立性便无法保证。因为物理内存暴露,其中就有可能有恶意程序直接通过物理地址,进行内存数据的篡改。比如说某进程里有帐号和密码的数据,那就有可能会被更改帐号密码,如果操作系统不让改,那也可以进行读取,如果操作系统不想让你读取,操作系统就要实现一些较为困难的权限管理,成本较高。后来大佬对进程和物理内存之间就引出了虚拟地址空间,其中每一个进程都有自己的地址空间、页表。虚拟地址最终通过页表转换为物理地址,那么页表需要根据实际情况转或不转。好比小时候过年,收到亲戚的压岁钱后,妈妈怕你乱花钱,所以就帮你存起来,当你要买资料时,你妈就帮你支出,但你要买游戏机时,你妈就可以拒绝你。换言之,虚拟地址到物理地址的转换,是由页表完成的,同时也需要进行合法性检测。

在这里插入图片描述

  至此我们认识到地址空间的引入可以保护物理内存。其它情况,越界时不一定报错,比如在栈区越界后还是在栈区,在一个合法区域内,操作系统是有其它机制去检测的,那么既定的 C/C++ 事实是我们在越界时是不一定报错的,因为编译器是以抽查的形式来检测,这里可以去了解一下金丝雀技术。对于有区域划分的地址空间,你访问数据区,但是因为越界访问了代码区,操作系统就可以根据你曾经区域划分时的[start, end]来确认是否越界。对于页表,它将每个区域映射至物理内存中,页表要进行某种映射级别的权限管理,比如在映射数据区时,物理内存的任意位置都是可以被修改的,否则曾经的数据是怎么被加载的;但在映射代码区后,你有任何的写入操作时,操作系统发现对应页表你只有r权限,一旦写了,操作系统就终止你的进程。我们都知道如上这种字符串是在代码区存储,代码区是只读的,所以你要修改它,在 Linux 下报的是段错误,在 VS 下报的是表达式必需是可修改的左值。从 Linux 报的错误来看,这段代码是能编译通过的,但是运行后,操作系统发现页表在映射时,你要映射的区域是不可写的,那么经过这样的进程地址空间 + 页表,操作系统就可以直接终止进程,换言之,进程地址空间是为了更好的进行权限管理。

只读的代码区,那么第一次是怎么形成的 ???

  形成代码区时不就是把数据往代码区里写吗,其实代码区在操作系统的角度,它一定是物理内存的任何位置都可以改的,只不过*str = 'H'是在你进行写入后修改字符串起始的第一个字符,所以经过对应的页表映射时,发现你对这个区域的权限是只读的,而你竟然想写入,所以操作系统就不会映射,直接终止进程。
在这里插入图片描述

我们都知道操作系统具有 4 种核心功能:进程管理、内存管理、驱动管理、文件管理。而上图很明显是与进程管理和内存管理有关,比如说一个进程要执行,首先要申请内存资源,并加载到内存,然后创建 PCB 等进程管理工作;而进程死亡后,就需要内存管理模块来进行尽快回收,内存管理必须得做到知道某个进程的状态。所以内存管理模块和进程管理模块是强耦合的。如果有了虚拟地址空间的概念,那么进程管理关注左半部分,而内存管理关注右半部分。在 C++ 中有一个技术叫做智能指针,比如说给物理内存的一块区域设置一个计数器 count,其中当页表映射一个进程后,count++,当进程释放后,映射关系消失,count--。所以内存管理只需要检测当前物理内存中的 count 是否为 0。

一个 16G 的游戏能否在 4G 的物理内存上运行 ❓

  能,比如你的内存是 32G,即便你加载了 16G,对计算机而言,它是从头开始访问的,也就是说 16G,你已经有 15G 已经加载到内存了,但你尚没有正常使用,还需要等待后面的数据加载进来,所以这是一种很低效的方案。所以操作系统要执行这个进程,但内存管理模块认为给你搞这么多你又不使用,所以就先加载 200M 给你,当你从上至下访问到最后时,如果你还需要,就再给你覆盖式的加载 200M,此时进程是不知道内存管理模块给他做的操作,内存管理就可以通过不断延迟加载的技术方案,来保证进程照样可以正常运行,这就是进程管理模块和内存管理模块解耦。所以对于用户来说,唯一感受到的是我的电脑变慢了,当然这也是应该的。


在这里插入图片描述

在磁盘上形成的 .exe 文件在编译时其实并不是无脑的把代码和数据一分为二就完了,而是在磁盘中按照文件的方式组织成一个一个区域,这样做的原因是便于生成可执行程序,如果划分好了区域,那么就会减少程序链接过程的成本。因为磁盘上的可执行程序本身就是按模块划分的,所以进程地址空间才有了区域划分的概念,但要注意物理内存的情况有可能大部分的空间已经被使用了,那么进程的代码和数据就零散的分布于物理内存的不同位置。物理内存也有区域,只不过它的内存分配是按页为单位,一页是 4kb,换言之,磁盘数据加载到物理内存时,是按 4kb 为单位,其中每页是页框、4kb 是页帧

顺序语句 ❓

  本质是将虚拟地址线性连续后,顺序语句就能实现了。所以顺序语句就是当前语句的起始地址 + 当前代码的长度。

show 函数调用完后,字符串还在吗 ❓

在这里插入图片描述

  当 show 函数调用完后,函数栈帧销毁,所以局部变量 str 一定是不在了;但是对于字符串,它存储于常量区,只要进程还在,那么字符串就还在。show 栈帧结束,理论上是找不到字符串了,所以我们就能理解所有的地址信息都必须要用变量保存,当你在物理内存中 malloc 好一块内存,页表构建映射关系,把地址映射到堆区,最后这个区域的起始地址就返回给用户,如果用户不使用变量保存,那么就会存在内存泄漏

所以虚拟地址空间存在的意义有:

  1. 更好的进行权限管理和保护物理内存不受到任何进程内地址的直接访问,方便进行合法性校验。
  2. 进程管理和物理管理进行解耦。
  3. 让每个进程以同样的方式,来看待代码和数据。
💦 解释子进程修改全局变量g_val后的地址相同于父进程&g_val

在这里插入图片描述

  父进程在 fork 时,操作系统一定是多了一个进程,而子进程需要创建自己的 mm_struct、页表,其中子进程中的大多数属性是以父进程为模板。代码里是通过 if 和 else 来分流的 —— 父进程执行 if,子进程执行 else,实际上,不管是父进程还是子进程都能看到所有的代码,只不过不会全部执行。因为子进程中的大多数属性是以父进程为模板,所以父子进程 &g_val 的虚拟地址是相同的,当子进程尝试对 g_val 写入,而操作系统发现对于 g_val,父子进程只有 r 权限(因为它们指向同一块内存),你居然想 w,操作系统又发现,你俩是父子关系,所以没有杀掉子进程, 而给你重新开辟一块空间,把旧空间的内容拷贝,子进程的页表就不再映射至父进程的 g_val,而是子进程的 g_val 自己私有一份,所以子进程再修改时,就可以把 g_val = 100 了,其过程的本质是写时拷贝。所以我们就能解释为啥 g_val 的值改变后,而 &g_val 的值却是相同的。

进程和程序有什么区别 ❓

在这里插入图片描述

  从现在开始我们再提到进程,就应该立马能联想到 task_struct、mm_struuct、页表、代码和数据。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。