跳转至

02-实现一个内存分配器


项目简介

内存分配器是操作系统和运行时库的核心组件。通过实现一个简化版的malloc/free,你将深入理解: - 堆内存管理的底层机制 - 内存碎片问题及解决方案 - 系统调用(brk/mmap)的使用 - 内存对齐和缓存优化

项目目标: - ✅ 实现基础的malloc/free - ✅ 使用sbrk/mmap系统调用 - ✅ 实现内存碎片管理 - ✅ 支持内存对齐 - ✅ 实现简单的调试功能


基础知识

1. 内存分配器的作用

Text Only
┌─────────────────────────────────────────────────────────────────────┐
│                    内存分配器在系统中的位置                              │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  应用程序                                                            │
│  ┌───────────────────────────────────────────────────────────────┐ │
│  │  void* ptr = malloc(100);                                      │ │
│  │  free(ptr);                                                    │ │
│  └───────────────────────────────────────────────────────────────┘ │
│                        ↓                                            │
│  C标准库(glibc)                                                     │
│  ┌───────────────────────────────────────────────────────────────┐ │
│  │  ptmalloc / jemalloc / tcmalloc                                │ │
│  │  • 内存池管理                                                   │ │
│  │  • 线程缓存                                                     │ │
│  │  • 碎片整理                                                     │ │
│  └───────────────────────────────────────────────────────────────┘ │
│                        ↓                                            │
│  系统调用                                                            │
│  ┌───────────────────────────────────────────────────────────────┐ │
│  │  brk()  - 调整数据段末尾(小内存)                               │ │
│  │  mmap() - 映射内存页(大内存)                                   │ │
│  │  munmap() - 解除内存映射                                        │ │
│  └───────────────────────────────────────────────────────────────┘ │
│                        ↓                                            │
│  操作系统内核                                                         │
│  ┌───────────────────────────────────────────────────────────────┐ │
│  │  虚拟内存管理                                                   │ │
│  │  • 页表管理                                                     │ │
│  │  • 物理内存分配                                                 │ │
│  │  • 页面置换                                                     │ │
│  └───────────────────────────────────────────────────────────────┘ │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

2. 内存分配策略

Text Only
┌─────────────────────────────────────────────────────────────────────┐
│                    内存分配策略对比                                    │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  1. 首次适应(First Fit)                                             │
│  ┌───────────────────────────────────────────────────────────────┐ │
│  │  策略:从头开始搜索,选择第一个足够大的空闲块                      │ │
│  │  优点:简单快速                                                 │ │
│  │  缺点:容易产生碎片                                             │ │
│  │                                                                │ │
│  │  示例:                                                        │ │
│  │  空闲链表:[32B] → [128B] → [64B] → [256B]                      │ │
│  │  申请64B:选择[128B](第一个≥64B的块)                          │ │
│  │                                                                │ │
│  └───────────────────────────────────────────────────────────────┘ │
│                                                                     │
│  2. 最佳适应(Best Fit)                                              │
│  ┌───────────────────────────────────────────────────────────────┐ │
│  │  策略:搜索所有空闲块,选择最小的足够大的块                        │ │
│  │  优点:减少内存浪费                                             │ │
│  │  缺点:搜索耗时,产生大量小碎片                                  │ │
│  │                                                                │ │
│  │  示例:                                                        │ │
│  │  空闲链表:[32B] → [128B] → [64B] → [256B]                      │ │
│  │  申请64B:选择[64B](正好匹配)                                 │ │
│  │                                                                │ │
│  └───────────────────────────────────────────────────────────────┘ │
│                                                                     │
│  3. 最差适应(Worst Fit)                                             │
│  ┌───────────────────────────────────────────────────────────────┐ │
│  │  策略:选择最大的空闲块                                         │ │
│  │  优点:保留大块内存                                             │ │
│  │  缺点:大内存被分割,不适合大对象                                │ │
│  │                                                                │ │
│  │  示例:                                                        │ │
│  │  空闲链表:[32B] → [128B] → [64B] → [256B]                      │ │
│  │  申请64B:选择[256B](最大的块)                                │ │
│  │                                                                │ │
│  └───────────────────────────────────────────────────────────────┘ │
│                                                                     │
│  4. 分离空闲列表(Segregated Fit) - 现代分配器常用                    │
│  ┌───────────────────────────────────────────────────────────────┐ │
│  │  策略:按大小分类,维护多个空闲链表                               │ │
│  │  优点:分配和释放都很快                                         │ │
│  │  缺点:实现复杂,内存可能浪费                                   │ │
│  │                                                                │ │
│  │  示例:                                                        │ │
│  │  8B链表:[8B] → [8B] → [8B]                                     │ │
│  │  16B链表:[16B] → [16B]                                         │ │
│  │  32B链表:[32B] → [32B] → [32B]                                 │ │
│  │  ...                                                           │ │
│  │  大对象链表:[1KB] → [4KB] → [16KB]                             │ │
│  │                                                                │ │
│  └───────────────────────────────────────────────────────────────┘ │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

