太赞了20张图揭开内存管理的迷雾,瞬间

中科白癜风医院康复经历分享 http://www.xftobacco.com/zzbb/myjd/m/1941.html

作者:小林coding文章来源:小林coding

前言

之前有不少读者跟我反馈,能不能写图解操作系统?

既然那么多读者想看,我最近就在疯狂的复习操作系统的知识。

操作系统确实是比较难啃的一门课,至少我认为比计算机网络难太多了,但它的重要性就不用我多说了。

学操作系统的时候,主要痛苦的地方,有太多的抽象难以理解的词语或概念,非常容易被劝退。

即使怀着满腔热血的心情开始学操作系统,不过3分钟睡意就突然袭来。。。

该啃的还是得啃的,该图解的还是得图解的,万众期待的「图解操作系统」的系列来了。

本篇跟大家说说内存管理,内存管理还是比较重要的一个环节,理解了它,至少对整个操作系统的工作会有一个初步的轮廓,这也难怪面试的时候常问内存管理。

干就完事,本文的提纲:

本文提纲

正文

虚拟内存

如果你是电子相关专业的,肯定在大学里捣鼓过单片机。

单片机是没有操作系统的,所以每次写完代码,都需要借助工具把程序烧录进去,这样程序才能跑起来。

另外,单片机的CPU是直接操作内存的「物理地址」。

在这种情况下,要想在内存中同时运行两个程序是不可能的。如果第一个程序在的位置写入一个新的值,将会擦掉第二个程序存放在相同位置上的所有内容,所以同时运行两个程序是根本行不通的,这两个程序会立刻崩溃。

操作系统是如何解决这个问题呢?

这里关键的问题是这两个程序都引用了绝对物理地址,而这正是我们最需要避免的。

我们可以把进程所使用的地址「隔离」开来,即让操作系统为每个进程分配独立的一套「虚拟地址」,人人都有,大家自己玩自己的地址就行,互不干涉。但是有个前提每个进程都不能访问物理地址,至于虚拟地址最终怎么落到物理内存里,对进程来说是透明的,操作系统已经把这些都安排的明明白白了。

进程的中间层

操作系统会提供一种机制,将不同进程的虚拟地址和不同内存的物理地址映射起来。

如果程序要访问虚拟地址的时候,由操作系统转换成不同的物理地址,这样不同的进程运行的时候,写入的是不同的物理地址,这样就不会冲突了。

于是,这里就引出了两种地址的概念:

我们程序所使用的内存地址叫做虚拟内存地址(VirtualMemoryAddress)实际存在硬件里面的空间地址叫物理内存地址(PhysicalMemoryAddress)。操作系统引入了虚拟内存,进程持有的虚拟地址会通过CPU芯片中的内存管理单元(MMU)的映射关系,来转换变成物理地址,然后再通过物理地址访问内存,如下图所示:

虚拟地址寻址

操作系统是如何管理虚拟地址与物理地址之间的关系?

主要有两种方式,分别是内存分段和内存分页,分段是比较早提出的,我们先来看看内存分段。

内存分段

程序是由若干个逻辑分段组成的,如可由代码分段、数据分段、栈段、堆段组成。不同的段是有不同的属性的,所以就用分段(Segmentation)的形式把这些段分离出来。

分段机制下,虚拟地址和物理地址是如何映射的?

分段机制下的虚拟地址由两部分组成,段选择子和段内偏移量。

内存分段-寻址的方式

段选择子就保存在段寄存器里面。段选择子里面最重要的是段号,用作段表的索引。段表里面保存的是这个段的基地址、段的界限和特权等级等。虚拟地址中的段内偏移量应该位于0和段界限之间,如果段内偏移量是合法的,就将段基地址加上段内偏移量得到物理内存地址。在上面了,知道了虚拟地址是通过段表与物理地址进行映射的,分段机制会把程序的虚拟地址分成4个段,每个段在段表中有一个项,在这一项找到段的基地址,再加上偏移量,于是就能找到物理内存中的地址,如下图:

