美文网首页
《操作系统概念精要》之内存篇(一)

《操作系统概念精要》之内存篇(一)

作者: 小pb | 来源:发表于2019-11-11 12:35 被阅读0次

基本概念

内存是现在计算机运行的核心。CPU可以直接访问的通用存储只有内存和处理器的内置寄存器。机器指令可以用内存地址作为参数,但是磁盘地址不可以。

在访问速度上,寄存器的内容一般都可以在一个时钟周期解释并执行完。但是对于内存,可能需要多个时钟周期。所以为了访问速度上能更加快速,一般会有高速缓存

在系统安全上,首先,用户的进程不能影响操作系统的执行; 在多用户系统上,还应该保护不同的进程之间不能互相影响。所以一般会有专门的硬件方式来进行保护。
为了进程之间不会相互影响,首先,我们需要确保每个进程都有一个单独的内存空间。单独的进程内存空间可以保护进程不互相影响。
为了分开内存空间,我们需要能够确定一个进程可以访问的合法地址的范围;并且确保该进程只能访问这些合法地址。 一般通过两个寄存器来实现, 基地址寄存器界限地址寄存器。
如下图,如果基地址是300040,而界限寄存器为120900,那么程序可以合法访问到的地址为300040 ~ 420939的所包含的地址。

logical_address.png
地址绑定

一般的,进程在执行时,会先从磁盘被调入内存,而且根据内存的管理,有可能在执行的时候,被换出到磁盘上。但是磁盘的内容会被调入到物理内存的哪个地方,这个就是后面要讨论的内存分配机制。

大多数系统允许用户进程放在屋里内存中的任意位置。也就是说,物理内存的地址是从000000开始的,但是用户进程的地址不必也是000000。
实际上,在用户程序被执行前,会有很多的步骤,这些步骤中对地址会有不同的表示方法。

  • 编译:在程序中,一般我们不用地址来表示,比如C语言都是按照符号来表示的,在编译的时候,它是一堆符号地址,编译器将这些符号地址绑定到可重定向的地址,比如:从本模块的第 14个字节开始。
  • 加载:在上面编译后,生成了可重定向地址,那么在加载时,加载器或者链接器会把这些可重定向地址 加上自己在地址空间的基地址,从而形成绝对地址。
  • 执行:进程在执行的时候,从地址空间中映射到计算机的物理地址上。具体映射方法需要先了解下逻辑地址空间和物理地址空间。
逻辑地址空间和物理地址空间

在程序执行的时候,从程序中加载的地址,称为逻辑地址。它是一个照片,我可以用手指出哪里是这个人的眼睛,耳朵,鼻子,但是这只是在照片的位置。这个人本人的肉体才是真正存在的东西。物理地址就好比这个人的肉体,是一个实际存在的东西。

加载时的地址绑定方法生成一个相同的逻辑地址和物理地址。然而,执行时,逻辑地址和物理内存上的地址不一定是一样的。后面内存分配会讲到。

动态加载和动态链接

动态加载指的是一个进程只有在执行的时候才被加载,而不是一次性把所有的进程全部放进内存中。
动态链接类似于动态加载。但是这里不是加载和是链接,会延迟到运行。动态链接这里链接的一般称为动态链接库。它是系统库,一般会驻留在内存中,供所有的用户程序使用。
它的原理是:当程序中有动态链接,在二进制文件中,每个库程序的引用都会有个存根(stub)。存根是一小段代码,用来指出如何定位适当的内存驻留库程序,以及不在内存时,如何加载。

动态链接库的好处是,一个库的更新,所有使用它的程序都会自动使用新的版本。

交换

最后一个基本概念是交换(swap)。进程可以暂时从内存交换到备份存储中,当再次执行的时候,再调回到内存中。

标准交换

这种交换和概念定义的一样,一般备份存储为磁盘。
这种交换的上下文切换时间相当高,假设用户进程大小为100MB,并且磁盘的传输速度为50M/s, 那么换人和换成分别需要2s,加起来可能需要4s。

交换也受到其他因素的约束,如果我们想要换出一个进程,那么确保该进程是完全处于空闲的。特别需要关注等待I/O。当需要换出一个进程以释放内存时,该进程可能正在等待I/O操作。
然而如果I/O异步访问用户内存的I/O缓冲区,那么该进程就不能被换出。假定由于设备忙,I/O操作正在排队等待。如果我们需要换出P1进程而执行P2进程,那么I/O操作可能试图使用现在已经属于进程P2的内存。
解决的办法有两种:

  • 不能换出等待处理I/O的进程
  • I/O操作的执行只能操作系统的缓冲池。
    只有在进程进入时,操作系统缓冲才能与进程之间进行数据迁移。但是这增加了开销。
移动设备的交换