3. 内存碎片问题

Text Only
┌─────────────────────────────────────────────────────────────────────┐
│                    内存碎片问题                                        │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  内部碎片(Internal Fragmentation)                                    │
│  ├── 定义:分配给进程的内存大于实际需要的内存                         │
│  ├── 原因:按固定大小分配(如页大小4KB)                              │
│  └── 示例:申请100字节,分配4KB页,浪费4096-100=3996字节              │
│                                                                     │
│  外部碎片(External Fragmentation)                                    │
│  ├── 定义:空闲内存总量足够,但没有连续的大块                         │
│  ├── 原因:频繁的分配和释放,产生大量小块空闲内存                     │
│  └── 示例:                                                        │
│                                                                     │
│     分配前:┌──────────────┬──────────────┬──────────────┐          │
│             │   已分配A    │   已分配B    │   已分配C    │          │
│             │    (64B)     │    (64B)     │    (64B)     │          │
│             └──────────────┴──────────────┴──────────────┘          │
│                                                                     │
│     释放B: ┌──────────────┬──────────────┬──────────────┐          │
│             │   已分配A    │    空闲64B   │   已分配C    │          │
│             │    (64B)     │              │    (64B)     │          │
│             └──────────────┴──────────────┴──────────────┘          │
│                                                                     │
│     释放A: ┌──────────────┬──────────────┬──────────────┐          │
│             │    空闲64B   │    空闲64B   │   已分配C    │          │
│             │              │              │    (64B)     │          │
│             └──────────────┴──────────────┴──────────────┘          │
│                                                                     │
│     问题:总空闲128B,但无法分配100B的连续内存!                      │
│                                                                     │
│  解决方案:                                                          │
│  1. 内存紧缩(Compaction):移动已分配内存,合并空闲块                │
│  2. 伙伴系统(Buddy System):按2的幂次分配,易于合并                 │
│  3.  slab分配器:预分配固定大小的对象池                              │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

第一步:基础malloc实现

内存块结构

C
// mymalloc.h
#ifndef MYMALLOC_H
#define MYMALLOC_H

#include <stddef.h>
#include <stdbool.h>

// 内存块头部结构(元数据)
typedef struct block_header {
    size_t size;           // 块大小(不包括头部)
    bool is_free;          // 是否空闲
    struct block_header* next;  // 下一个块
} block_header_t;

// 对齐要求(8字节对齐)
#define ALIGNMENT 8
#define ALIGN(size) (((size) + (ALIGNMENT - 1)) & ~(ALIGNMENT - 1))

// 块头部大小
#define BLOCK_SIZE ALIGN(sizeof(block_header_t))

// 最小分配大小(避免碎片)
#define MIN_BLOCK_SIZE 16

// 函数声明
void* my_malloc(size_t size);
void my_free(void* ptr);
void* my_calloc(size_t num, size_t size);
void* my_realloc(void* ptr, size_t size);

// 调试函数
void print_heap_status(void);
size_t get_total_allocated(void);
size_t get_total_free(void);

#endif

基础实现

C
// mymalloc.c
#include "mymalloc.h"
#include <unistd.h>
#include <string.h>
#include <stdio.h>

// 堆起始位置(静态变量)
static block_header_t* heap_start = NULL;
static block_header_t* free_list = NULL;

// 获取块的数据指针
static void* get_block_payload(block_header_t* block) {
    return (char*)block + BLOCK_SIZE;
}

