在这篇文章中,叙述了如何实现 malloc, calloc, realloc 和 free。在构建内存分配器之前,首先需要了解一个进程的内存布局:

Diagram
  • text 段储存了编译为汇编的代码

  • data 段储存了初值不为零的静态数据

  • bss(Block Started by Symbol) 储存了初值为零的静态数据

  • heap 段储存了手动分配的内存。向上增长

  • stack 段储存了程序自动分配的内存,向下增长

就像栈顶指针指向了栈的顶部一样,brk 指针指向了堆区的末尾。在这片文章中,使用 sbrk 系统调用来分配内存。sbrk 有三种调用方法:

更推荐的方式是使用 mmap 分配内存。sbrk 系统调用在 MacOS 上标记为废弃。尽管如此,libc 依然使用 sbrk 用来分配一些比较小的内存。
  • sbrk(0) 用来获取当前 brk 指针的位置

  • sbrk(|x|) 用来将 brk 指针向上移动 |x| 个位置。也就是分配内存。

  • sbrk(-|x|) 用来将 brk 指针向下移动 -|x| 个位置。也就是释放内存。

一个简单的 mem_alloc 如下:

void *mem_alloc(size_t size){
	void *block = sbrk(size);
	if (block == (void*) -1)
		return nullptr;
	return block;
}

mem_alloc 很简单,但是 mem_free 并不是这样,因为:

  1. mem_free 必须知道需要释放的内存的大小

  2. mem_free 只能从堆区末尾释放内存

以下是两个问题的解决方案:

  • 对于第一点。我们建立一个链表,在链表节点中储存内存的大小

  • 对于第二点。我们区分 release memory 和 free memory。当我们调用 mem_free 的时候,我们只是简单地在链表中将这块内存标记为 未使用 。当且仅当释放的内存是链表的最后一个节点时,我们才调用 sbrk 将内存归还给系统

另外,为了线程安全性,我们创建一个全局锁,在分配内存和释放内存时进行加锁。

链表节点的定义如下:

struct alignas(16) header_t {
    size_t    size    = 0;
    bool      is_free = true;
    header_t* next    = nullptr;
};

现在,如果我们分配一个内存,,其这块内存的实际布局为:

Diagram

这样,mem_alloc 如下:

std::mutex global_mem_lock; (1)

header_t* mem_header = nullptr; (2)

header_t* get_free_block(size_t size) {
    auto curr = mem_header;
    while(curr) {
        if(curr->is_free && curr->size >= size) {
            return curr;
        }

        curr = curr->next;
    }

    return nullptr;
}

void* mem_alloc(size_t size) {
    if(!size) return nullptr;

    std::unique_lock<std::mutex> lock(global_mem_lock); (3)

    auto header = get_free_block(size); (4)
    if(header) {
        header->is_free = false;
        return (void*)(header + 1);
    }

    header = (header_t*)sbrk(sizeof(header_t) + size); (5)
    if(header == (void*)-1) {
        return nullptr;
    }

    header->size    = size;
    header->is_free = false;
    header->next    = nullptr;
    if(!mem_header) {
        mem_header = header;
    }

    return (void*)(header + 1);
}
1全局内存锁
2链表头节点
3在进行内存分配动作之前首先进行加锁操作
4首先检查链表中是否有满足条件的空闲节点
5未找到满足条件的空闲节点时才进行实际的内存分配操作

从上面的代码中可以看出,在查找空闲节点的时候使用的是 最先适应算法 ,而且不对空闲节点进行合并,从而简化了实现。

mem_free 代码如下:

void mem_free(void* block) {
    if(!block) return;
    std::unique_lock<std::mutex> lock(global_mem_lock);

    auto  header  = (header_t*)block - 1;
    void* brk_pos = sbrk(0);
    if((char*)block + header->size < brk_pos) {
        header->is_free = 1;
        return;
    }
    // 释放的位置位于 brk 位置,进行实际释放动作
    if(!mem_header->next) {    // 释放的是最后一个节点
        mem_header = nullptr;
    } else {    // 释放的只是尾节点
        auto parent = mem_header;
        while(parent && (parent->next != header)) {
            parent = parent->next;
        }

        parent->next = nullptr;
    }

    sbrk(-sizeof(header_t) - header->size);
}

mem_calloc 和 mem_realloc 也类似:

void* mem_calloc(size_t num, size_t nsize) {
    if(!num || !nsize) return nullptr;

    auto block = mem_alloc(num * nsize);
    bzero(block, num * nsize);
    return block;
}

void* mem_realloc(void* block, size_t size) {
    if(!block) {
        return mem_alloc(size);
    }
    if(!size) {
        mem_free(block);
        return nullptr;
    }

    auto header = (header_t*)block - 1;
    if(header->size >= size) return block;

    auto new_block = mem_alloc(size);
    if(new_block) {
        memcpy(new_block, block, size);
    }

    mem_free(block);
    return new_block;
}
Last moify: 2023-12-27 05:37:04
Build time:2025-07-18 09:41:42
Powered By asphinx