虽然用于PC和服务器的大多数操作系统都支持一些交换。但是移动设备通常不支持任何形式的交换。
移动设备通常是闪存(U盘,内存卡)而不是更大空间的硬盘。所以这是不支持交换的一个原因之一。还有一些原因就是,闪存和内存之间的数据交换的吞吐量差。
苹果的IOS系统,当空闲内存降低到一定的阈值时,不是采用交换,而是要求应用程序资源放弃分配的内存,只读数据可以从系统中删除,以后如有必要再从闪存中重新加载。它修改的数据不会被删除,然而, 操作系统可以终止任意的未能释放足够多内存的用户程序。
Android系统不支持交换。而当没有足够内存的时候,系统可以终止任何进程。但是他在删除前会把应用程序的状态写入到闪存,以便后面快速启动。
由于这些限制,所以移动的程序员应该小心分配内存,避免有资源泄露。

内存分配

我们通常需要把多个进程同时放在内存中,因此我们需要进程内存分配。一般内存分为两个区域:一个用于驻留在操作系统中,另一个用于用户进程。操作系统可以放在高位置,也可以放在低内存,影响这一决定的通常为中断向量的位置。一般PC是放在低内存位置的。

分区

内存分配最简单的分配方式就是:将内存分为多个固定大小的分区。每个分区可以包含一个进程,因此,多道程序设计的受限于分区数。使用这种多分区方法,那么当一个分区空闲时,可以从进程的就输入队列中选择一个进程,加载到空闲分区。虽然这种方案在现在的操作系统中已经不用,但是它的思想为后面很多内存分配提供了思想。

除此之外,还有一个可变分区方案,它的主要思想是:
操作系统有一个表,用于记录哪些内存可用,哪些内存已经使用。
开始,所有的内存都有可用于用户进程,因此可以看做是一个很大的内存。随着进程进入系统,操作系统根据所有进程的内存需要和现有的内存情况,决定哪些系统可分配内存。
在任何时候,都有一个可用块大小的列表和一个输入队列。操作系统根据调度算法来对输入队列进行排序。内存不断地分配给进程,直到下一个进程的内存需求不能满足为止,这时没有足够大的可用块来加载进程。或者继续往下扫描,输入队列,看看有没有其他内存需求比较小的进程可以被满足。

动态存储分配问题

通常,按上面的分配算法,可用的内存块分散在内存里的不同大小的孔(这里既是一个一个小的内存块)的集合。当进程需要内存时,系统为该进程查找足够大的空,如果孔太大,那么就分为两块:一块分配给内存,另一块还给孔集合。当进程终止时,他将释放内存,该内存将还给孔集合。
如果新孔与其他孔相邻,那么将这个孔合并成一个大孔。这是系统继续检测,有没有符合要求的处于等待的进程。
这种分配方法被称为动态存储分配问题。 这种方法在我们程序设计中有很多的例子,比如(C++STL的内存分配,memory cache)。

这种方法有许多变种,其中根据从一组可用的孔中选择一个空闲孔的最为常用的方法包括: 首次适应, 最优适应,最差适应

  • 首次适应:分配首个足够大的空。查找从头开始,也可以从上次首次适应结束的时候开始,寻找一个足够大的空闲孔,分配个进程。
  • 最优适应: 分配足够小的孔,只要可以满足进程的内存需求。需要查找整个列表,所以可能要求,列表按大小进程排序,以便更快速的找到最小满足的孔。
  • 最差适应: 分配最大的孔。这个和最优相对,也要查找整个列表。
    经过模拟显示:首次适应和最优适应的执行效果和空间利用方面都要优于最差适应。

碎片

外部碎片:根据上面的三种算法,随着进程的加载到内存和从内存退出,空闲内存空间被分为小的片段。当总的可用内存纸盒可以满足请求但并不连续时,他不能分配给进程。这些 不连续的内存块就成了碎片了。
内部碎片: 假设有一个1800字节的碎片,并采取上面的算法进行分配,进程需要1798 的内存。那么分配完了以后,就剩下两个字节,这两个字节也需要维护,但是维护的代价比他本身的价值要大的多,所以在分配的时候,把这两个字节也分配给进程,那么这两个字节就在进程内部形成了碎片。

在清理内存碎片的时候,我们有两种解决方案:

  • 把所有的进程的移到内存的一端,把孔移到另一端,但是这种代价太高。
  • 允许进程的逻辑地址空间不是连续的; 从而让代码和数据分离,进程在使用的时候,当物理内存可用,就允许进程分配内存, 然后根据不同的基地址寄存器和界限寄存器来控制和保护进程的内存。 也就是分段和分页技术。

相关文章

网友评论

      本文标题:《操作系统概念精要》之内存篇(一)

      本文链接:https://www.haomeiwen.com/subject/bgnbbctx.html