操作系统笔记(5)-空闲空间管理

简介

  • 在堆上管理空闲空间的数据结构通常称为 空闲列表(free list),该结构包含了管理内存区域中所有空闲块的引用

  • 外部碎片 是指空闲列表中出现了很多小的内存块,导致无法分配一块较大的内存

  • 内部碎片 是指分配了多余的内存,浪费发生在已分配单元的内部

底层机制

分割与合并

  • 假设有一段 0~30 字节的地址空间,其中 10~20 已经被使用了,那么剩余的就是空闲空间,用空闲列表表示如下:

空闲列表

  • 如果要申请一块大小为 5 字节的空间,可以将第一个节点 分割

  • 如果要释放原来的空间,也可以与空闲列表中的结点 合并 成一个更大的结点,以满足较大内存的需求

追踪已分配空间的大小

  • 在 C 语言中,free() 函数可以释放空间,但是它只接受一个指针为参数,所以要知道待释放的空间的大小,需要在头块中保存一点额外的信息,这个头块通常就在返回的内存块之前

  • 该头块至少包含所分配空间的大小,也可能包含一些额外的指针来加速空间释放,包含一个幻数来提供完整性检查,以及其它信息

    typedef struct header_t {
        int size;
        int magic;
    } header_t;
  • 用户调用 free(ptr) 时,库会通过简单的指针运算得到头块的位置

    void free(void *ptr) {
        header_t *hptr = (void*)ptr - sizeof(header_t);
    }

    示意图

基本策略

  • 为了更好地管理空闲空闲,出现了一些基本策略:

    1. 最优匹配(best fit) :在空闲列表中找到一块最接近请求的块的大小的结点并分配

    2. 最差匹配(worst fit) : 与最优匹配相反,它尝试找到最大的空闲块,分割并满足用户需求后,将剩余的块(很大)加入空闲列表

    3. 首次匹配(first fit) : 找到第一个足够大的块,将请求的空间返回给用户

    4. 下次匹配(next fit) :多维护一个指针,指向上一次场照的结束位置

C 语言模拟基本策略

  • 首先,我们的策略是支持空闲块合并的,并且为了方便合并,空闲列表结点按地址从小到大排列

  • 其次,我们考虑了头部结点的大小,因此在申请内存块时需要加上头部结点的大小

  • 最后,对于分配失败的操作,返回 NULL ,否则返回内存块的 指针 ,如上图的 ptr 所示

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

typedef struct node_t {
    int addr;               // 空闲块的起始地址
    int size;               // 分配的地址空间大小
    struct node_t *next;    // 指向下一个结点的指针    
} node_t;

typedef struct freelist {
    node_t *head;
} freelist;

// 初始化空闲列表, 提供地址块的起始地址和大小
freelist* initList(int addr_, int size_) {
    freelist* flist = (freelist*)malloc(sizeof(freelist));
    flist->head = (node_t*)malloc(sizeof(node_t));      // 虚拟头结点, 便于操作实现

    node_t* node = (node_t*)malloc(sizeof(node_t));
    node->addr = addr_;
    node->size = size_;
    node->next = NULL;
    
    flist->head->next = node;

    return flist;
}


node_t* best_fit(freelist* flist, int size_) {
    node_t *pre = flist->head, *cur = pre->next;
    node_t* pp = NULL;
    node_t *p = NULL;
    int minsz = INT_MAX;
    while (cur) {
        if (cur->size >= (size_ + sizeof(node_t))) {
            if (cur->size < minsz) {
                minsz = cur->size;
                p = cur;
                pp = pre;
            }
        }
        cur = cur->next;
    }

    if (!p) return NULL;

    node_t* node = (node_t*)malloc(sizeof(node_t));
    node->addr = p->addr + sizeof(node_t);
    node->size = size_;
    node->next = NULL;

    p->addr += size_ + sizeof(node_t);
    p->size -= size_ + sizeof(node_t);

    if (p->size == 0) {
        pp->next = p->next;
        free(p);
    }

    return node;
}


node_t* worst_fit(freelist* flist, int size_) {
    node_t *pre = flist->head, *cur = pre->next;
    node_t* pp = NULL;
    node_t *p = NULL;
    int maxsz = INT_MIN;
    while (cur) {
        if (cur->size >= (size_ + sizeof(node_t))) {
            if (cur->size > maxsz) {
                maxsz = cur->size;
                p = cur;
                pp = pre;
            }
        }
        cur = cur->next;
    }

    if (!p) return NULL;

    node_t* node = (node_t*)malloc(sizeof(node_t));
    node->addr = p->addr + sizeof(node_t);
    node->size = size_;
    node->next = NULL;

    p->addr += size_ + sizeof(node_t);
    p->size -= size_ + sizeof(node_t);

    if (p->size == 0) {
        pp->next = p->next;
        free(p);
    }

    return node;     
}

node_t* first_fit(freelist* flist, int size_) {
    node_t *pre = flist->head, *cur = pre->next;
    node_t* pp = NULL;
    node_t* p = NULL;
    while (cur) {
        if (cur->size >= (size_ + sizeof(node_t))) {
            p = cur;
            pp = pre;
            break;
        }
        cur = cur->next;
    }
    if (!p) return NULL;

    node_t* node = (node_t*)malloc(sizeof(node_t));
    node->addr = p->addr + sizeof(node_t);
    node->size = size_;
    node->next = NULL;

    p->addr += size_ + sizeof(node_t);
    p->size -= size_ + sizeof(node_t);

    if (p->size == 0) {
        pp->next = p->next;
        free(p);
    }

    return node;
}