// 从数据指针获取块头部
static block_header_t* get_block_header(void* ptr) {
    return (block_header_t*)((char*)ptr - BLOCK_SIZE);
}

// 扩展堆空间
static block_header_t* extend_heap(size_t size) {
    // 使用sbrk扩展堆
    void* ptr = sbrk(0);  // 获取当前break位置

    if (sbrk(size + BLOCK_SIZE) == (void*)-1) {
        return NULL;  // sbrk失败
    }

    block_header_t* block = (block_header_t*)ptr;
    block->size = size;
    block->is_free = false;
    block->next = NULL;

    return block;
}

// 在空闲链表中查找合适的块(首次适应)
static block_header_t* find_free_block(size_t size) {
    block_header_t* current = free_list;

    while (current != NULL) {
        if (current->is_free && current->size >= size) {
            return current;
        }
        current = current->next;
    }

    return NULL;
}

// 分割块(如果块太大)
static void split_block(block_header_t* block, size_t size) {
    // 只有当剩余空间足够大时才分割
    if (block->size >= size + BLOCK_SIZE + MIN_BLOCK_SIZE) {
        // 创建新块
        block_header_t* new_block = (block_header_t*)((char*)block + BLOCK_SIZE + size);
        new_block->size = block->size - size - BLOCK_SIZE;
        new_block->is_free = true;
        new_block->next = block->next;

        // 更新原块
        block->size = size;
        block->next = new_block;
    }
}

// 合并相邻的空闲块
static void coalesce_blocks(void) {
    block_header_t* current = heap_start;

    while (current != NULL && current->next != NULL) {
        if (current->is_free && current->next->is_free) {
            // 合并两个块
            current->size += BLOCK_SIZE + current->next->size;
            current->next = current->next->next;
            // 继续检查当前块(可能还可以继续合并)
        } else {
            current = current->next;
        }
    }
}

// 初始化堆
static void init_heap(void) {
    if (heap_start == NULL) {
        // 初始分配一个较大的块
        heap_start = extend_heap(1024);
        if (heap_start != NULL) {
            heap_start->is_free = true;
            free_list = heap_start;
        }
    }
}

// malloc实现
void* my_malloc(size_t size) {
    if (size == 0) {
        return NULL;
    }

    // 对齐大小
    size = ALIGN(size);

    // 确保最小大小
    if (size < MIN_BLOCK_SIZE) {
        size = MIN_BLOCK_SIZE;
    }

    // 初始化堆(首次调用)
    if (heap_start == NULL) {
        init_heap();
    }

    // 在空闲链表中查找
    block_header_t* block = find_free_block(size);

    if (block != NULL) {
        // 找到合适的块
        block->is_free = false;
        split_block(block, size);
        return get_block_payload(block);
    }

    // 没有找到,扩展堆
    block = extend_heap(size);
    if (block == NULL) {
        return NULL;  // 内存不足
    }

    // 将新块加入链表
    block_header_t* current = heap_start;
    while (current->next != NULL) {
        current = current->next;
    }
    current->next = block;

    return get_block_payload(block);
}

// free实现
void my_free(void* ptr) {
    if (ptr == NULL) {
        return;
    }

    block_header_t* block = get_block_header(ptr);

    // 检查是否是有效的块
    if (block < heap_start) {
        fprintf(stderr, "Error: Invalid free (before heap start)\n");
        return;
    }

    // 标记为空闲
    block->is_free = true;

    // 尝试合并相邻的空闲块
    coalesce_blocks();
}

// calloc实现
void* my_calloc(size_t num, size_t size) {
    size_t total_size = num * size;
    void* ptr = my_malloc(total_size);

    if (ptr != NULL) {
        memset(ptr, 0, total_size);
    }

    return ptr;
}

// realloc实现
void* my_realloc(void* ptr, size_t size) {
    if (ptr == NULL) {
        return my_malloc(size);
    }

    if (size == 0) {
        my_free(ptr);
        return NULL;
    }

    block_header_t* block = get_block_header(ptr);

    // 如果当前块足够大,直接返回
    if (block->size >= ALIGN(size)) {
        return ptr;
    }

    // 分配新块
    void* new_ptr = my_malloc(size);
    if (new_ptr == NULL) {
        return NULL;
    }

    // 复制数据
    memcpy(new_ptr, ptr, block->size);

    // 释放旧块
    my_free(ptr);

    return new_ptr;
}

