69天探索操作系统-第17天:连续内存分配

244 阅读7分钟

pro8.avif

1.介绍

连续内存分配是一种基本的内存管理技术,其中每个进程都被分配到一个连续的内存块。这种方法虽然简单,但在现代操作系统中既带来了优势也面临着挑战。其简单性使得它易于实现和管理,特别是在资源有限的系统中。然而,随着内存请求变得动态且大小各异,连续分配的局限性变得越来越明显。

image.png

连续内存分配利用线性地址方案,快速访问内存。由于分配给进程的整个内存块是连续的,访问分配内存的不同部分非常简便和高效。这对于需要可预测的内存访问模式的应用尤其有利。然而,这种简单性也带来了潜在的碎片化和处理不同内存请求的困难。

2.内存分配策略

本节探讨了分配连续内存块的不同策略,包括首次适应、最佳适应和最坏适应。这些策略决定了操作系统如何选择一块可用的内存来满足进程的内存需求。选择哪种策略会影响内存利用效率和碎片化的程度。

提供的C代码实现了这些策略,展示了如何将其集成到内存管理系统中。该代码提供了初始化内存管理器、根据所选策略分配内存和管理空闲和已分配内存区块的函数。每种策略都有其速度和内存利用方面的权衡。

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

#define MAX_MEMORY_SIZE 1024
#define MIN_PARTITION_SIZE 64

typedef struct MemoryBlock {
    size_t size;
    size_t start_address;
    bool is_allocated;
    struct MemoryBlock* next;
} MemoryBlock;

typedef struct {
    MemoryBlock* head;
    size_t total_size;
    size_t free_size;
    int allocation_strategy; // 1: First Fit, 2: Best Fit, 3: Worst Fit
} MemoryManager;

MemoryManager* initMemoryManager(size_t size, int strategy) {
    MemoryManager* manager = (MemoryManager*)malloc(sizeof(MemoryManager));
    manager->total_size = size;
    manager->free_size = size;
    manager->allocation_strategy = strategy;
    
    // Create initial free block
    manager->head = (MemoryBlock*)malloc(sizeof(MemoryBlock));
    manager->head->size = size;
    manager->head->start_address = 0;
    manager->head->is_allocated = false;
    manager->head->next = NULL;
    
    return manager;
}

// First Fit Algorithm
MemoryBlock* firstFit(MemoryManager* manager, size_t size) {
    MemoryBlock* current = manager->head;
    
    while(current != NULL) {
        if(!current->is_allocated && current->size >= size) {
            return current;
        }
        current = current->next;
    }
    return NULL;
}

// Best Fit Algorithm
MemoryBlock* bestFit(MemoryManager* manager, size_t size) {
    MemoryBlock* current = manager->head;
    MemoryBlock* best_block = NULL;
    size_t smallest_difference = manager->total_size;
    
    while(current != NULL) {
        if(!current->is_allocated && current->size >= size) {
            size_t difference = current->size - size;
            if(difference < smallest_difference) {
                smallest_difference = difference;
                best_block = current;
            }
        }
        current = current->next;
    }
    return best_block;
}

// Worst Fit Algorithm
MemoryBlock* worstFit(MemoryManager* manager, size_t size) {
    MemoryBlock* current = manager->head;
    MemoryBlock* worst_block = NULL;
    size_t largest_difference = 0;
    
    while(current != NULL) {
        if(!current->is_allocated && current->size >= size) {
            size_t difference = current->size - size;
            if(difference > largest_difference) {
                largest_difference = difference;
                worst_block = current;
            }
        }
        current = current->next;
    }
    return worst_block;
}

void* allocateMemory(MemoryManager* manager, size_t size) {
    if(size < MIN_PARTITION_SIZE || size > manager->free_size) {
        return NULL;
    }
    
    MemoryBlock* selected_block = NULL;
    
    switch(manager->allocation_strategy) {
        case 1:
            selected_block = firstFit(manager, size);
            break;
        case 2:
            selected_block = bestFit(manager, size);
            break;
        case 3:
            selected_block = worstFit(manager, size);
            break;
        default:
            return NULL;
    }
    
    if(selected_block == NULL) {
        return NULL;
    }
    
    if(selected_block->size > size + MIN_PARTITION_SIZE) {
        MemoryBlock* new_block = (MemoryBlock*)malloc(sizeof(MemoryBlock));
        new_block->size = selected_block->size - size;
        new_block->start_address = selected_block->start_address + size;
        new_block->is_allocated = false;
        new_block->next = selected_block->next;
        
        selected_block->size = size;
        selected_block->next = new_block;
    }
    
    selected_block->is_allocated = true;
    manager->free_size -= selected_block->size;
    
    return (void*)selected_block->start_address;
}