内存分段-虚拟地址与物理地址

如果要访问段3中偏移量的虚拟地址,我们可以计算出物理地址为,段3基地址+偏移量=7。

分段的办法很好,解决了程序本身不需要关心具体的物理内存地址的问题,但它也有一些不足之处:

第一个就是内存碎片的问题。第二个就是内存交换的效率低的问题。接下来,说说为什么会有这两个问题。

我们先来看看,分段为什么会产生内存碎片的问题?

我们来看看这样一个例子。假设有1G的物理内存,用户执行了多个程序,其中:

游戏占用了MB内存浏览器占用了MB内存音乐占用了MB内存。这个时候,如果我们关闭了浏览器,则空闲内存还有--=MB。

如果这个MB不是连续的,被分成了两段MB内存,这就会导致没有空间再打开一个MB的程序。

内存碎片的问题

这里的内存碎片的问题共有两处地方:

外部内存碎片,也就是产生了多个不连续的小物理内存,导致新的程序无法被装载;内部内存碎片,程序所有的内存都被装载到了物理内存,但是这个程序有部分的内存可能并不是很常使用,这也会导致内存的浪费;针对上面两种内存碎片的问题,解决的方式会有所不同。

解决外部内存碎片的问题就是内存交换。

可以把音乐程序占用的那MB内存写到硬盘上,然后再从硬盘上读回来到内存里。不过再读回的时候,我们不能装载回原来的位置,而是紧紧跟着那已经被占用了的MB内存后面。这样就能空缺出连续的MB空间,于是新的MB程序就可以装载进来。

这个内存交换空间,在Linux系统里,也就是我们常看到的Swap空间,这块空间是从硬盘划分出来的,用于内存与硬盘的空间交换。

再来看看,分段为什么会导致内存交换效率低的问题?

对于多进程的系统来说,用分段的方式,内存碎片是很容易产生的,产生了内存碎片,那不得不重新Swap内存区域,这个过程会产生性能瓶颈。

因为硬盘的访问速度要比内存慢太多了,每一次内存交换,我们都需要把一大段连续的内存数据写到硬盘上。

所以,如果内存交换的时候,交换的是一个占内存空间很大的程序,这样整个机器都会显得卡顿。

为了解决内存分段的内存碎片和内存交换效率低的问题,就出现了内存分页。

内存分页

分段的好处就是能产生连续的内存空间,但是会出现内存碎片和内存交换的空间太大的问题。

要解决这些问题,那么就要想出能少出现一些内存碎片的办法。另外,当需要进行内存交换的时候,让需要交换写入或者从磁盘装载的数据更少一点,这样就可以解决问题了。这个办法,也就是内存分页(Paging)。

分页是把整个虚拟和物理内存空间切成一段段固定尺寸的大小。这样一个连续并且尺寸固定的内存空间,我们叫页(Page)。在Linux下,每一页的大小为4KB。

虚拟地址与物理地址之间通过页表来映射,如下图:

内存映射

页表实际上存储在CPU的内存管理单元(MMU)中,于是CPU就可以直接通过MMU,找出要实际要访问的物理内存地址。

而当进程访问的虚拟地址在页表中查不到时,系统会产生一个缺页异常,进入系统内核空间分配物理内存、更新进程页表,最后再返回用户空间,恢复进程的运行。

分页是怎么解决分段的内存碎片、内存交换效率低的问题?

由于内存空间都是预先划分好的,也就不会像分段会产生间隙非常小的内存,这正是分段会产生内存碎片的原因。而采用了分页,那么释放的内存都是以页为单位释放的,也就不会产生无法给进程使用的小内存。

如果内存空间不够,操作系统会把其他正在运行的进程中的「最近没被使用」的内存页面给释放掉,也就是暂时写在硬盘上,称为换出(SwapOut)。一旦需要的时候,再加载进来,称为换入(SwapIn)。所以,一次性写入磁盘的也只有少数的一个页或者几个页,不会花太多时间,内存交换的效率就相对比较高。