// 打印堆状态(调试用)
void print_heap_status(void) {
    printf("\n=== Heap Status ===\n");

    block_header_t* current = heap_start;
    int block_count = 0;
    size_t total_allocated = 0;
    size_t total_free = 0;

    while (current != NULL) {
        printf("Block %d: addr=%p, size=%zu, %s\n",
               block_count,
               (void*)current,
               current->size,
               current->is_free ? "FREE" : "USED");

        if (current->is_free) {
            total_free += current->size;
        } else {
            total_allocated += current->size;
        }

        current = current->next;
        block_count++;
    }

    printf("\nTotal blocks: %d\n", block_count);
    printf("Total allocated: %zu bytes\n", total_allocated);
    printf("Total free: %zu bytes\n", total_free);
    printf("===================\n\n");
}

// 获取已分配内存总量
size_t get_total_allocated(void) {
    size_t total = 0;
    block_header_t* current = heap_start;

    while (current != NULL) {
        if (!current->is_free) {
            total += current->size;
        }
        current = current->next;
    }

    return total;
}

// 获取空闲内存总量
size_t get_total_free(void) {
    size_t total = 0;
    block_header_t* current = heap_start;

    while (current != NULL) {
        if (current->is_free) {
            total += current->size;
        }
        current = current->next;
    }

    return total;
}

第二步:测试程序

C
// test_mymalloc.c
#include "mymalloc.h"
#include <stdio.h>
#include <string.h>

int main() {
    printf("=== MyMalloc Test ===\n\n");

    // 测试1:基本分配和释放
    printf("Test 1: Basic allocation and free\n");
    void* p1 = my_malloc(100);
    printf("Allocated 100 bytes at %p\n", p1);
    print_heap_status();

    my_free(p1);
    printf("Freed p1\n");
    print_heap_status();

    // 测试2:多次分配
    printf("\nTest 2: Multiple allocations\n");
    void* p2 = my_malloc(50);
    void* p3 = my_malloc(100);
    void* p4 = my_malloc(200);
    printf("Allocated 50, 100, 200 bytes\n");
    print_heap_status();

    // 测试3:中间释放(产生碎片)
    printf("\nTest 3: Middle free (fragmentation)\n");
    my_free(p3);
    printf("Freed p3 (100 bytes)\n");
    print_heap_status();

    // 测试4:重用空闲块
    printf("\nTest 4: Reuse free block\n");
    void* p5 = my_malloc(80);
    printf("Allocated 80 bytes (should reuse p3's block)\n");
    print_heap_status();

    // 测试5:calloc
    printf("\nTest 5: Calloc\n");
    int* arr = my_calloc(10, sizeof(int));
    printf("Allocated array of 10 ints\n");
    printf("arr[0]=%d, arr[5]=%d (should be 0)\n", arr[0], arr[5]);
    print_heap_status();

    // 测试6:realloc
    printf("\nTest 6: Realloc\n");
    arr = my_realloc(arr, 20 * sizeof(int));
    printf("Reallocated array to 20 ints\n");
    print_heap_status();

    // 清理
    my_free(p2);
    my_free(p4);
    my_free(p5);
    my_free(arr);

    printf("\nFinal status:\n");
    print_heap_status();

    printf("\n=== All tests passed! ===\n");
    return 0;
}

第三步:编译运行

Bash
# 编译
gcc -o test_mymalloc mymalloc.c test_mymalloc.c

# 运行
./test_mymalloc

# 使用valgrind检测内存泄漏(可选)
valgrind --leak-check=full ./test_mymalloc

第四步:进阶优化

1. 使用mmap分配大块内存

C
#include <sys/mman.h>

#define MMAP_THRESHOLD (128 * 1024)  // 128KB以上使用mmap

