根据 CPU 能否直接访问,计算机中的存储器可以分为两类,即内部存储器(简称内存)和外部存储器(简称外存),内存还可以细分为寄存器、高速缓存和主存。日常生活中,我们所说的内存指的仅是主存,本文也遵照这种习惯。
内存是计算机重要的存储部件之一,只有进入内存的数据或指令才能得到 CPU 的处理和执行。通常情况下,操作系统会将整个内存空间划分为 2 个区域,分别是系统区和用户区,系统区用于存储操作系统自身要执行的程序,例如中断处理程序、进程调度程序等。用户区用于存储计算机用户的程序和数据。本节介绍的有关操作系统对内存空间的管理,主要针对的是用户区的管理。
操作系统负责管理所有的内存空间,包括内存空间的分配和回收、提高内存空间的利用率、防止内存中的数据被恶意篡改等等。因此,本节将带您了解与内存管理相关的一些基本概念。
进程地址空间
讲解进程地址空间之前,您首先要了解什么是物理地址和逻辑地址。
我们知道,整个内存空间会分成很多个字节(8 个二进制位),每个字节都拥有一个唯一的地址,通常称为物理地址或者绝对地址。也就是说,物理地址指的是内存空间中每个字节的真实地址。
早期的计算机中,程序确实是通过直接操作内存的物理地址来实现功能的,但这种方式存在很多问题,例如:
- 程序所需要的内存空间不能超过实际的内存空间,否则程序将无法进入内存,也就无法执行;
- 当内存中存储多个程序时,程序 A 可以修改程序 B 的数据,这是非常危险的;
- 程序员必须清楚地知道内存的具体使用情况,比如哪些内存空间可以用,哪些不能用,这极大地影响了程序员的开发效率。
为了解决这些问题,人们想出了一种间接操作物理地址的方式,即使用逻辑地址。逻辑地址又称相对地址,是程序中使用的内存地址。例如程序员在编写程序时,可以将数据依次存储到以 0 为起始地址的内存空间中,该地址就是逻辑地址。
大多数情况下,逻辑地址和物理地址不同,当程序载入内存时,操作系统会采用既定的转换策略,将程序中用到的逻辑地址转换成真实的物理地址。例如,程序中使用的以 0 为起始地址的内存空间,操作系统会将其转换为以 0x12345678 为起始地址的实际物理空间。
程序从编辑到运行的整个过程,所使用的内存地址会经历 3 个阶段的转变:
- 符号地址:编辑程序的过程中,我们直接使用变量名、常量等表示目标数据所在的地址;
- 逻辑地址:程序在编译时,编译器会将程序中所有的符号地址转变为逻辑地址;
- 物理地址:编译后的程序加载到内存中时,操作系统会将每个逻辑地址转换为真实的物理地址。
程序中所有逻辑地址的集合,称为逻辑地址空间;相应的,这些逻辑地址所对应的物理地址的集合,称为物理地址空间。
所谓进程地址空间,指的是进程使用的逻辑地址空间。以 32 位操作系统为例,理论上各个进程所能使用的逻辑地址空间的大小为 4 GB,但实际上,操作系统将整个逻辑地址空间划分了很多区域,用户进程所能使用的逻辑地址空间通常仅有2~3
GB。
地址变换
地址变换,又称地址映射或地址重定位,指的是将逻辑地址转换成物理地址的过程。
地址变换的实现方式有 2 种,分别是静态地址重定位和动态地址重定位。
1) 静态地址重定位
所谓静态地址重定位,是指将程序装入内存时,将程序中使用的逻辑地址全部转换成绝对地址,进入内存后的程序将不再进行地址变换。
经过静态地址重定位进入内存的程序,具备以下 2 个特性:
- 程序的存储位置将不再改变;
- 程序存储在连续的内存空间中,而不是分散存储。
2) 动态地址重定位
动态地址重定位指的是在程序执行过程中将指令中使用的逻辑地址转换为物理地址。
也就是说,程序进入内存后,代码中使用的仍是逻辑地址,直至 CPU 访问某条指令前,才将该指令中使用的逻辑地址转换为物理地址。因此,采用动态地址重定位进入内存的程序,其内存位置还可以进行再次移动。
动态地址重定位是操作系统控制内存管理单元(简称 MMU)完成的。MMU 是一个硬件设备,专门负责将逻辑地址转换为物理地址。
内存碎片
频繁的加载和删除进程,内存的可用空间往往会被分隔成很多小的片段,如下图所示。
图 1 内存碎片
图中,网状部分模拟的就是内存碎片。
某些空闲片段的可用容量很小,以致操作系统无法再将其分配给其他进程使用,这样的内存片段称为碎片(或零头)。也就是说,内存碎片指的是空闲但无法利用的内存空间。
内存碎片有两种存在形式,分别称为外部碎片和内部碎片。
1) 外部碎片
外部碎片指的是容量太小,无法再分配给任何进程使用的内存空间。
通过使用动态地址重定位技术,移动内存中某些进程的存储位置,就可以有效减少外部碎片的数量。此外,采用分页存储方式,也可以有效减少外部碎片的产生。
2) 内部碎片
内部碎片指的是已经分配给某个进程,但未被有效利用的内存空间。例如,操作系统为某个进程分配了 10MB 的内存空间,但该进程只使用了 8MB,那么剩余的 2MB 就称为内部碎片。
采用分段存储方式,可有效减少内部碎片的产生。
内存分配方式
用户程序进入内存时,操作系统需要为其分配所需的内存空间。
不同的操作系统,内存的分配方式也不一样,大体可分为 2 种,分别是连续分配方式和离散分区分配方式。
1) 连续分配方式
所谓连续分配,操作系统会给用户程序分配一整块内存空间,使程序能够连续存储。
连续分配的具体实现方式有 4 种,如表 1 所示。
实现方式 | 描 述 |
---|---|
单一连续分配 | 最简单的一种实现连续分配的方法,在早期的计算机中非常流行。
用户区只能驻留一道程序,且只能为 1 个用户使用。因此,该分配方式仅适用于单用户单任务操作系统,例如 MS-DOS 系统和 CP/M 系统。 |
固定分区分配 | 最简单的实现多道程序系统的内存分配方式。
此方式将内存分为几个固定大小的区域,每个区域可以装入一道程序。不同区域的空间大小可以相等,也可以不等。 |
动态分区分配 | 操作系统会根据程序所需要的内存大小,动态将整个内存空间划分为多个区域。
此分配方式的实现,需要建立空闲分区表或者空闲分区链,方便操作系统分配和回收内存空间。 |
动态重定位分区分配 | 该方式可以看做是动态分区分配方式的“升级版”。当内存空间不足时,此方式会移动已进入内存的程序的存储位置(借助动态地址重定位技术),将它们集中存储在内存的一端,从而使内存碎片同空闲空间整合到内存的另一端,然后再采用动态分区分配的方式分配和回收内存空间。 |
2) 离散分区分配方式
连续分配方式很容易产生外部碎片,虽然动态重定位技术可以减少外部碎片的数量,但移动程序的存储位置会增加系统开销,影响系统效率。
和连续存储方式恰恰相反,离散分区分配方式指的是将一个程序分解成很多小的模块,然后将这些模块分散存储到内存中的不同区域。
离散分区分配方式的具体实现方式有 3 种,分别是页式存储,段式存储和段页式存储。
内存交换
内存交换,又称内存对换,是指当内存空间不足时,操作系统会将内存中暂时不运行(处于等待状态)的进程移至外存(硬盘),为其他进程腾出可用空间,一段时间后操作系统会再将其换回内存。
注意,内存交换针对的是某个进程,是将进程整体换入或换出内存的一种技术。借助内存交换技术,有助于计算机并发执行多个大型进程,因此内存交换技术又被称为内存压缩技术。
为了提高内存交换的效率,操作系统会将外存空间(通常为硬盘)分为 2 个区域,称为文件区和交换区。文件区存储用户需要永久存放的数据,交换区负责存储从内存移出的进程和数据。
考虑到文件区和交换区的不同用途,文件区采用的是离散分区分配方式,以提高空间的利用率;交换区采用的是连续分区分配方式,以提高内存交换的效率。
页式存储
页式存储是离散分区分配的一种具体实现方式。首先将该程序使用的逻辑地址空间分成很多大小相同的页(或者称为页面);整个内存空间也会被分成很多块,块的大小和页面的大小相等。
当操作系统为某个程序分配内存空间时,会以内存块为单位,将进程的若干个页分散存储到同等大小的内存块中。
对于进程的若干个页,最后一页的存储空间很可能未充分利用,因此会产生内部碎片。
由于进程的各个页分散存储在内存空间中,为了保证进程的完整,操作系统会建立一张表格(称为页表),记录进程的各个页所在内存的实际位置。
段式存储
和页式存储类似,段式存储也将程序分散地存储在内存的各个区域。不同之处在于,段式存储是将大小不同的程序段分散存储在内存的各个区域。
一个完整的程序,通常由很多个模块组成,每个模块负责完成一定的功能,每个模块包含的代码和数据量不同,因此占用逻辑地址空间的大小也不尽相同。段式内存管理的实现方式是:以模块为单位,将程序分散存储在内存空间中。
采用段式存储方式存储程序时,各个程序段分散存储在内存中的不同区域,但每个程序段内部的代码和数据是连续存储的。
为了保证程序的完整,操作系统在分段存储程序时,会建立一张表格(称为段表),记录各个程序段在内存中的实际物理位置。
段页式存储
在页式存储和段式存储的基础上,操作系统还可以使用段页式存储方式来实现对内存空间的离散分配。
所谓段页式存储,即先将用户程序分成若干个程序段,再将每个程序段分为若干个页,最后将所有页面分散存储在内存空间中。
由于段页式存储的具体实现方式较为复杂,这里不再做详细介绍。