3.内存分区

内存分区是将可用内存划分为较小的区域或分区,以适应多个进程的过程。这可以通过使用固定大小的分区或动态分区来实现,其中分区的大小会根据内存请求进行调整。分区方案在很大程度上会影响内存的使用效率和碎片程度。

image.png

动态分区可以更灵活地分配内存,通过根据进程的具体需求创建分区来实现。这减少了内部碎片,但可能会导致外部碎片,因为自由块在内存中分散开来。有效管理自由块对于动态分区方案至关重要。

4. 内存紧凑

内存紧凑化是一种技术,用于通过重新定位已分配的内存块来解决外部碎片,从而创建一个更大的连续可用空间。他的过程涉及移动内存内容并更新相应的地址,这可能是一个耗时操作。不过,它可以显著提高内存利用率,并允许分配更大的连续块。

当外部碎片化达到一个点,妨碍了新进程的分配时,紧凑化就变得必要了。提供的代码演示了一个基本的紧凑算法,它将相邻的空闲块合并,并将分配的块洗牌,以创建更大的连续空闲区域。这有助于恢复碎片化的内存,并提高内存管理系统的工作效率。

void compactMemory(MemoryManager* manager) {
    if(manager->head == NULL) return;
    
    MemoryBlock* current = manager->head;
    size_t new_address = 0;
    
    while(current != NULL) {
        if(current->is_allocated) {
            // Move block to new address
            current->start_address = new_address;
            new_address += current->size;
        }
        current = current->next;
    }
    
    // Merge free blocks
    current = manager->head;
    while(current != NULL && current->next != NULL) {
        if(!current->is_allocated && !current->next->is_allocated) {
            // Merge blocks
            current->size += current->next->size;
            MemoryBlock* temp = current->next;
            current->next = temp->next;
            free(temp);
        } else {
            current = current->next;
        }
    }
}

5.内存保护

内存保护机制确保进程只能访问分配给它们的内存区域,防止未授权访问并保持系统稳定。这对于防止进程相互干扰和保护关键系统数据至关重要。各种技术,如基址和限制寄存器和访问控制列表,用于强制执行内存保护。

image.png

内存保护通过硬件和软件机制实现,这些机制控制对内存位置的访问。基址和界限寄存器定义进程分配内存的边界,而访问权限则指定在这些边界内允许的操作类型(读取、写入、执行)。这防止进程访问未指定区域以外的内存,提高系统安全性和稳定性。

6.基本实现

代码演示了如何管理具有不同访问权限的内存区域,以及如何在内存访问期间强制这些权限。

ProtectedMemoryManager结构体将内存管理功能与存储每个内存区域的访问权限的保护表集成在一起。这个示例说明了如何在连续分配的原则基础上构建一个更加强大和安全的内存管理系统。

7.性能分析

分析不同内存分配策略的性能对于选择最合适的策略以应对特定工作负载至关重要。分配时间、内存利用率以及碎片等指标提供了每种策略效率的见解。了解这些指标有助于针对特定的应用需求优化内存管理。

image.png

不同的工作负载和内存访问模式受益于不同的分配策略。分析分配时间、内存利用率以及碎片化等性能特征,可以根据系统的特定需求做出明智的决策。

8.最佳实践

  • 根据工作负载选择适当的分配策略: 考虑请求大小分布和频率等因素,选择最有效的策略。
  • 实施定期的内存压缩: 周期性压缩可以减少外部碎片并提高内存利用率。
  • 监控碎片级别: 跟踪碎片有助于发现潜在问题,并在必要时触发压缩。
  • 使用保护机制: 使用基址/限界寄存器和访问控制列表来确保内存安全。
  • 优化块大小: 选择适当的块最小大小可以最小化内部碎片。

9.总结

尽管有局限性,连续内存分配在特定的使用案例中仍然有其相关性。其简洁性和速度使其适合于某些嵌入式系统和需要可预测内存要求的实时应用。然而,由于易于碎片化,它不太适合动态环境。现代系统通常结合连续分配与其他技术(如分页或分段)以实现更有效和灵活的内存管理。

image.png

虽然连续分配提供了简便性和快速访问,但其关于碎片化的局限性使其不太适合现代通用操作系统。然而,其原则在专门系统中仍然具有相关性,并且是理解更复杂内存管理技术的基础。