换入换出

更进一步地,分页的方式使得我们在加载程序的时候,不再需要一次性都把程序加载到物理内存中。我们完全可以在进行虚拟内存和物理内存的页之间的映射之后,并不真的把页加载到物理内存里,而是只有在程序运行中,需要用到对应虚拟内存页里面的指令和数据时,再加载到物理内存里面去。

分页机制下,虚拟地址和物理地址是如何映射的?

在分页机制下,虚拟地址分为两部分,页号和页内偏移。页号作为页表的索引,页表包含物理页每页所在物理内存的基地址,这个基地址与页内偏移的组合就形成了物理内存地址,见下图。

内存分页寻址

总结一下,对于一个内存地址转换,其实就是这样三个步骤:

把虚拟内存地址,切分成页号和偏移量;根据页号,从页表里面,查询对应的物理页号;直接拿物理页号,加上前面的偏移量,就得到了物理内存地址。下面举个例子,虚拟内存中的页通过页表映射为了物理内存中的页,如下图:

虚拟页与物理页的映射

这看起来似乎没什么毛病,但是放到实际中操作系统,这种简单的分页是肯定是会有问题的。

简单的分页有什么缺陷吗?

有空间上的缺陷。

因为操作系统是可以同时运行非常多的进程的,那这不就意味着页表会非常的庞大。

在32位的环境下,虚拟地址空间共有4GB,假设一个页的大小是4KB(2^12),那么就需要大约万(2^20)个页,每个「页表项」需要4个字节大小来存储,那么整个4GB空间的映射就需要有4MB的内存来存储页表。

这4MB大小的页表,看起来也不是很大。但是要知道每个进程都是有自己的虚拟地址空间的,也就说都有自己的页表。

那么,个进程的话,就需要MB的内存来存储页表,这是非常大的内存了,更别说64位的环境了。

多级页表

要解决上面的问题,就需要采用的是一种叫作多级页表(Multi-LevelPageTable)的解决方案。

在前面我们知道了,对于单页表的实现方式,在32位和页大小4KB的环境下,一个进程的页表需要装下多万个「页表项」,并且每个页表项是占用4字节大小的,于是相当于每个页表需占用4MB大小的空间。

我们把这个多万个「页表项」的单级页表再分页,将页表(一级页表)分为个页表(二级页表),每个表(二级页表)中包含个「页表项」,形成二级分页。如下图所示:

二级分页

你可能会问,分了二级表,映射4GB地址空间就需要4KB(一级页表)+4MB(二级页表)的内存,这样占用空间不是更大了吗?

当然如果4GB的虚拟地址全部都映射到了物理内上的,二级分页占用空间确实是更大了,但是,我们往往不会为一个进程分配那么多内存。

其实我们应该换个角度来看问题,还记得计算机组成原理里面无处不在的局部性原理么?

每个进程都有4GB的虚拟地址空间,而显然对于大多数程序来说,其使用到的空间远未达到4GB,因为会存在部分对应的页表项都是空的,根本没有分配,对于已分配的页表项,如果存在最近一定时间未访问的页表,在物理内存紧张的情况下,操作系统会将页面换出到硬盘,也就是说不会占用物理内存。

如果使用了二级分页,一级页表就可以覆盖整个4GB虚拟地址空间,但如果某个一级页表的页表项没有被用到,也就不需要创建这个页表项对应的二级页表了,即可以在需要时才创建二级页表。做个简单的计算,假设只有20%的一级页表项被用到了,那么页表占用的内存空间就只有4KB(一级页表)+20%*4MB(二级页表)=0.MB,这对比单级页表的4MB是不是一个巨大的节约?

那么为什么不分级的页表就做不到这样节约内存呢?我们从页表的性质来看,保存在内存中的页表承担的职责是将虚拟地址翻译成物理地址。假如虚拟地址在页表中找不到对应的页表项,计算机系统就不能工作了。所以页表一定要覆盖全部虚拟地址空间,不分级的页表就需要有多万个页表项来映射,而二级分页则只需要个页表项(此时一级页表覆盖到了全部虚拟地址空间,二级页表在需要时创建)。

