引言:
北京时间:2023/2/22,离补考期末考试还有5天,不慌,刚午觉睡醒,闹钟2点20,拖到2点50,是近以来,唯一一次有一种睡不醒的感觉,但是现在却没有精神,因为听了一首歌(当然洗脸更重要),天后孙燕姿的《直来直往》,三天170遍,上头,感兴趣的小伙伴可以去尝试一下哦!上篇博客我们学习了系统中环境变量等知识,现在我们接着系统环境变量的知识,来学习一下进程地址空间的相关知识
虚拟地址空间
什么是进程地址空间呢?让我们带着这个疑问,一步一步的来探索吧!首先我们在以前的知识中,学习过如果创建子进程(fork
函数),并且明白进程其中的一个特性:独立性
所以明白这两点,看下图中的代码和运行结果,就可以很轻松的搞定
搞定了上述的代码和运行结构,此时我们就可以通过下述的代码和运行结果来引出进程地址空间的一个小知识点了,就是为什么父进程和子进程之间的地址是一样的呢?
看到上述代码,我们可以发现,利用进程的独立性,此时一个进程改变全局变量,并不会影响到另一个进程当中的全局变量,了解了这点,突然觉得进程真的是非常的神奇,看起来就是一个独立的个体一样,当然更神奇的是,这两个进程的地址是一样的,为了探索地址一样的问题,此时我们就找到了进程地址空间的切入点,从父子进程拥有相同的地址空间问题,引出地址空间的概念,接下来就让我们再次探索地址空间的知识吧!
复习有关内存的知识
当我们从上述例子之中得到了有关虚拟地址空间的概念之后,此时我们肯定是会有疑问的,比如:最大的疑问,为什么要有虚拟地址空间?电脑中不是存在内存(物理地址),为什么不使用内存(物理地址)呢?
此时在学习虚拟地址空间的概念之前,我们就先来复习一下以前学过的有关内存的相关知识,首先内存也叫存储器,是电脑中非常重要的硬件组成部分,因为有了存储器,才使电脑拥有的记忆功能,并且存储器可以分为内存和外存(磁盘),这里我们具体讲一讲什么是内存,内存也还可以进行细分,分为只读内存(ROM)和随机存取内存(RAM),注意:此时的ROM指的就是硬盘,也是属于内存中的一种哦!搞清了之间的关系,我们来看看存储器的组成,存储器主要是采用半导体器件和磁性材料构成,存储器的最小单元是存储元(由一个双稳态半导体电路或一个CMOS晶体管或磁性材料构成),然后由若干个存储元可以构成一个存储单元,最后存储器就是由许多的存储单元构成的,并且每一个存储单元可存放一个字节的空间,所以如果你的机器是一个32位的机器,此时就物理上就有32根地址线,有了32根地址线,当电脑进行通电的时候,就会产生32个正电/负电(1/0),此时就使我们的电脑有了32个由1/0构成的二进制序列,通过组合此时就有2^32 次方中可能,有了这么多的组合,此时就可以把这些组合和我们的存储元给结合在一起,通过各种二进制序列组合,给存储元编号,这种就使我们拥有的地址的概念,所以如果是一个32位的机器,此时他就有43亿左右(2的32次方)存储单元地址,此时每个存储单元存放一个字节,此时该存储器就拥有的4G左右的内存空间。(所以也就导致了,你如果想要使用内存,就必须把对应的信息给转换成二进制序列,这样才可以在内存中这么多的地址中寻找到对应的数据)如下图所示:
看到这幅图,我相信大家对内存的概念,应该会有更深层次的体会和认识吧!所以在我们电脑中的内存,也就是物理内存,本质上就是如上图的形式进行数据的存储。
为什么要有虚拟地址空间?
在了解为什么要有虚拟地址空间之前,我们先来谈谈操作系统的特性和基本功能,所有应用程序对硬件的操作都必须通过操作系统实现,并且为了防止硬件被失控的应用程序滥用和向应用程序提供简单一致的机制来控制复杂而又通常大不相同的低级硬件设备;操作系统此时需要通过抽象进程、虚拟内存和文件等概念来实现,如下图:
文件是对I/O设备的抽象表示,虚拟内存是对主存和磁盘I/O设备的抽象表示,进程则是对处理器、主存和I/O设备的抽象表示;
明白了上述知识,引出关键概念:进程是对处理器、主存和I/O设备的抽象
所以此时就有了,当我们运行一个程序的时候,操作系统会提供一个假象,就好像系统上只有这个程序在运行。程序看上去是在独占地使用处理器、主存和I/O设备,处理器看上去就好像是在不间断地一条一条地执行该程序中的指令,即该程序的代码和数据是系统内存中唯一的对象;所以进程是操作系统对一个正在运行的程序的一种抽象,在一个系统上可以同时运行多个进程,而每个进程都好像在独占地使用硬件。
所以此时我们就明白了,一个进程在运行时,是独占所有的系统资源的
有了上述的知识,此时就可以引出我们想要学习的重点知识,为什么要有虚拟地址空间,理由如下:
在虚拟内存出现之前,程序寻址用的都是物理地址,因此程序能寻址的范围是有限的,具体寻址的范围是2^32也就是4G;
并且如果没有虚拟内存,那么每次运行进程就需要分配4G的物理内存给该进程使用,因为进程是对处理器、主存和I/O设备的抽象,是独占使用内存的,运行一个进程就需要把我们所有的内存供给其使用;
并且由于物理内存是有限的,当有多个进程要执行的时候,每个进程都需要分配4G内存,独占的使用所有的硬件,所以当内存被分配完之后,没有得到分配资源的进程就只能等待,当另一个进程执行完后,再将等待的进程装入内存,这种方式执行进程效率是非常低下的;
并且由于指令都是直接访问物理内存,那么进程就可以修改其他进程的数据,甚至会修改内核地址空间的数据,所以这种直接访问物理内存的方式是非常的可怕的,并且因为内存是随机分配的,所以程序运行的地址也是不正确的。
所以根据以上的种种原因,我们就需要引入一个叫虚拟地址空间的概念来尝试解决这些问题。
什么是虚拟地址空间
首先根据上述的知识,我们知道了为什么要有虚拟地址空间的概念,所以此时我们就正式来谈谈什么是虚拟地址空间,概念:每个进程创建加载的时候,会被分配一个大小为4G的连续的虚拟地址空间,虚拟的意思就是,其实这个地址空间时不存在的,仅仅是每个进程“认为”自己拥有4G的内存,而实际上,它用了多少空间,操作系统就在磁盘上划出多少空间给它。
通过概念,我们可以知道,虚拟地址空间的本质在我们的电脑中是不存在的(不像物理内存那样,拥有存储单元,每个单元一个字节),它是通过映射的形式将对应的数据存储在内存之中,具体说起来较为复杂,这里先不做了解。让我们看图来加深对虚拟地址空间的认识,如下图:
每个进程看到的虚拟地址空间由大量准确定义的区构成,如上图所示,每个区都有专门的功能
此时我们就来一一简单介绍一下这些区域的功能
1.程序代码和数据区(代码段):例如此时我们在执行一个C程序,对所有的进程来说,代码是从同一固定地址开始,紧接着的是和 C全局变量相对应的数据位置存放,代码和数据区是直接按照可执行目标文件的内容初始化的
2.堆:代码和数据区后紧随着的是运行时堆。代码和数据区在进程一开始运行时就被指定了大小,与此不同,当调用像 malloc 和free 这样的C标准库函数时,堆可以在运行时动态地扩展和收缩。
3.共享库:大约在地址空间的中间部分是一块用来存放像C标准库和数学库这样的共享库的代码和数据的区域。共享库的概念非常强大,这里先简单介绍。
4.栈:位于用户虚拟地址空间顶部的是用户栈,编译器用它来实现函数调用。和堆一样,用户栈在程序执行期间可以动态地扩展和收缩。特别地,每次我们调用一个函数时,栈就会增长;从一个函数返回时,栈就会收缩。
5.内核虚拟内存:地址空间顶部的区域是为内核保留的。不允许应用程序读写这个区域的内容或者直接调用内核代码定义的函数。相反,它们必须调用内核来执行这些操作。
总:虚拟内存的运作需要硬件和操作系统软件之间精密复杂的交互,包括对处理器生成的每个地址的硬件翻译。基本思想是把一个进程虚拟内存的内容存储在磁盘上,然后用主存作为磁盘的高速缓存
如何管理进程地址空间
通过上述的知识,此时我们明白了什么是虚拟地址空间,所以当我们想要执行一个程序的时候,该程序被加载到内存中,内存生成一个对应的PCB(task_struct)结构体,来管理该程序对应的代码和数据,此时的PCB中就存放这该进程的全部信息,此时就可以明白,我们的操作系统管理任何东西,都是通过先描述,再组织的方式,进行管理的(总而言之就是生成一个有合适成员变量的结构体),所以此时操作系统管理进程地址空间,也是通过先描述,再组织的形式,这样此时就又有了一个新的结构体( mm_struct ),专门给进程使用的结构体,也就是操作系统专门用来管理进程的结构体,所以,我们上述所说的进程可以独占的使用系统资源,本质上就是因为其被操作系统通过先描述,再组织的方式形成了一个结构体,就是一个内核数据结构而已
pcb和mm_struct和进程地址空间具体关系请看下图(搞清关系就行):
从上图可以看出,我们的task_struct结构体中是有一个结构体指针,指向着mm_struct进程结构体的
从物理内存理解虚拟内存
明白了上述的知识,此时我们知道,无论内存怎样虚拟化,最后肯定是都要放在物理内存之中,所以想要深入了解虚拟内存,此时就需要从物理内存的方面去理解,首先文章开始,我们已经复习过了物理内存的知识,这里就不多做介绍了,我们只要明白32位的机器,物理内存是通过2^32个字节的地址空间构成就行,并且如图,我们可以看出,物理内存是连续的,线性的,每个字节的空间是独立的(一个地址代表一个字节),所以当我们此时开辟了一个整形类型(int),此时就需要在线性内存中占4个连续的地址(因为一个地址只代表一个字节),所以本质上,开辟一个整形,向线性内存申请了4个不同的地址(地址连续),理解这个,我相信大家都有疑问,因为平时我们在调式,看变量地址的时候,并不可以看到4个地址,只能看到一个地址,所以此时就需要把这两个概念给结合起来理解,最终得出,本质上是4个地址,但是由于这些地址是连续的,方便查找的,所以,操作系统只是把4个地址中的最下面(编号最小)的那个地址(线性)给给我们,剩下的3个地址,是默认存在,只是你看不到而已;并且此时系统想要通过我们看到的那一个地址,找到剩下3个地址的话,就需要识别出该变量的类型(区别double、float、long),通过各个类型规定的字节大小,依次从内存中的地址向后读取,例:int,我们就再向内存读取三个字节,读取三个字节后就停止,double,就再向后读取7个字节,这样,我们就可以很好的把物理内存的地址给管理使用起来。
总:地址空间是线性结构
当我们了解到了,地址空间是线性的,那么可以知道我们的虚拟地址空间应该也是线性的,所以此时就如上述的虚拟地址空间图一样,我们可以把虚拟地址空间给划分成一个一个的区域,实现每个区域执行不同的功能,又因为,我们的操作系统,会将进程给抽象成一个结构体(mm_struct
),如下图:
此时我们就知道,操作系统中的进程结构体就是通过一个一个的作用区域的开始(start)和结束(end)来控制进程虚拟地址空间,例如:当code_start = 100,code_end = 200,此时在该代码段中的100和200之间的区域表示的就是我们的虚拟地址,所以虚拟地址空间就是一个线性的结构,并且此时我们可以把这个结构通过结构体的方式定义成一个具有区域划分的新结构,并且可以通过改变某个区域的开始(start)和结束(end),来改变该区域的大小,例如:虚拟地址中的栈区和堆区的扩大缩小,就是通过这种形式来完成的。(注:只是简单理解,原理不会错,只是系统操作起来远复杂)
浅谈数据从虚拟地址空间拷贝到物理内存
这个问题是比较复杂的,感兴趣的同学请看这篇博客大佬博客
每个进程启动之后,操作系统都会自动生成一个该进程的页表,用于存储的是虚拟地址和物理地址的映射,然后通过和CPU上的 MMU 进行交互,把虚拟地址翻译成物理地址,进而存储在物理内存之中(原理就是这样,但是实际复杂n倍)。
如下图所示:
通过上图,我们可以发现,为什么修改子进程的值,不会改变父进程的值,原因就是:当修改子进程中的值时,内存会自己重新开辟一块空间给你,然后把你的页表映射关系给改变,此时页表映射了位置就是新的空间,此时就可以实现子进程的值改变,并且因为页表是独立的,所以改变的只是物理内存和页表的映射关系,不会改变原虚拟地址中的值,所以此时就很好的证明了,子进程和父进程同一地址,但不同值的问题,并且因为此时在物理内存中开辟了两块空间,所以也解释了父子进程有两个返回值的问题。
使用虚拟地址空间的好处
1.防止地址随意访问,保护物理内存与其它进程
2.将进程管理和内存管理进行解耦合(本质上是为了提高操作系统效率和内存使用效率)
3.可以让进程以统一的视角,看待自己的代码和数据(进而忽略内存中的地址位置),提高运行效率