static void* allocate_large(size_t size) {
    // 使用mmap分配大块内存
    void* ptr = mmap(NULL, size + BLOCK_SIZE,
                     PROT_READ | PROT_WRITE,
                     MAP_PRIVATE | MAP_ANONYMOUS,
                     -1, 0);

    if (ptr == MAP_FAILED) {
        return NULL;
    }

    block_header_t* block = (block_header_t*)ptr;
    block->size = size;
    block->is_free = false;
    block->next = NULL;

    // 标记为mmap分配的块(需要特殊处理)
    // ...

    return get_block_payload(block);
}

2. 线程安全(加锁)

C
#include <pthread.h>  // 引入头文件

static pthread_mutex_t malloc_lock = PTHREAD_MUTEX_INITIALIZER;

void* my_malloc_thread_safe(size_t size) {
    pthread_mutex_lock(&malloc_lock);
    void* ptr = my_malloc(size);
    pthread_mutex_unlock(&malloc_lock);
    return ptr;
}

void my_free_thread_safe(void* ptr) {
    pthread_mutex_lock(&malloc_lock);
    my_free(ptr);
    pthread_mutex_unlock(&malloc_lock);
}

3. 内存调试功能

C
// 在DEBUG模式下记录分配信息
#ifdef DEBUG
#define MAX_ALLOCATIONS 1000

typedef struct alloc_info {  // struct结构体:自定义复合数据类型
    void* ptr;  // 指针:存储变量的内存地址
    size_t size;
    const char* file;
    int line;
} alloc_info_t;

static alloc_info_t allocations[MAX_ALLOCATIONS];
static int alloc_count = 0;

void* my_malloc_debug(size_t size, const char* file, int line) {
    void* ptr = my_malloc(size);
    if (ptr != NULL && alloc_count < MAX_ALLOCATIONS) {
        allocations[alloc_count].ptr = ptr;
        allocations[alloc_count].size = size;
        allocations[alloc_count].file = file;
        allocations[alloc_count].line = line;
        alloc_count++;
    }
    return ptr;
}

#define my_malloc(size) my_malloc_debug(size, __FILE__, __LINE__)

// 检查内存泄漏
void check_leaks(void) {
    printf("\n=== Memory Leak Check ===\n");
    int leaks = 0;
    for (int i = 0; i < alloc_count; i++) {
        // 检查是否已释放
        block_header_t* block = get_block_header(allocations[i].ptr);
        if (!block->is_free) {
            printf("Leak: %p (%zu bytes) at %s:%d\n",
                   allocations[i].ptr,
                   allocations[i].size,
                   allocations[i].file,
                   allocations[i].line);
            leaks++;
        }
    }
    if (leaks == 0) {
        printf("No memory leaks detected!\n");
    }
    printf("========================\n");
}
#endif

💡 核心要点总结

内存分配器关键设计

  1. 元数据管理:每个内存块前面有头部记录大小和状态
  2. 对齐要求:确保返回的地址满足对齐要求(通常是8或16字节)
  3. 空闲链表:维护空闲块链表,快速查找可用内存
  4. 分配策略:首次适应、最佳适应、分离空闲列表等
  5. 碎片管理:分割大块、合并相邻空闲块

性能优化技巧

优化 效果 实现复杂度
分离空闲列表 分配O(1)
线程缓存 减少锁竞争
mmap大块 减少碎片
延迟合并 减少合并开销
内存对齐 提高缓存命中率

常见问题

Q1:为什么需要内存对齐?

A: - 硬件要求:某些架构只能访问对齐的地址 - 性能优化:未对齐访问可能需要多次内存访问 - 缓存行对齐:避免一个数据跨两个缓存行

Q2:如何检测内存泄漏?

A: - 记录每次分配的位置(文件、行号) - 程序退出时检查哪些内存未释放 - 使用工具:valgrind、AddressSanitizer

Q3:分配器为什么使用sbrk和mmap?

A: - sbrk:适合小内存分配,连续性好,但无法单独释放 - mmap:适合大内存分配,可以独立释放,但开销较大


📚 扩展阅读

  1. 《深入理解计算机系统》 - 第9章:虚拟内存
  2. ptmalloc源码:GNU C库的内存分配器
  3. jemalloc:Facebook开发的高性能分配器
  4. tcmalloc:Google开发的线程缓存分配器

🎯 下一步

继续完成其他实战项目,如: - 编写汇编程序实现字符串操作 - CUDA加速矩阵运算 - 实现一个简单的Shell