我们把二级分页再推广到多级页表,就会发现页表占用的内存空间更少了,这一切都要归功于对局部性原理的充分应用。

对于64位的系统,两级分页肯定不够了,就变成了四级目录,分别是:

全局页目录项PGD(PageGlobalDirectory);上层页目录项PUD(PageUpperDirectory);中间页目录项PMD(PageMiddleDirectory);页表项PTE(PageTableEntry);

四级目录

TLB

多级页表虽然解决了空间上的问题,但是虚拟地址到物理地址的转换就多了几道转换的工序,这显然就降低了这俩地址转换的速度,也就是带来了时间上的开销。

程序是有局部性的,即在一段时间内,整个程序的执行仅限于程序中的某一部分。相应地,执行所访问的存储空间也局限于某个内存区域。

程序的局部性

我们就可以利用这一特性,把最常访问的几个页表项存储到访问速度更快的硬件,于是计算机科学家们,就在CPU芯片中,加入了一个专门存放程序最常访问的页表项的Cache,这个Cache就是TLB(TranslationLookasideBuffer),通常称为页表缓存、转址旁路缓存、快表等。

地址转换

在CPU芯片里面,封装了内存管理单元(MemoryManagementUnit)芯片,它用来完成地址转换和TLB的访问与交互。

有了TLB后,那么CPU在寻址时,会先查TLB,如果没找到,才会继续查常规的页表。

TLB的命中率其实是很高的,因为程序最常访问的页就那么几个。

段页式内存管理

内存分段和内存分页并不是对立的,它们是可以组合起来在同一个系统中使用的,那么组合起来后,通常称为段页式内存管理。

段页式地址空间

段页式内存管理实现的方式:

先将程序划分为多个有逻辑意义的段,也就是前面提到的分段机制;接着再把每个段划分为多个页,也就是对分段划分出来的连续空间,再划分固定大小的页;这样,地址结构就由段号、段内页号和页内位移三部分组成。

用于段页式地址变换的数据结构是每一个程序一张段表,每个段又建立一张页表,段表中的地址是页表的起始地址,而页表中的地址则为某页的物理页号,如图所示:

段页式管理中的段表、页表与内存的关系

段页式地址变换中要得到物理地址须经过三次内存访问:

第一次访问段表,得到页表起始地址;第二次访问页表,得到物理页号;第三次将物理页号与页内位移组合,得到物理地址。可用软、硬件相结合的方法实现段页式地址变换,这样虽然增加了硬件成本和系统开销,但提高了内存的利用率。

Linux内存管理

那么,Linux操作系统采用了哪种方式来管理内存呢?

在回答这个问题前,我们得先看看Intel处理器的发展历史。

早期Intel的处理器从开始使用的是段式内存管理。但是很快发现,光有段式内存管理而没有页式内存管理是不够的,这会使它的X86系列会失去市场的竞争力。因此,在不久以后的中就实现了对页式内存管理。也就是说,除了完成并完善从开始的段式内存管理的同时还实现了页式内存管理。

但是这个的页式内存管理设计时,没有绕开段式内存管理,而是建立在段式内存管理的基础上,这就意味着,页式内存管理的作用是在由段式内存管理所映射而成的的地址上再加上一层地址映射。

由于此时段式内存管理映射而成的地址不再是“物理地址”了,Intel就称之为“线性地址”(也称虚拟地址)。于是,段式内存管理先将逻辑地址映射成线性地址,然后再由页式内存管理将线性地址映射成物理地址。

IntelX86逻辑地址解析过程

这里说明下逻辑地址和线性

转载请注明地址:http://www.1xbbk.net/jwbzn/7363.html


  • 上一篇文章:
  • 下一篇文章:
  • 网站简介 广告合作 发布优势 服务条款 隐私保护 网站地图 版权声明
    冀ICP备19027023号-7