目录
连续分配存储管理方式是指将内存空间连续地分配给用户进程的技术。在这种方式下,内存被分为系统区和用户区,系统区供操作系统使用,用户区供用户进程使用。当一个用户进程执行时,操作系统为其分配一个连续的内存区域,直到进程结束或换出内存。这种方式的优点是简单高效,缺点是内存利用率较低,且只能用于单用户、单任务的操作系统。
单一连续分配
单一连续分配(Single Contiguous Allocation)是一种最简单的内存管理方法,适用于早期计算机系统,也称为单一分区分配。它将内存分为两个主要部分:系统区和用户区。用户区仅有一个单独的分区供用户程序使用。
内存划分
-
系统区(System Area)
- 位置:位于内存的低地址区域。
- 用途:存放操作系统核心和相关系统服务,保持常驻内存中。
-
用户区(User Area)
- 位置:紧接系统区之后,覆盖从系统区结束到内存的剩余部分。
- 用途:用于存放用户程序和数据。整个用户区被分配给一个用户进程。
单一连续分配的特点
- 简单性:实现简单,易于编程和管理,无需复杂的内存管理算法。
- 没有外部碎片:由于只有一个用户进程加载进内存,不存在外部碎片问题,即内存中的空闲空间始终是连续的。
- 覆盖技术:可以采用覆盖技术,以便在有限内存中运行较大的程序段,虽然覆盖增加了编程的复杂度,但解决了一部分内存不足问题。
单一连续分配的优缺点
优点
- 实现简单:算法和结构简单,易于实现和维护。
- 无外部碎片:由于内存中的连续性,无外部碎片问题。
- 覆盖技术支持:允许通过覆盖技术运行业务逻辑复杂且占用内存较大的程序。
- 不需要额外的硬件支持:不需要复杂的硬件支持(如内存管理单元)。
缺点
- 仅支持单用户、单任务:只能同时运行一个用户程序,无法支持多用户、多任务环境。
- 内部碎片:如果用户进程实际需要的内存量小于用户区的大小,会产生内部碎片,导致内存利用率低。
- 低内存利用率:无法充分利用系统内存,对于现代系统和应用程序,效率极低。
覆盖技术在单一连续分配中的应用
由于内存受限,覆盖技术可以在单一连续分配环境中帮助运行较大的程序:
- 将程序划分为固定区和覆盖区:固定区常驻内存,覆盖区分段存放,按需加载和替换。
- 动态加载:根据程序运行时的调用关系,动态加载需要的覆盖段,从而在内存中最终只存放所需运行的最小部分。
示例
考虑一个简单的程序由三个模块组成:主程序、模块A、模块B。主程序常驻内存,而模块A和模块B根据需要分别被加载到覆盖区执行。
// 主程序驻留在固定区
function main() {
while (true) {
performCriticalOperations();
loadModuleA();
moduleAOperations();
loadModuleB();
moduleBOperations();
}
}
// 模块A被加载到覆盖区
function loadModuleA() {
// 假定将模块A加载到覆盖区
}
// 模块B被加载到覆盖区
function loadModuleB() {
// 假定将模块B加载到覆盖区
}
单一连续分配的历史背景与现代替代方案
单一连续分配在现代操作系统中已不再使用,取而代之的是更为复杂和高效的内存管理技术:
- 分区内存管理(Partitioned Memory Management):内存划分为多个分区,每个分区可以分配给不同的进程。
- 分页管理(Paging):将内存和进程地址空间划分为固定大小的页,按需加载,实现逻辑地址到物理地址的映射。
- 分段管理(Segmentation):将程序划分为不同的段,如代码段、数据段、堆栈段等,各段独立分配地址空间。
- 虚拟内存:结合分页和分段技术,实现内存扩展,提供更高的内存利用率和系统性能。
固定分区分配
固定分区分配将用户内存空间划分为若干个固定大小的分区,每个分区只能装入一个作业。这样可以允许多道作业并发执行。当有空闲分区时,从外存的后备作业队列中选择一个适当大小的作业装入该分区。当作业结束时,从后备队列中找出另一作业调入该分区。
划分分区的方式
固定分区分配有两种划分分区大小的方式:
1. 分区大小相等
这种方式将内存划分为若干个大小相等的分区,适用于控制多个相同对象的场合,但缺乏灵活性。
特点:
- 简单易实现
- 缺乏灵活性,可能导致内存浪费
2. 分区大小不等
这种方式将内存划分为多个较小的分区、适量的中等分区和少量的大分区,根据程序的大小为之分配适当的分区。
特点:
- 提高了灵活性,可以更好地适应不同大小的作业
- 复杂度较高,需要更复杂的管理和调度
内存分配过程
固定分区分配的内存分配过程包括以下几个步骤:
- 分区说明表:建立一张分区说明表,其中每个表项包括分区的起始地址、大小和状态(是否已分配)。
- 检索分区:当有用户进程需要装入时,检索分区说明表以找到合适的分区。
- 分配分区:将找到的分区状态置为"已分配",并将作业装入该分区。
- 拒绝分配:如果未找到合适的分区,则拒绝为该用户进程分配内存。
示例:
typedef struct {
int start_address;
int size;
int is_allocated;
} Partition;
Partition partition_table[] = {
{0x0000, 1024, 0},
{0x0400, 2048, 0},
{0x0C00, 4096, 0},
// 其他分区
};
int allocate_partition(int job_size) {
for (int i = 0; i < sizeof(partition_table) / sizeof(partition_table[0]); i++) {
if (!partition_table[i].is_allocated && partition_table[i].size >= job_size) {
partition_table[i].is_allocated = 1;
return partition_table[i].start_address;
}
}
return -1; // 没有合适的分区
}
优点和缺点
优点
- 多道程序系统:可以用于多道程序系统,允许多个作业并发执行。
- 无外部碎片:由于分区是固定大小的,不会产生外部碎片。
缺点
- 内部碎片:由于分区是固定大小的,可能会产生内部碎片,即分区内未使用的内存空间。
- 低存储空间利用率:由于内部碎片和固定分区大小的限制,存储空间利用率较低。
- 缺乏灵活性:不能实现多进程共享一个主存区,分区大小固定,缺乏灵活性。
应用场景
虽然固定分区分配在现代通用操作系统中很少使用,但在一些特定场景中仍然发挥着作用,例如:
- 嵌入式系统:控制多个相同对象的系统,要求简单且可靠的内存管理。
- 实时系统:需要确定的内存分配和调度,保证系统的实时性和稳定性。
综合示例
以下是一个综合示例,展示了固定分区分配的完整过程:
#include <stdio.h>
typedef struct {
int start_address;
int size;
int is_allocated;
} Partition;
Partition partition_table[] = {
{0x0000, 1024, 0},
{0x0400, 2048, 0},
{0x0C00, 4096, 0},
// 其他分区
};
int allocate_partition(int job_size) {
for (int i = 0; i < sizeof(partition_table) / sizeof(partition_table[0]); i++) {
if (!partition_table[i].is_allocated && partition_table[i].size >= job_size) {
partition_table[i].is_allocated = 1;
return partition_table[i].start_address;
}
}
return -1; // 没有合适的分区
}
void deallocate_partition(int start_address) {
for (int i = 0; i < sizeof(partition_table) / sizeof(partition_table[0]); i++) {
if (partition_table[i].start_address == start_address) {
partition_table[i].is_allocated = 0;
return;
}
}
}
int main() {
int job_size = 1500;
int address = allocate_partition(job_size);
if (address != -1) {
printf("Job allocated at address: 0x%X\n", address);
// 作业执行
// ...
// 作业结束,释放分区
deallocate_partition(address);
} else {
printf("No suitable partition found for job size: %d\n", job_size);
}
return 0;
}
动态分区分配
动态分区分配(Dynamic Partition Allocation),又称为可变分区分配,是一种内存管理技术,旨在解决固定分区方法中的内存浪费问题。通过动态划分内存分区,根据进程的需求适时调整分区大小,有效提高内存利用率。
动态分区分配的基本思想
动态分区分配不预先划分内存,而是在进程装入内存时,根据进程的大小动态建立分区,使分区的大小正好适合进程的需要。系统中的分区大小和数量是可变的,可以随时根据进程的需求进行调整。
动态分区分配的优点
- 高内存利用率:分区大小与进程需求相匹配,避免了固定分区方法中的内存浪费问题。
- 灵活性:系统可以根据当前运行的进程数和大小动态调整分区,提高适应性。
动态分区分配的缺点
- 外部碎片:随着时间的推移,内存中会产生越来越多的碎片,这些小的内存块称为外部碎片,导致内存利用率下降。
- 需要紧凑技术(Compaction):为了提高内存利用率,需要定期进行内存紧凑,将分散的空闲内存块合并成大的连续内存块,以便分配给大进程。
内存分配策略
在动态分区分配中,存在几种常见的内存分配策略,用以决定如何选择适合的空闲分区来满足进程需求:
-
首适应(First-Fit):从头开始扫描空闲分区链表,选择第一个足够大的空闲分区。
- 优点:实现简单,查找速度快。
- 缺点:可能会在内存的低地址部分留下许多小碎片。
-
最佳适应(Best-Fit):扫描所有空闲分区,选择能够满足要求且大小最接近的空闲分区。
- 优点:减少剩余空闲分区的大小,理论上减少碎片。
- 缺点:查找过程较慢,可能会留下更多难以利用的小碎片。
-
最坏适应(Worst-Fit):扫描所有空闲分区,选择最大的空闲分区。
- 优点:避免产生非常小的碎片,增加大块分区的使用。
- 缺点:可能留下大量中等大小的碎片,利用率相对较低。
内存紧凑技术(Compaction)
为了提高内存利用率,动态分区分配需要定期进行内存紧凑,将分散的空闲内存块合并成大的连续内存块。紧凑技术的步骤包括:
- 暂停进程:暂停所有当前运行的进程,保存其当前状态。
- 移动进程:将所有进程移动到内存的一个连续区域,并调整其地址引用。
- 更新分区表:更新内存分区表,以反映新的内存布局。
- 重新启动进程:重新启动被暂停的进程。
示例伪代码:
function compactMemory() {
pauseAllProcesses();
moveProcessesToContiguousArea();
updatePartitionTable();
resumeAllProcesses();
}
function pauseAllProcesses() {
for each process in processList {
process.pause();
saveState(process);
}
}
function moveProcessesToContiguousArea() {
address = startOfMemory;
for each process in processList {
if (process.isRunning()) {
moveProcess(process, address);
address += process.size();
}
}
}
function updatePartitionTable() {
// Update partition table with new address mappings
}
function resumeAllProcesses() {
for each process in processList {
process.resume();
}
}
function moveProcess(process, newAddress) {
// Move process data to new address
process.setAddress(newAddress);
}
现代替代方案
虽然动态分区分配解决了静态分区方法的内存浪费问题,但由于外部碎片和内存紧凑的复杂性,现代操作系统通常使用以下方法:
- 分页(Paging):将内存和进程地址空间划分为固定大小的页和页框,按需加载页面,避免碎片化。
- 分段(Segmentation):将进程地址空间划分为不同的段,如代码段、数据段、堆栈段等,各段独立分配内存空间。
- 虚拟内存:结合分页和分段技术,利用磁盘作为扩展内存,实现按需调页和分段,提高内存利用率和系统灵活性。
动态重定位分区分配
在动态重定位分区分配中,每个进程都有一个基地址寄存器(Base Register),用于存储进程在内存中的起始地址。当进程执行时,基地址寄存器的内容被加载到内存地址寄存器中,从而实现了动态的重定位。
动态重定位的工作原理
基地址寄存器
基地址寄存器存储进程在内存中的起始地址。当进程需要访问内存时,实际的内存地址是通过将逻辑地址(进程使用的地址)与基地址寄存器的内容相加得到的。
地址转换
地址转换是在进程访问内存时进行的,具体步骤如下:
- 逻辑地址:进程生成一个逻辑地址。
- 基地址寄存器:基地址寄存器的内容是进程在内存中的起始地址。
- 实际地址:通过将逻辑地址与基地址寄存器的内容相加,得到实际的内存地址。
示例:
int logical_address = 0x0040; // 进程生成的逻辑地址
int base_address = 0x1000; // 基地址寄存器的内容
int physical_address = base_address + logical_address; // 实际内存地址
动态重定位的优点和缺点
优点
- 提高内存利用率:允许进程在内存中的位置动态变化,可以更有效地利用内存空间。
- 灵活性:可以在运行时调整进程的内存位置,适应不同的内存需求。
缺点
- 地址转换开销:每次内存访问都需要进行地址转换,增加了系统的开销。
- 硬件支持:需要额外的硬件支持,如基地址寄存器和地址转换逻辑。
动态重定位的实现
动态重定位的实现需要操作系统和硬件的支持。以下是一个简单的实现示例,展示了如何使用基地址寄存器进行地址转换:
#include <stdio.h>
typedef struct {
int base_address;
int limit; // 进程的内存大小限制
} ProcessControlBlock;
int translate_address(ProcessControlBlock *pcb, int logical_address) {
if (logical_address >= 0 && logical_address < pcb->limit) {
return pcb->base_address + logical_address;
} else {
// 地址越界
printf("Address out of bounds\n");
return -1;
}
}
int main() {
ProcessControlBlock pcb;
pcb.base_address = 0x1000; // 基地址
pcb.limit = 0x200; // 进程的内存大小限制
int logical_address = 0x0040;
int physical_address = translate_address(&pcb, logical_address);
if (physical_address != -1) {
printf("Logical address: 0x%X\n", logical_address);
printf("Physical address: 0x%X\n", physical_address);
}
return 0;
}
动态重定位的应用
动态重定位分区分配在现代操作系统中有广泛的应用,特别是在需要高内存利用率和灵活性的场景中。例如:
- 多任务操作系统:允许多个进程同时运行,并动态调整它们在内存中的位置。
- 虚拟内存系统:结合分页或分段技术,实现更高级的内存管理。
结语
连续分配存储管理方式是早期操作系统中常用的内存管理技术,它简单高效,但只能应用于单用户、单任务的操作系统。随着技术的发展,分页、分段和段页式存储管理方式逐渐取代连续分配,提供了更好的内存利用率和灵活性。了解连续分配存储管理方式,有助于我们理解操作系统的历史演进和内存管理技术的发展。