在回答你们的这个疑惑之前,让我们先来看下linux系统如何支持虚存,假如在程序中直接使用化学显存地址会发生哪些情况?
假定现今没有虚拟显存地址,我们在程序中对显存的操作全都都是使用数学显存地址,在这些情况下,程序员就须要精确的晓得每一个变量在显存中的具体位置,我们须要自动对化学显存进行布局,明晰什么数据储存在显存的什么位置,除此之外我们还须要考虑为每位进程到底要分配多少显存?显存紧张的时侯该如何办?怎样防止进程与进程之间的地址冲突?等等一系列复杂且繁杂的细节。
假如我们在单进程系统中例如嵌入式设备上开发应用程序,系统中只有一个进程,这单个进程独享所有的数学资源包括显存资源。在这些情况下,上述提及的那些直接使用化学显存的问题可能还好处理一些,并且一直具有很高的开发门槛。
但是在现代操作系统中常常支持多个进程,须要处理多进程之间的协同问题,在多进程系统中直接使用化学显存地址操作显存所带来的上述问题就显得十分复杂了。
这儿笔者为你们举一个简单的事例来说明在多进程系统中直接使用化学显存地址的复杂性。
例如我们现今有这样一个简单的Java程序。
publicstaticvoidmain(String[]args)throwsException{stringi=args[0];..........}
在程序代码相同的情况下,我们用这份代码同时启动三个JVM进程,我们暂时将进程依次命名为a,b,c。
这三个进程用到的代码是一样的,都是我们提早写好的,可以被多次运行。因为我们是直接操作数学显存地址,假定变量i保存在0x354这个数学地址上。这三个进程运行上去以后,同时操作这个0x354化学地址,这样这个变量i的值不就混乱了吗?三个进程都会出现变量的地址冲突。
文章插图
所以在直接操作数学显存的情况下,我们须要晓得每一个变量的位置都被安排在了那里,但是还要注意和多个进程同时运行的时侯,不能共用同一个地址,否则都会导致地址冲突。
现实中一个程序会有好多的变量和函数,这样一来我们给它们都须要估算一个合理的位置,还不能与其他进程冲突,这就很复杂了。
这么我们该怎么解决这个问题呢?程序的局部性原理再一次救了我们~~
程序局部性原理表现为:时间局部性和空间局部性。时间局部性是指假如程序中的某条指令一旦执行,则不久以后该指令可能再度被执行;若果某块数据被访问,则不久以后该数据可能再度被访问。空间局部性是指一旦程序访问了某个储存单元,则不久以后,其附近的储存单元也将被访问。
从程序局部性原理的描述中我们可以得出这样一个推论:进程在运行以后,对于显存的访问不会一下子就要访问全部的显存,相反进程对于显存的访问会表现出显著的倾向性,愈发倾向于访问近来访问过的数据以及热点数据附近的数据。
按照这个推论我们就清楚了,无论一个进程实际可以占用的显存资源有多大,按照程序局部性原理,在某一段时间内,进程真正须要的数学显存虽然是极少的一部份,我们只须要为每位进程分配极少的化学显存就可以保证进程的正常执行运转。
写在本文开始之前....从本文开始我们就即将开启了Linux内核显存管理子系统源码解析系列,笔者还是会秉持之前系列文章的风格,采用一步一图的形式先是详尽介绍相关原理,在保证你们清晰理解原理的基础上,我们再来一步一步的解析相关内核源码的实现。有了源码的辅证,这样你们看得也安心,理解上去也放心,最至少可以证明笔者没有胡诌乱造骗你们,哈哈~~
显存管理子系统堪称是Linux内核诸多子系统中最为复杂最为庞大的一个,其中包含了诸多纷扰的概念和原理,通过显存管理这条主线我们把可以把操作系统的诸多核心系统给拎下来,例如:进程管理子系统,网路子系统,文件子系统等。
因为显存管理子系统过分复杂庞大,其中涉及到的诸多纷扰的概念又是一环套一环,层层递进。怎样把这种纷扰的概念具有层次感地,而且清晰地,给你们梳理呈现下来真是一件比较有难度的事情,因而关于这个问题,笔者在动笔写这个显存管理源码解析系列之前也是思索了好久。
万事开头难,这么究竟哪些内容适宜作为这个系列的开篇呢?笔者还是认为从你们日常开发工作中接触最多最为熟悉的部份开始比较好,例如:在我们日常开发中创建的类,调用的函数,在函数中定义的局部变量以及new下来的数据容器(Map,List,Set.....等)都须要储存在数学显存中的某个角落。
而我们在程序中编撰业务逻辑代码的时侯,常常须要引用那些创建下来的数据结构,并通过这种引用对相关数据结构进行业务处理。
当程序运行上去以后就弄成了进程,而这种业务数据结构的引用在进程的视角里全都都是虚拟显存地址,由于进程无论是在用户态还是在内核态就能看见的都是虚拟显存空间,化学显存空间被操作系统所屏蔽进程是看不到的。
进程通过虚拟显存地址访问那些数据结构的时侯,虚拟显存地址会在显存管理子系统中被转换成化学显存地址linux系统如何支持虚存,通过数学显存地址就可以访问到真正储存这种数据结构的化学显存了。随即就可以对这块化学显存进行各类业务操作,进而完成业务逻辑。
本文笔者就来为你们详尽一一解答上述几个问题,让我们马上开始吧~~~~
文章插图
1.究竟哪些是虚拟显存地址首先人们提出地址这个概念的目的就是拿来便捷定位现实世界中某一个具体事物的真实地理位置,它是一种用于定位的概念模型。
举一个生活中的事例linux服务器配置与管理,例如你们在日常生活中给亲朋好友寄送一些本地特产时,就会填写寄件人地址以及收件人地址。以及在日常网上购物时,就会在相应电商APP中填写自己的收获地址。
文章插图
此后快件小哥都会按照我们填写的收货地址找到我们的真实住所,将我们网购的商品送达到我们的手里。
收货地址是拿来定位我们在现实世界中真实住所地理位置的,而现实世界中我们所在的城市,街道,新村,房子都是一砖一瓦,一草一木真实存在的。但收货地址这个概念模型在现实世界中并不真实存在,它只是人们提出的一个虚拟概念,通过收货地址这个虚拟概念将它和现实世界真实存在的城市,新村,街道的地理位置一一映射上去,这样我们就可以通过这个虚拟概念来找到现实世界中的具体地理位置。
综上所述,收货地址是一个虚拟地址,它是人为定义的,而我们的城市,新村,街道是真实存在的,她们的地理位置就是化学地址。
文章插图
例如现今的福建省厦门市在过去叫罗湖县,河南省的安阳过去叫常山,四川省的成都过去叫乐山。不管是常山也好,广州也好,又或是西安也好,乐山也罢,这种都是人为定义的名子而已,而且地方还是哪个地方,它所在的地理位置是不变的。也就说虚拟地址可以人为的变来变去,并且数学地址永远是不变的。
如今让我们把视角在切换到计算机的世界,在计算机的世界里显存地址拿来定义数据在显存中的储存位置的,显存地址也分为虚拟地址和化学地址。而虚拟地址也是人为设计的一个概念,类比我们现实世界中的收货地址,而化学地址则是数据在数学显存中的真实储存位置,类比现实世界中的城市,街道,新村的真实地理位置。
说了如此多,这么究竟虚拟显存地址长哪些样子呢?
我们还是以日常生活中的收货地址为例作出类比,我们都很熟悉收货地址的格式:xx省xx市xx区xx街道xx新村xx室,它是根据地区层次递进的。同样,在计算机世界中的虚拟显存地址也有这样的递进关系。
这儿我们以IntelCorei7处理器为例,64位虚拟地址的格式为:全局页目录项(9位)+下层页目录项(9位)+中间页目录项(9位)+页内偏斜(12位)。共48位组成的虚拟显存地址。
文章插图
虚拟显存地址中的全局页目录项就类比我们日常生活中收获地址里的省,下层页目录项就类比市,中间层页目录项类比区县,页表项类比街道新村,页内偏斜类比我们所在的楼座和几层几号。
这儿你们只须要大体明白虚拟显存地址究竟长哪些样子,它的格式是哪些,才能和日常生活中的收货地址对比理解上去就可以了,至于页目录项,页表项以及页内偏斜这种计算机世界中的概念,你们暂时先不用管,后续文章中笔者会渐渐给你们解释清楚。
32位虚拟地址的格式为:页目录项(10位)+页表项(10位)+页内偏斜(12位)。共32位组成的虚拟显存地址。
文章插图
进程虚拟显存空间中的每一个字节都有与其对应的虚拟显存地址,一个虚拟显存地址表示进程虚拟显存空间中的一个特定的字节。
2.为何要使用虚拟地址访问显存经过第一小节的介绍,我们如今明白了计算机世界中的虚拟显存地址的含意及其诠释方式。这么你们可能会问了,既然化学显存地址可以直接定位到数据在显存中的储存位置,那为何我们不直接使用化学显存地址去访问显存而是选择用虚拟显存地址去访问显存呢?
而虚拟显存的引入正是要解决上述的问题,虚拟显存引入以后,进程的视角都会显得十分宽阔,每位进程都拥有自己独立的虚拟地址空间,进程与进程之间的虚拟显存地址空间是互相隔离,互不干扰的。每位进程都觉得自己独占所有显存空间,自己想干哪些就干哪些。
文章插图
系统上还运行了什么进程和我没有任何关系。这样一来我们就可以将多进程之间协同的相关复杂细节统统交给内核中的显存管理模块来处理,极大地解放了程序员的心智负担。这一切都是由于虚拟显存才能提供显存地址空间的隔离,极大地扩充了可用空间。
文章插图
这样进程就以为自己独占了整个显存空间资源,给进程形成了所有显存资源都属于它自己的幻觉,这也许是CPU和操作系统使用的一个伎俩罢了,任何一个虚拟显存里所储存的数据,本质上还是保存在真实的数学显存里的。只不过内核帮我们做了虚拟显存到化学显存的这一层映射,将不同进程的虚拟地址和不同显存的化学地址映射上去。
当CPU访问进程的虚拟地址时,经过地址翻译硬件将虚拟地址转换成不同的化学地址,这样不同的进程运行的时侯,尽管操作的是同一虚拟地址,但毕竟背后写入的是不同的化学地址,这样就不会冲突了。
3.进程虚拟显存空间上小节中,我们介绍了为了避免多进程运行时导致的显存地址冲突,内核引入了虚拟显存地址,为每位进程提供了一个独立的虚拟显存空间,致使进程以为自己独占全部显存资源。
这么这个进程独占的虚拟显存空间究竟是哪些样子呢?在本小节中,笔者就为你们揭露这层神秘的面纱~~~
在本小节内容开始之前,我们先想像一下,假如我们是内核的设计人员,我们该从什么方面来规划进程的虚拟显存空间呢?
本小节我们只讨论进程用户态虚拟显存空间的布局,我们先把内核态的虚拟显存空间当作一个黑盒来看待,在前面的小节中笔者再来详尽介绍内核态相关内容。
首先我们会想到的是一个进程运行上去是为了执行我们交待给进程的工作,执行这种工作的步骤我们通过程序代码事先编撰好,之后编译成二补码文件储存在c盘中,CPU会执行二补码文件中的机器码来驱动进程的运行。所以在进程运行之前,这种储存在二补码文件中的机器码须要被加载进显存中,而用于储存这种机器码的虚拟显存空间称作代码段。
文章插图
在程序运行上去以后,总要操作变量吧,在程序代码中我们一般会定义大量的全局变量和静态变量,这种全局变量在程序编译然后也会储存在二补码文件中,在程序运行之前,这种全局变量也须要被加载进显存中供程序访问。所以在虚拟显存空间中也须要一段区域来储存这种全局变量。
文章插图
里面介绍的那些全局变量和静态变量都是在编译期间就确定的,并且我们程序在运行期间常常须要动态的申请显存,所以在虚拟显存空间中也须要一块区域来储存这种动态申请的显存,这块区域就称作堆。注意这儿的堆指的是OS堆并不是JVM中的堆。
文章插图
除此之外,我们的程序在运行过程中还须要依赖动态链接库,这种动态链接库以.so文件的方式储存在c盘中,例如C程序中的glibc,里面对系统调用进行了封装。glibc库里提供的用于动态申请堆显存的malloc函数就是对系统调用sbrk和mmap的封装。这种动态链接库也有自己的对应的代码段,数据段,BSS段,也须要一起被加载进显存中。
还有用于显存文件映射的系统调用mmap,会将文件与显存进行映射,这么映射的这块显存(虚拟显存)也须要在虚拟地址空间中有一块区域储存。
这种动态链接库中的代码段,数据段,BSS段,以及通过mmap系统调用映射的共享显存区,在虚拟显存空间的储存区域称作文件映射与匿名映射区。
文章插图
最后我们在程序运行的时侯总该要调用各类函数吧,这么调用函数过程中使用到的局部变量和函数参数也须要一块显存区域来保存。这一块区域在虚拟显存空间中称作栈。
文章插图
如今进程的虚拟显存空间所包含的主要区域,笔者就为你们介绍完了,我们看见内核按照进程运行的过程中所须要不同种类的数据而为其开辟了对应的地址空间。分别为:
以上就是我们通过一个程序在运行过程中所须要的数据所规划出的虚拟显存空间的分布,这种只是一个大约的规划,这么在真实的Linux系统中,进程的虚拟显存空间的具体规划又是怎样的呢?我们接着往下看~~
4.Linux进程虚拟显存空间在上小节中我们介绍了进程虚拟显存空间中各个显存区域的一个大约分布,在此基础之上,本小节笔者就带你们分别从32位和64位机器上看下在Linux系统中进程虚拟显存空间的真实分布情况。
4.132位机器上进程虚拟显存空间分布在32位机器上kali linux,表针的轮询范围为2^32,所能抒发的虚拟显存空间为4GB。所以在32位机器上进程的虚拟显存地址范围为:0x00000000-0xFFFFFFFF。
其中用户态虚拟显存空间为3GB,虚拟显存地址范围为:0x00000000-0xCxC000000。
内核态虚拟显存空间为1GB,虚拟显存地址范围为:0xCxC000000-0xFFFFFFFF。
文章插图
然而用户态虚拟显存空间中的代码段并不是从0x00000000地址开始的,而是从0x08048000地址开始。
0x00000000到0x08048000这段虚拟显存地址是一段不可访问的保留区,由于在大多数操作系统中,数值比较小的地址一般被觉得不是一个合法的地址,这块小地址是不容许访问的。例如在C语言中我们一般会将一些无效的表针设置为NULL,指向这块不容许访问的地址。
保留区的左边就是代码段和数据段,它们是从程序的二补码文件中直接加载进显存中的,BSS段中的数据也存在于二补码文件中,由于内核晓得这种数据是没有终值的,所以在二补码文件中只会记录BSS段的大小,在加载进显存时会生成一段0填充的显存空间。
紧挨到BSS段的左边就是我们常常使用到的堆空间,从图中的蓝色箭头我们可以晓得在堆空间中地址的下降方向是从低地址到高地址下降。
内核中使用start_brk标示堆的起始位置,brk标示堆当前的结束位置。当堆申请新的显存空间时,只须要将brk表针降低对应的大小,回收地址时降低对应的大小即可。例如当我们通过malloc向内核申请很小的一块显存时(128K之内),就是通过改变brk位置实现的。
堆空间的左边是一段待分配区域,用于扩充堆空间的使用。接出来就来到了文件映射与匿名映射区域。进程运行时所依赖的动态链接库中的代码段,数据段,BSS段就加载在这儿。还有我们调用mmap映射下来的一段虚拟显存空间也保存在这个区域。注意:在文件映射与匿名映射区的地址下降方向是从高地址向低地址下降。
接出来用户态虚拟显存空间的最后一块区域就是栈空间了,在这儿会保存函数运行过程所须要的局部变量以及函数参数等函数调用信息。栈空间中的地址下降方向是从高地址向低地址下降。每次进程申请新的栈地址时,其地址值是在降低的。
在内核中使用start_stack标示栈的起始位置,RSP寄存器中保存栈顶表针stackpointer,RBP寄存器中保存的是栈基地址。
在栈空间的下面也有一段待分配区域用于扩充栈空间,在栈空间的左边就是内核空间了,进程其实可以看见这段内核空间地址,并且就是不能访问。这就好比我们在酒店里其实可以看见卧室在那里,并且卧室门上写着“厨房重地,闲人免进”,我们就是进不去。
文章插图
4.264位机器上进程虚拟显存空间分布上小节中介绍的32位虚拟显存空间布局和本小节即即将介绍的64位虚拟显存空间布局都可以通过cat/proc/pid/maps或则pmappid来查看某个进程的实际虚拟显存布局。
我们晓得在32位机器上,表针的轮询范围为2^32,所能抒发的虚拟显存空间为4GB。
这么我们理所应该的会觉得在64位机器上,表针的轮询范围为2^64,所能抒发的虚拟显存空间为16EB。虚拟显存地址范围为:0x000000000-0xFFFFFFFFFFFFFFFF。
好家伙!!!16EB的显存空间,笔者都没见过如此大的c盘,在现实情况中根本不会用到如此大范围的显存空间,
事实上在目前的64位系统下只使用了48位来描述虚拟显存空间,轮询范围为2^48,所能抒发的虚拟显存空间为256TB。
其中低128T表示用户态虚拟显存空间,虚拟显存地址范围为:0x00000-0x00007FFFFFFFF000。