// 申请空间, 提供申请大小和分配策略
// 0 : 最优匹配
// 1 : 最差匹配
// 2 : 首次匹配
node_t* alloc(freelist* flist, int size_, int policy) {
    if (policy == 0) {
        return best_fit(flist, size_);
    }
    else if (policy == 1) {
        return worst_fit(flist, size_);
    }
    return first_fit(flist, size_);
}


// 回收内存
void recovery(freelist* flist, node_t* node) {
    if (!node) return;
    node_t *pre = flist->head;
    node_t *cur = pre->next;

    node->addr -= sizeof(node_t);
    node->size += sizeof(node_t);

    while (cur) {
        if (cur->addr > node->addr) {
            // 检查是否可以合并
            if (cur->addr == node->addr + node->size) { // 可以合并
                cur->addr = node->addr;
                cur->size += node->size;
                free(node);
                // 检查能不能和 pre 合并
                if (pre != flist->head && pre->addr + pre->size == cur->addr) {
                    pre->size += cur->size;
                    pre->next = cur->next;
                    free(cur);
                }
                return;
            }
            else if (pre != flist->head && pre->addr + pre->size == node->addr) {
                pre->size += node->size;
                free(node);
                return;
            }
            else {
                node->next = cur;
                pre->next = node;
                return;
            }
        }
        pre = pre->next;
        cur = cur->next;
    }

    pre->next = node;
}

// 打印空闲块信息
void printNode(node_t* node) {
    if (!node) {
        puts("alloc error!");
        return;
    }
    printf("( addr : %d  sz : %d )\n", node->addr, node->size);
}

// 打印空闲列表中的空闲块
void printList(freelist* flist) {
    node_t* cur = flist->head->next;

    while (cur) {
        printf("[ addr: %d  sz: %d ]  ", cur->addr, cur->size);
        cur = cur->next;
    }

    puts("");
}

// 释放空闲列表
void freeList(freelist* flist) {
    node_t *pre = flist->head, *cur = pre->next;

    while (cur) {
        free(pre);
        pre = cur;
        cur = cur->next;
    }
    free(pre);
    free(flist);
}

测试

int main(int argc, char* argv[]) {

    freelist* flist = initList(1000, 300);      // 初始化一块地址空间, 从 1000 开始, 大小为 300 B

    printf("头块大小为: %ld \n", sizeof(node_t));

    // 最优匹配测试
    node_t* b1 = alloc(flist, 20, 0);   // addr : 1016 
    printNode(b1);
    node_t* b2 = alloc(flist, 30, 0);   // addr : 1052
    printNode(b2);
    node_t* b3 = alloc(flist, 300, 0);  // error
    printNode(b3);

    printList(flist);                   // addr : 1082 sz : 218 (300 - (20 + 16) - (30 + 16))

    recovery(flist, b1);                
    printList(flist);                   // (addr : 1000  sz : 36) + (addr : 1082  sz : 218)

    node_t* b4 = alloc(flist, 4, 0);    // addr : 1016
    printNode(b4);
    printList(flist);                   // (addr : 1020  sz : 16) + (addr : 1082  sz : 218)

    recovery(flist, b2);
    recovery(flist, b3);
    printList(flist);                   // (addr : 1020  sz : 280)

    recovery(flist, b4);

    freeList(flist);


    // 最差匹配测试
    flist = initList(2000, 500);

    node_t* w1 = alloc(flist, 200, 1);  // addr : 2016
    printNode(w1);
    node_t* w2 = alloc(flist, 100, 1);  // addr : 2232
    printNode(w2);

    recovery(flist, w1);                
    printList(flist);                   // (addr : 2000  sz : 216) + (addr : 2332  sz : 168)

    node_t* w3 = alloc(flist, 50, 1);   // (addr : 2016  sz : 50)
    printNode(w3);

    printList(flist);                   // (addr : 2066  sz : 150) + (addr : 2332  sz : 168)

    recovery(flist, w2);
    recovery(flist, w3);
    freeList(flist);

    // 首次匹配测试
    flist = initList(3000, 1000);

    node_t* f1 = alloc(flist, 100, 2);      // addr : 3016
    printNode(f1);
    node_t* f2 = alloc(flist, 200, 2);
    recovery(flist, f1);

    printList(flist);                       // (addr : 3000  sz : 116) + (addr : 3332  sz : 668)

    node_t* f3 = alloc(flist, 50, 2);       // addr : 3016
    printNode(f3);
    node_t* f4 = alloc(flist, 100, 2);      // addr : 3348
    printNode(f4);

    recovery(flist, f3);

    printList(flist);                       // (addr : 3000  sz : 116) + (addr : 3348  sz : 552)

    recovery(flist, f2);
    recovery(flist, f4);
    freeList(flist);

    return 0;
}

其它方式

分离空闲列表

  • 如果某个应用程序经常申请一种(或几种)大小的内存空间,那就用一个独立的列表,值管理这样大小的对象,其它大小的请求都交给更通用的内存分配程序

伙伴系统

  • 二分伙伴分配程序(binary buddy allocator) 让合并变得简单,空闲空间首先从概念上被看出大小为 $2^N$ 的大空间

  • 当有一个内存分配请求时,空闲空间被递归地一分为二,直到刚好可以满足请求的大小(再分就不行了)

  • 当释放空间后,会找到相邻的 伙伴 合并