15分钟!一文帮小白搞懂操作系统之内存
前言
操作系统是一门比较难啃的课程,同时操作系统知识对开发者们来说是十分重要,相信各位在学操作系统的时候,有太多的抽象难以理解的词汇与概念,把我们直接劝退,即使怀着满腔热血的心情学操作系统,不到 3 分钟睡意就突然袭来。
所以本人想把自己的想法通过图解+大白话的形式,产出操作系统系列文章,让小白也能看懂,帮助大家快速科普入门
本篇开始介绍内存,内存在操作系统中还是比较重要的,理解了它,对整个操作系统的工作会有一个初步的轮廓。
内容大纲
正文
什么是内存
小故事
我们想去摆地摊(准备运行程序进程)需要经过那几 个步骤,这里猜测一下。
首先要去城管申请摊位(申请内存),城管(操作系统)根据现在剩余的地毯空间与你地毯的规模划分一块相应大小的摊位(内存)给你,接着你就可以愉快的摆摊(运行程序进程)赚钱啦。
城管也会时不时的来检查(整理内存空间碎片),摊位是否规整,有没有阻碍正常的人行道。
简而言之,电脑上的程序(进程)运行是需要使用到对应大小的物理内存。
虚拟内存
实际上运行的进程并不是直接使用物理内存地址,而是把进程使用的内存地址与实际的物理内存地址做隔离,即操作系统会为每个进程分配独立的一套「虚拟地址」。
每个进程玩自己的地址,互不干涉,至于虚拟地址怎么映射到物理地址,对进程来说是透明的,操作系统已经把这些安排的明明白白了。
操作系统会提供一种机制,将不同进程的虚拟地址和不同内存的物理地址映射起来,如下图所示
由此我们引出了两个概念:
进程中使用的内存地址叫虚拟地址 存在计算硬件里的空间地址叫物理地址
简单来说操作系统引入虚拟空间,进程持有的虚拟地址会通过 CPU 芯片中的内存管理单元(MMU)的映射关系,来转换成物理地址,再通过物理地址访问物理内存
操作系统是如何管理虚拟地址与物理内存地址之间关系?
主要有三种方式,分别是分段、分页、段页,下面我们来看看这三种内存管理方式
内存分段
程序包含若干个逻辑分段,如可由代码段、数据段、栈段、堆段组成,每个分段都有不同的属性,所以内存以分段的形式把这些段分离出来进行管理
在内存分段方式下,虚拟地址和物理地址是如何映射的?
分段管理下的虚拟地址由两部分组成,段号和段内偏移量
通过段号映射段表的项 从项中获取到段基地址 段基地址+段内偏移量=使用的物理内存
通过上述知道了,使用段号去映射段表的项,使用项中的段基地址与偏移量计算出物理内存地址,但实际上,分段方式会把程序的虚拟地址分为4段,每个段在段表中有一个项,在这一项找到段的基地址,再加上偏移量计算出物理内存地址
分段的方式,很好的解决了,程序本身不需要关心具体物理内存地址的问题,但是它仍有不足之处:
内存碎片的问题 内存交换的效率低的问题
接下来对这两个问题进行分析
分段方式是如何产生内存碎片的?
在说内存碎片之前,还是先弄明白,什么是内存碎片?,8个人去外面吃饭,因为饭点原因,人比较多,剩下的都是4人小餐桌,这些4人小餐桌就是我们所说的内存碎片,此时会有小伙伴说,把2个4人小餐桌拼凑在一起就解决了这个问题,非常简单,我们把这种方式称为内存碎片整理(涉及到内存交换)。
回到正题,我们来看一例子,假设物理内存只有1GB (1024MB),用户电脑上运行了多个程序:
浏览器占用128MB 音乐软件占用256MB 游戏占用了512MB
这个时候我们关闭浏览器,剩余物理内存1024MB -(256MB+512MB)= 256MB。但是这剩余的256MB物理内存不是连续的,被分为了两段128MB,导致没有空间再打开一个200MB的程序,如下图所示
这里的内存碎片问题共有两点:
外部内存碎片,就是多个不连续的小物理内存空间,导致新的程序无法被装载 内部内存碎片,程序所有的内存都被装载进了物理内存,但是程序有部分的内存,可能不经常使用,造成内存的浪费
解决外部内存碎片的方法就是使用内存碎片整理
内存碎片整理通过内存交换的方式来实现,我们可以把音乐软件占用的256MB加载到硬盘上面去,再从硬盘读取回来,但是读取回来的位置不再是原来的位置,而是紧跟已经占用的游戏512MB后面,这样两个128MB的空闲物理内存就合并成了一个256MB的连续物理内存,于是新的200MB新程序就能被装载进来
内存交换空间,在 Linux 系统里,是我们常看到的 Swap 空间,这块空间是从硬盘划分出来的,用于内存与硬盘的空间交换。
分段方式为什么内存交换效率低?
首先分段管理容易造成内存碎片,导致内存交换的频率较高,因为硬盘的访问速度比内存慢太多了,然后每次交换的时候,把一大段连续的内存写入到硬盘,再又从硬盘读取出来,如果交换的是一个占内存空间很大的程序,这样整个机器都会显得卡顿,过程也很慢的,所以说分段方式内存交换效率低。
为了解决内存分段管理造成的内存碎片与内存交换效率低的问题,就出现了内存分页
内存分页
分段的好处是能产生连续的内存空间,但是会出现大量内存碎片与内存交换效率低的问题
先思考一下怎么解决这两个问题,内存碎片是由多个不连续的小物理内存空间造成,如果把这些不连续的小物理内存空间组合起来,是不是解决了这个问题?同样的,内存交换的时候我们保证交换的数据小,是不是能提高内存交换的效率?
这个办法就是内存分页,分页是把整个虚拟与物理空间切成一段段固定尺寸的大小,这样一个连续并且尺寸固定的空间,我们叫页,在 Linux 下,每一页的大小为 4KB。(虚拟空间是指存储一套虚拟地址的空间)
虚拟地址与物理地址是通过页表来映射,虚拟空间内的虚拟地址一定是连续的,物理地址不一定,但可以通过连续的虚拟地址把多个不连续的物理内存组合使用。
而当进程访问的虚拟地址在页表中查不到时,系统会产生一个缺页异常,进入系统内核空间分配物理内存、更新进程页表,最后再返回用户空间,恢复进程的运行。
分页方式是如何解决内存碎片与内存交换效率慢的问题呢?
内存碎片的解决:
因为使用内存的单位变成固定大小的页,所以每个程序的虚拟空间维护的也是连续的页(虚拟地址),通过页表再映射到物理内存页,虽然映射的物理内存页不连续,但是虚拟空间是连续的,可以让它们组合起来使用,但这也只能解决外部内存碎片问题,没有解决内部内碎片问题,因为每页都有固定大小,可能某一页只使用了部分,依然会造成一些浪费。
内存交换效率慢的解决:
之前说过,减少交换数据的大小,可以提高内存交换效率,分页方式是这样解决的,如果内存空间不够时,操作系统会把其他正在运行的进程中的「最近没被使用」的内存页释放掉,也就是加载到硬盘,称为换出,一旦需要的时候再加载进来,称为换入。所以一次性写入硬盘的也只有一个页或几个页,内存的交换效率自然就提升了。
分页方式使加载程序的时候,不再需要一次性都把程序加载到物理内存中。完全可以在进行虚拟内存和物理内存的页之间的映射之后,并不真的把页加载到物理内存里,而是只有在程序运行中,需要用到对应虚拟内存页里面的指令和数据时,再加载到物理内存里面去(用大白话说,当你需要用到的时候才会去使用对应的物理内存)。
在内存分页方式下,虚拟地址和物理地址是如何映射的?
在分页机制下,每个进程都会分配一个页表,虚拟地址会分为两部分,页号和页内偏移量,页号作为页表的索引,页表包含物理页每页所在物理内存的基地址,页内偏移量+物理内存基地址就组成了物理内存地址,如下图所示
就是下面这几步
页号找到页表中的页项 获取页项的物理页号基地址 偏移量+物理页号基地址计算出物理内存地址
是不是非常的简单,但是这种分页方式使用到操作系统上会不会问题呢?那必然是会有问题的,还记得之前提到的每个进程会分配一个页表嘛?下面来为大家解开这个伏笔
在分页方式下,每个进程分配一个页表会有什么问题?
不卖关子了,每个进程分配一个页表会有空间上的缺陷,因为操作系统上可以运行非常多的进程,那不就意味着页表数量非常多!
1B(Byte 字节)=8bit,
1KB (Kilobyte 千字节)=1024B,
1MB (Megabyte 兆字节 简称“兆”)=1024KB,
1GB (Gigabyte 吉字节 又称“千兆”)=1024MB
以32 位的环境为例,虚拟地址空间范围共有 4GB,假设一个页的大小是 4KB(2^12),那么就需要大约 100 万 (2^20) 个页,每个「页表项」需要 4 个字节大小来存储,那么整个 4GB 空间范围的映射就要有 4MB 的内存来存储页表。
4MB看起来不大,但是数量上来了就很恐怖了,假设 100 个进程的话,就需要 400MB 的内存来存储页表,这是非常大的内存了,更别说 64 位的环境了。
为了解决空间上的问题,在对分页方式的基础上,进行优化,出现了多级页表方式
多级页表
在前面我们知道了,分页方式在32位环境下,以每页4KB来计算,一共有100万页,「页表项」需要 4 个字节大小来存储,一个页表包含100万个「页表项」,那么每个进程的页表需要占用4MB大小,多级页表要如何解决这种问题呢?
在页表的基础上做一次二级分页,把100万「页表项」分为一级页表「1024个页表项」,「一级页表项」下又关联二级页表「1024个页表项」,这样一级页表的1024个页表项就覆盖到了4GB的空间范围映射,并且二级页表按需加载,这样页表占用的空间就大大降低。
做个简单的计算,假设只有 20% 的一级页表项被用到了,那么页表占用的内存空间就只有 4KB(一级页表) + 20% * 4MB(二级页表)= 0.804MB,这对比单级页表的 4MB 是不是一个巨大的节约?
接着思考,在二级的基础上是不是又可以继续分级呢,能分二级,必然也能分三级、四级,在64位操作系统是做了四级分页,分为了四个目录,分别是
全局页目录项 上层页目录项 中间页目录项 页表项
TBL
多级页表虽然解决了空间上的问题,但是我们发现这种方式需要走多道转换才能找到映射的物理内存地址,经过的多道转换造成了时间上的开销。
程序是局部性的,即在一段时间内,整个程序的执行仅限于程序的某一部分。相应的,执行所访问的存储空间也局限于某个内存区域。
操作系统就利用这一特性,把最多使用的几个页表项放到TBL缓存, CPU 在寻址时,会先查 TLB,如果没找到,才会继续查常规的页表,TLB 的命中率其实很高的,因为程序最常访问的页就那么几个。
内存段页
段式与页式并不是相对的,他们也可以组合在一起使用,在段的基础上进行分页分级
先将程序划分为多个有逻辑意义的段,也就是前面提到的分段机制 接着再把每个段划分为多个页,也就是对分段划分出来的连续空间,再划分固定大小的页
虚拟地址结构由段号、段内页号和页内位移三部分组成
就是下面这几步
通过段号获取段表的段项 通过段项获取到页表地址 通过页表地址找到段页表 通过段内页号找到段页表的段页项 通过段页项获取物理页基地址 通过物理页基地址+偏移量计算出物理内存地址
总结
进程并不是直接使用物理内存,而是通过虚拟地址映射使用,所以操作系统会为每个进程分配虚拟空间(一套地址),使得每个进程使用物理内存互不影响,相互隔离。
启用大量进程造成内存紧张不足的时候,操作系统会通过内存交换技术,把不常使用的内存加载到硬盘(换出),使用时从硬盘加载到内存(换入)
操作系统对内存的管理方式分为三种,分段、分页、段页,分段的好处是物理内存空间是连续的,但是缺点很明显,容易造成内存碎片,并且内存交换效率慢,采用分页能很好的解决分段的缺陷,通过连续的虚拟地址解决了外部内存碎片问题,每次内存交换将最近不使用的内存以页的单位换出换入,保证交换数据大小,提高内存交换效率,但是会有页表空间占用问题,为了解决此问题,在分页的基础上优化成多级分页+TBL方式来减少空间占用与时间消耗,最后一个就是段页,段页是分段与分页的结合。
通过思考,我们发现,多级分页通过树+懒加载+缓存解决了空间占用与时间消耗的问题,虚拟地址很好的做到了让进程与物理内存地址解耦,正因如此,多进程使用物理内存时才不会有冲突,很好的做到了相互独立与隔离。