目录
前言
接着上篇文章 进程地址空间(一) ,这篇文章进一步探讨 什么是进程地址空间,以及为什么要有进程地址空间
1. 进程地址空间究竟是什么
在上一篇文章讲地址空间的时候,我们说过每个进程都要有自己的进程地址空间。
所以截止现在,我们应该要理解到这种程序! ----- 进程 = 内核数据结构( task struct && mm_struct) + 程序的代码和数据。
但是上一篇文章之后,我还是不太理解进程地址空间,到底是个啥玩意。所以我们再来讲个故事。
在漂亮国有一位大富翁,身价10个亿美金,但是私生活有点混乱,他拥有四个私生子,并且各自之间不知道其它私生子的存在。有一天,大富翁给他的四个私生子画了一个大饼,对每个私生子都说死后自己那10亿美金财产是他们的。作为儿子的他们,自然不会有过多的怀疑。1号私生子是开饭店的,于是他找他老爹要了100w,他老爹觉得还算合理,所以就马上给他打钱了。收到钱后,1号私生子就更加坚信他老爹真的有10亿财产。2号、3号私生子也各自向他老爹要了点钱,想干点事业出来,他老爹也都给了。但是4号私生子向他老爹要了10个亿,他老爹随便找了个理由拒绝了,“ 你老子我都还没 s,你就开始惦记我的财产,滚蛋!”。虽然 4号没有要到钱,但是作为儿子的他,也没有多想,还是选择相信了他爹有10亿财产。
而 大富翁给他四个私生子画的大饼,就是操作系统给每一个进程划分的进程地址空间,其中的 大富翁 就是操作系统,每一个进程就是操作系统的私生子(彼此都不知道其它进程的存在,并且坚信自己是唯一拥有操作系统给的 4GB 内存)。而在现实生活中,这种关系就好比 我 和 银行 之间的关系。你说今天我把100w 存入银行,银行真的就把我的钱存起来了吗?答案是基本不可能,当你把钱存在银行里,银行可能直接拿着你的钱去放贷吃利息了。但是银行为了让你觉得,你真的还有 100w,银行给了你一直银行卡,并且上面的金额是 100w。
所以每个进程在启动时,操作系统都会为其构建一个的进程地址空间,而操作系统就用进程地址空间这样的数据结构来表征每一个进程所能看到的内存空间范围,而这个进程地址空间就是擦着系统给进程画的一个大饼!当进程向操作系统合理申请内存时,操作系统一般都会给,而如果一个进程向操作系统申请其全部的内存,操作系统是给不出的,也不会给!
2. 为什么要有进程地址空间
2.1 让进程以统一的视角看待内存结构
有了进程地址空间之后,这个进程的代码和数据 与 另一个进程的代码和数据,在整个内存布局上是可以做到完全一致的!换言之,不同进程的代码和数据,有了进程地址空间,是可以放在同一个地方的(虚拟地址的同地方,他们的代码和数据都可以放在 0x112233 ~ 0x334455)! 这样,所有的进程就都可以以统一的视角看待内存结构!
如果把进程地址空间去除掉,那么在进程的 pcb 中就需要保存自己的代码和数据在物理内存中所处的位置。但不妨以后这个进程还可能会被挂起,代码和数据被换出到磁盘,之后重新换入,其在物理内存中的地址可能会发生改变,而代码和数据的地址变了,就需要修改 pcb 中记录的地址,这样就很麻烦。而有了进程地址空间,只需要以进程地址空间这样统一的视角去看待就行了,进程的 pcb 就不再需要关心其代码和数据在物理内存中的什么位置,将来地址变了需要修改pcb等问题。
2. 2 保护物理内存
在讲进程概念的时候,我们就说过,我们并不是直接访问的操作系统,而是通过各种系统调用来进行访问的,原因就是操作系统不信任任何人。那么在访问物理内存这件事上,也是同理。如果没有了进程地址空间,那么进程就直接访问物理内存了,而进程的代码都是我们写的啊,只要是人,就都可能犯错误,并且还可能存在有人向只读区域进行数据写入呢!
但是有了进程地址空间,那么一个进程要访问物理内存,都必须先在虚拟地址空间找到对应的地址,再通过页表进行关系映射到物理内存地址。假如今天一个进程就是在访问一片未曾申请的虚拟空间地址,在页面中甚至都找不到映射关系,或者映射到的物理内存的地址标记是只读的,有了进程地址空间,就可以在虚拟到物理内存的转换过程中做系统级别的检查!从而有效拦截进程的非法操作!!
所以进程地址空间是如何保护物理内存的?? ----- 增加进程虚拟地址空间可以让我们访问内存时,增加一个转换的过程。在这个转化的过程中,可以对我们的寻址请求进行审查,一旦异常访问,直接拦截,该请求不会到达物理内存,保护物理内存。避免了因为非法访问数据导致数据的篡改等问题,造成其它进程的异常!
2.3 进程管理 与 内存管理模块 的解耦合
2.3.1 页表地址
在介绍第三个作用之前,我们需要做一个页表相关的铺垫知识。
我们现在应该要知道的是,每个进程都有自己的进程地址空间这样的结构体数据,而为了实现虚拟地址到物理地址直接的映射,每个进程就要维护一个页表结构。对于一个正在运行的进程,在 cpu 内部会存在一个 cr3 寄存器,这个寄存器会保存当前进程的页表的起始地址(物理地址)。
那么现在有一个问题,当进程切换之后,有没有可能就找不到页表地址了。 ----- 可不敢担心啊!在讲进程切换的时候,我们就说过了,当一个进程要被换下时,它一定会带走执行过程中留在cpu的各种临时数据,而 cr3 寄存器中存储的该进程页表的地址,本质也是一个进程的硬件上下文!所以当进程切换的时候,进程自己是会把 cr3 寄存器中的地址信息带走的,后面恢复上下文数据的时候,再重新恢复到 cr3 中!
2.3.2 页表的权限管理
但是大家有没有想过一个问题,在虚拟地址中是有很大划分区域的,有只读,可读可写。但是操作系统是如何知道哪个地址是只读的。
其实在页表结构中,除了记录虚拟地址与其映射的物理地址,还有该虚拟地址的权限(只读?可读可写?)。所以假如今天我向代码常量区进行写入操作,cpu 通过 cr3 寄存器找到页表,查页表映射关系,最终发现这条地址在虚拟内存地址中的权限是只读的,这个行为是非法的!页表发现权限冲突,直接进行拦截,并且报告给操作系统,操作系统就直接将这个进程干掉!所以,页表的本质其实也是一种权限管理。
我们常常说,代码是只读的,字符常量区是只读的!但是我今天就很纳闷,你说代码是只读的,那我这个进程的代码是如何被加载到内存的?加载的本质不就是写入操作吗?----- 所以我们常说的代码和常量区是只读的,说的就代码和字符常量区所匹配的虚拟地址和物理地址的映射,它们的映射关系中的标志位都是只读!所以在进行写入操作时,操作系统才会拦截,最后进程挂掉。
2.3.3 关于进程挂起 && 惰性加载
当操作系统的内存资源紧张时,是可能会存在进程挂起的,我们也知道,进程挂起就意味着进程的代码和数据不在内存中了。问题是我们怎么知道这个进程被挂起了,linux 关于进程状态中可没有挂起这个状态。
回答这个问题之前,我们需要建立一个共识:现代操作系统,几乎不做任何浪费空间和时间的事情!可以稍微证明一下的就是,现在市面上各种主流一点的网游,动不动就30 40GB大小,一般而言,这个游戏中的代码和数据不可能一次性被加载到内存中,但是我们却可以正常运行游戏(满足游戏配置前提下),因为操作系统对大文件可以实现分批加载。
假设当前程序加载了 500MB 的代码和数据到内存中,由于进程的时间片,cpu的配置等原因,所以就注定了短期内,这些已经被加载到内存的代码跑不完,这就代表着有其它大部分的代码和数据放在内存中空闲着,那其实就是变相的浪费空间和浪费时间的操作,所以操作系统对可执行程序的加载采用的是 惰性加载。
而对于这种情况,原本 500MB 的代码和数据,最终只加载了 100MB,剩下的那部分,可以理解为,在页表中,虚拟地址处预先填充,而物理地址先空着,另外再给一个标志位,用于标记进程对应的代码和数据是否已经加载到内存中。当我们访问某一处虚拟地址时,操作系统就会先查页表,在地址转换之前,看一下这部分代码和数据是否已经加载到内存,如果加载了,那么就正常转换地址去访问物理内存,如果没有加载,操作系统就会触发 缺页中断(关于缺页中断具体的细节这篇文章不谈,目前可以理解为就是在物理内存中找一块空间,把剩下的代码和数据加载进来,再把物理地址填充到页表,虚拟地址重新映射,然后访问内存)。
需要知道的是,我们曾经说过子进程对父进程的数据采用的是 写时拷贝 的策略,这也是一种 缺页中断 !在访问修改父进程的数据时,查页表,虚拟地址虽然不是只读区域,但是页表给这个地址的权限是只读,那么就在内存中重新开辟一块空间,然后修改子进程页表中映射的物理地址,最后再进行访问修改!
所以当一个进程被创建时,是先加载内核数据结构还是先加载可执行程序呢? ----- 因为有了惰性加载,所以我们并不着急加载其代码和数据,一定是先创建各种内核数据结构(pcb,进程地址空间,页表等),然后再 “慢慢” 加载可执行程序。
但是,我们现在依旧有很多不解,比如上面说的 缺页中断(物理内存这么大,申请哪一块空间呢?加载的可执行程序的哪一部分呢?这一部分又要加载多少呢?物理地址怎么填充到页表的呢?什么时候填呢???),整个 缺页中断 的过程是操作系统的哪一部分来管理的呢?? ---- 操作系统的内存管理模块! 而关于内存管理这部分的工作,进程全然不知,它也不关心!
所以正是有了页表的存在,在操作系统中,进程的控制块,地址空间,包括页表的部分数据,都是属于进程管理模块干的事情,而物理内存如何申请,如何释放,在哪申请,可执行程序加载多少等等这些问题,都属于内存管理模块的工作。换言之,操作系统在软件层面上,实现了 进程管理 与 内存管理 的解耦!
所以,进程地址空间的第三个作用就是!因为有地址空间和页表的存在,将进程管理模块,和内存管理模块进行解耦合!
到这里,我们要进一步理解什么是进程 ?! ---- 进程 = 内核数据结构( task struct && mm_struct && 页表) + 代码和数据
换言之,时间片到了之后,进程的 pcb 被切换,其内部指向的进程地址空间也自然被切换,而页表地址存储在 cpu 中的 cr3 寄存器中,属于进程的硬件上下文,同样被切换。换言之,进程一旦切换,pcb、进程地址空间、页表全部都切换!
4. 进程独立性的体现
理解完上面的所有内容,我们再来谈论进程的独立性。怎么做到的呢??
- 每个进程都有自己独立的 pcb,进程地址空间,页表结构!
- 不同的进程,页表中的虚拟地址可以完全一样,物理地址完全不一样, 每个进程的代码和数据就互相解耦了。
- 即便是父子进程,代码共享,虚拟地址也一模一样,但是在数据层面上,最终映射的物理地址不一样,一样实现了数据上的解耦!
这一来,对于进程而言,可执行程序的代码和数据,加载到物理内存中的哪个地方,什么时候加载,就不重要了!因为有了页表映射,物理地址随便映射(物理内存没有只读的说法),进程也不需要去关心在内存中乱序的数据,但虚拟地址可以是按连续、线性的空间呈现给进程,这就是以统一的视角看待内存结构!
以上全部,就是进程地址空间的整体框架!后续文章还会更进一步谈论进程地址空间的细节!
如果感觉该篇文章给你带来了收获,可以 点赞👍 + 收藏⭐️ + 关注➕ 支持一下!
感谢各位观看!