️作者:@malloc不出对象
⛺专栏:Linux的学习之路
个人简介:一名双非本科院校大二在读的科班编程菜鸟,努力编程只为赶上各位大佬的步伐
相信大家在学习 C/C++ 的时候,肯定是见过类似下面的内存地址空间的图片,每种数据对应分布在不同的区域:
那么它真的是内存吗?其实它并不是真正的内存,那它究竟是什么呢?我们先看来一下下面的代码,再一起探究它究竟是什么。
通过上图我们可以发现子进程的g_val修改成了200,但是父进程的g_val始终未改变,这是非常好理解的。我们的fork调用之后创建了一个子进程,父子进程的代码和数据共享,而进程是具有独立性的,所以改变子进程的g_val的值并不会影响父进程的g_val,这是得益于fork函数采用了写实拷贝的方法实现的,这些都没有任何问题。最大的问题是父子进程的地址相同,对应的g_val值竟然不一样?同一块空间有两个不同的值??所以显然这块空间绝不是物理地址(内存),因为物理地址(内存)对应的内容肯定是唯一的!!不可能会出现同一个变量的地址读取出两个不同的值。实际上我们之前在C/C++语言层面用的地址都不是物理地址(内存),那么它到底是什么呢? 我们通常叫它为虚拟地址or线性地址!!
下面我们来通过一个小故事感性理解一下虚拟地址:美国有个大富翁叫小库,他有几十亿美金和很多个私生子,私生子不知道彼此的存在。小库对每个私生子都说过:“儿子啊,我有几十亿美金的存款。你好好打理生意,等我去世了,这些存款都是归你的。” 在这个故事中大富翁小库就对应着操作系统,几十亿美金对应着内存,私生子就是一个个的进程,大富翁小库给每个私生子画的大饼就对应着进程地址空间,那么这些大饼要不要管理起来呢?当然要,大富翁会将他画过的大饼都记下来,这样到时候也许可能会兑现某个私生子的承诺。
如何管理进程地址空间?先描述再组织,进程地址空间的本质是内核的一种数据结构struct mm_struct,我们对应的进程task_struct可以通过加入某个属性将struct mm_struct管理起来。
由此我们就明白了每一个进程都会创建一个进程地址空间,将我们平时写的语言级别的代码和数据放在进程地址空间中的对应区域,那么请问我们是如何划分这些相应的区域??维护相应的区域呢??
如何理解线性地址?
以32位计算机为例,我们有32根地址线,每根地址线对应的数据只有0 1信号,那么32根地址线就有2^32中排列组合,就有2^32个地址,我们的CPU在运算完某些数据之后,会进行寻址找到一段地址空间将其存放在内存中,内存地址中最小的单位为字节,那么2^32个地址占据2^32个字节空间,换算出来总的地址空间大小就是4GB!!因为我们的地址是按照字节号大小依次递增的,所以我们就认为地址空间是一个线性结构。
在上述我们讲解了我们平时在语言层面上见到的地址其实都是虚拟地址,但是我们最终的数据和代码真正只能在物理内存中对吧!!那虚拟地址空间与物理内存之间又是如何联系起来的呢?
我们的虚拟地址和物理内存是通过页表映射来建立关系的,它是一种K-V模式通过映射能找到对应变量的物理地址对数据进行读取和写入!!并且每个进程都会有各自的进程控制块task_struct、进程地址空间mm_struct和页表。
注:以上的页表其实并不是真正的页表结构,只是我们为了方便理解这样画罢了,实际上它是一个多级页表树状结构。
通过上述的讲解,此时一开始出现同一块空间值不一样的情况就很好理解了:
上述过程中父子进程的虚拟地址相同是非常正常的,因为子进程会继承父进程的大部分代码和数据,如果此时的数据需要进行修改,那么父进程或子进程会重新建立一组映射关系,保证它们使用不是同一块物理地址空间!!这也是进程独立性的一种具体表现,父子进程都具有各自的task_struct、进程地址空间struct mm_struct以及页表!!
之前讲到的fork函数为什么可以有两个返回值并且同一个地址空间会读取出不同的值,这里我们也就能够解释清楚了:
fork在返回时,父子进程都已经创建好了,这两个进程是独立的,是不是就会return两次?返回的本质是不是写入?谁先return谁就先进行写实拷贝,同一块地址是子进程继承父进程的虚拟地址,return返回写入后,它们就映射到不同的物理空间上了。
假设没有虚拟地址空间那么我们CPU进行寻址先内存中访问写入数据应该是下面这种状态:
那么请问如果我们的代码出现了错误,例如出现了野指针问题,CPU进行寻址的时候就可能会访问到其他进程的数据或修改其他进程的数据,这样导致了其他进程出现了安全性的问题,此时进程的独立性也就无法保证了!!
我们通过增加虚拟地址空间这一软件层,使我们的CPU进行寻址时必须先通过虚拟地址空间这一软件层,如果在虚拟地址空间这一层OS检测出了越界访问等问题就会进行拦截,此时就算进程崩溃了也不会影响到其他的进程,这样就保证了进程的安全性;另外,通过增加页表这一软件层如果我们在虚拟地址空间这块出现了问题就不能成功的进行映射,而且页表还有其他方法在一定程度上对虚拟地址做权限标识等其他安全机制,这样的双重保险机制就很大程度上保证了进程的独立性和安全性!!
结论1:防止地址随意访问,保护物理内存与其他进程。
下面我们来想一个问题:我们的malloc向操作系统申请内存空间,操作系统是立马给你还是在你需要的时候给你?
缺页错误
- 缺页错误(Page Fault)是指当程序要访问的内存页面不在物理内存中时,操作系统需要将该页面从磁盘或其他辅助存储设备中调入内存,以满足程序的访问需求。在这个过程中,操作系统会发现需要调入的页面不存在于物理内存中,就会产生一个缺页错误。操作系统会将该错误报告给程序,程序需要等待操作系统将页面调入内存后才能继续执行。
- 缺页错误是虚拟内存技术的核心概念之一。通过虚拟内存技术,操作系统可以将内存中不常用的页面置换到磁盘等辅助存储设备上,以释放物理内存空间。当程序再次访问该页面时,操作系统会将该页面重新调入内存。虚拟内存技术可以使得系统的内存利用率更高,但也会增加缺页错误的发生率。
Q:进程地址空间关心这些代码和数据放在物理内存的哪个位置吗?
并不关心,进程地址空间只需要关心代码和数据放在它对应的哪个区域,然后放入页表中,由页表进行映射到物理内存中,理论上来讲这些代码和数据可以映射放在任何位置,而物理内存只需要关心映射的位置是否是放置重叠了等问题,同样的也不关心虚拟地址空间会将这些代码和数据放在哪个位置。这些工作都是由页表来完成的,有了页表的映射关系就将它们两者紧密的联系在一起了,但是它们两者采用的确是不同的管理方式,是独立的管理方式。
结论2:将进程地址空间管理与内存管理进行解耦合。
再次理解虚拟地址空间
我们的程序在被编译之前没有加载到内存,请问我们的程序内部有没有地址呢?
答案是有的,在Linux中我们的目标文件以及可执行程序都是按照ELF格式将代码的数据保存在各个段中的。 源代码在被编译的时候,都是按照虚拟地址空间的方式对代码和数据早就已经编好了对应的编码 !!当程序被加载到物理内存中的时候,该程序对应的指令和数据就都具有了物理地址。CPU读取的都是指令,指令内部是有地址的(虚拟地址),CPU通过页表映射找到物理地址执行相应指令,此时的物理地址中的内容又是虚拟地址,又重复执行这个过程,此时整个进程就被滚动的执行起来了。
总结:
- 可执行程序内部有互相跳转的地址,即虚拟地址
- 程序被加载到内存中,有标识物理内存中代码和数据的地址
- CPU 见不到物理内存的地址,见到的只是虚拟地址
结论3:可以让进程以统一的视角看待自己的代码和数据。
本篇文章的内容就讲到这里了,如果对于本文有任何问题或者错处欢迎大家评论区相互交流orz~