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

2019-11-11  本文已影响0人  小pb

基本概念

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

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

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

logical_address.png
地址绑定

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

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

逻辑地址空间和物理地址空间

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

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

动态加载和动态链接

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

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

交换

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

标准交换

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

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

移动设备的交换

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

内存分配

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

分区

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

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

动态存储分配问题

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

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

碎片

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

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

上一篇 下一篇

猜你喜欢

热点阅读