malloc函数底层内存分配如何实现?

99ANYc3cd6
预计阅读时长 21 分钟
位置: 首页 C语言 正文

malloc(memory allocate)是 C 标准库中用于动态分配内存的函数,当你调用 malloc(size) 时,它会向操作系统请求一块连续的、指定大小的内存空间,并返回一个指向这块内存起始地址的指针。

c语言malloc函数实现
(图片来源网络,侵删)

理解 malloc 的实现,需要从两个层面来看:

  1. 应用程序层面:我们如何使用 malloc,以及它做了什么。
  2. 操作系统/标准库内部层面malloc 是如何管理内存、找到可用内存并返回给我们的。

应用程序如何使用 malloc

这是最直接的层面。malloc 的函数原型在 stdlib.h 中:

void* malloc(size_t size);
  • size_t size: 你需要分配的内存大小,以字节为单位。
  • void*: 返回一个通用指针,你可以将其转换为任何类型的指针(如 int*, char*, struct* 等)。

使用示例:

#include <stdio.h>
#include <stdlib.h> // 必须包含此头文件
int main() {
    int n = 10;
    // 1. 声明一个整型指针
    int *numbers;
    // 2. 调用 malloc 分配 10 个 int 大小的内存
    // sizeof(int) 获取一个 int 所占的字节数
    // malloc 返回 void*,我们将其转换为 int*
    numbers = (int*) malloc(n * sizeof(int));
    // 3. **至关重要的检查**:malloc 可能会失败(例如内存不足),此时返回 NULL
    if (numbers == NULL) {
        printf("内存分配失败!\n");
        return 1; // 返回错误码
    }
    // 4. 使用分配好的内存
    for (int i = 0; i < n; i++) {
        numbers[i] = i * 10;
    }
    // 5. 打印内容
    for (int i = 0; i < n; i++) {
        printf("%d ", numbers[i]);
    }
    printf("\n");
    // 6. **必须释放内存**:使用完毕后,调用 free 将内存归还给系统
    free(numbers);
    return 0;
}

核心要点:

c语言malloc函数实现
(图片来源网络,侵删)
  • 检查返回值malloc 可能会因为内存不足而失败,此时返回 NULL,直接使用 NULL 指针会导致程序崩溃(段错误)。
  • 手动管理malloc 分配的内存在程序结束前不会自动释放,你必须调用 free() 函数来释放它,否则会导致内存泄漏
  • 大小计算:计算所需大小时,通常使用 元素个数 * sizeof(元素类型)
  • free 的参数free 的参数必须是 malloc 返回的原始指针,如果你对指针进行了算术运算(如 p+1),free(p+1),结果是未定义的

malloc 的内部实现原理

这才是问题的核心。malloc 并不是一个简单的系统调用,它是一个复杂的内存管理器,其内部工作流程通常如下:

内存池

malloc 不会每次都向操作系统申请内存,频繁地与操作系统交互(系统调用)开销很大。malloc一次性向操作系统申请一大块内存,作为自己的“内存池”或“堆”,之后,所有的内存分配和释放都在这个内存池内部进行。

这块内存池通常被称为 Heap

内存块管理

为了管理这块内存池,malloc 会将内存划分为一系列的“块”(Blocks),每个块包含两部分信息:

  • 元数据:关于这块内存本身的信息,
    • 块的大小(包括元数据本身)
    • 块是否已被分配
    • 指向下一个块的指针(用于链表管理)
  • 有效载荷:实际分配给用户使用的内存区域。

元数据通常存储在有效载荷的前面(或后面),形成一个链表

分配算法

当调用 malloc(size) 时,malloc 内部会执行以下步骤:

步骤 1:调整请求大小

  • 用户请求的 size 可能不够存放元数据。malloc 会将 size 调整为一个对齐后的最小值,以确保有效载荷的起始地址满足特定类型(如 int, double)的对齐要求,提高访问效率。
  • 会加上元数据的大小,得到最终的块大小。

步骤 2:搜索空闲块 malloc 会遍历它的空闲块链表,寻找一个足够大的空闲块来满足请求,主要有三种搜索策略:

  • 首次适应法

    • 从链表头部开始,找到第一个大小足够的空闲块就立即返回。
    • 优点:速度快,倾向于把小的空闲块留在链表尾部,有利于大块内存的分配。
    • 缺点:容易在链表头部产生很多小的“碎片”。
  • 最佳适应法

    • 遍历整个链表,找到大小最合适(即大于等于请求大小的最小空闲块)的块。
    • 优点:内存利用率高,产生的碎片小。
    • 缺点:速度慢,且容易产生大量无法利用的“小碎片”。
  • 最差适应法

    • 总是使用链表中最大的空闲块。
    • 优点:可以避免产生太多的小碎片。
    • 缺点:大块内存被迅速切小,导致后续需要大块内存分配时失败。

现代的 malloc 实现(如 glibc 中的 ptmalloc)会结合多种策略,并根据使用情况进行优化。

步骤 3:处理找到的块

根据找到的空闲块大小,有两种情况:

  • 情况 A:找到的块大小 略大于 请求大小

    • 为了避免浪费,malloc 会将这块内存分割
    • 它会把这块内存分成两部分:
      1. 一部分是恰好满足请求大小的块,将其标记为“已分配”,返回给用户。
      2. 剩余的部分作为一个新的空闲块,放回空闲链表中。
  • 情况 B:找到的块大小 远大于 请求大小

    • 为了提高效率,malloc 可能不会进行分割,而是直接将整个大块返回给用户,虽然会有一些内部碎片,但减少了链表操作的开销。
  • 情况 C:没有找到足够大的空闲块

    • 如果遍历完整个空闲链表都找不到合适的块,malloc 就会向操作系统申请更多的内存(例如通过 sbrkmmap 系统调用)。
    • 获得新内存后,将其加入内存池,然后再次尝试分配。

返回指针

malloc 返回的是有效载荷区域的起始地址,而不是包含元数据的整个块的起始地址,用户只能看到和使用这个指针,对元数据部分一无所知。


释放内存 (free) 的工作原理

free(ptr) 的工作流程相对简单但同样重要:

  1. 定位块free 接收用户传来的指针 ptr,通过这个指针,malloc 内部可以计算出整个内存块(包括元数据)的起始地址。
  2. 标记为空闲:它找到对应的块,将其状态从“已分配”改为“空闲”。
  3. 合并空闲块(关键!)free 不会立即把内存还给操作系统,它会检查相邻的内存块是否也是空闲的。
    • 如果是,它会将当前块前一个或后一个空闲块合并,形成一个更大的空闲块。
    • 这个合并操作是解决内存碎片问题的关键,它能减少内存浪费,提高后续 malloc 的成功率。
  4. 归还内存(可选):当某个空闲块变得足够大,并且在一段时间内没有被使用,malloc 可能会选择将其归还给操作系统,以减少程序的内存占用。

一个简化的 malloc 实现示例

下面是一个极度简化的、用于演示核心思想的 my_mallocmy_free 的实现。它不安全,不高效,仅用于教学

#include <stdio.h>
#include <unistd.h> // for sbrk
// 简化的块结构(元数据)
typedef struct block {
    size_t size;      // �的大小(包括这个结构体)
    int free;         // 是否空闲
    struct block *next; // 指向下一个块
} Block;
Block *heap_list = NULL; // 空闲块链表的头指针
// 简化的 malloc
void* my_malloc(size_t size) {
    if (size == 0) return NULL;
    // 1. 调整大小,并加上元数据大小
    size_t total_size = sizeof(Block) + size;
    // 简单对齐到 8 字节
    total_size = (total_size + 7) & ~7;
    // 2. 遍历链表寻找合适的空闲块
    Block *current = heap_list;
    while (current) {
        if (current->free && current->size >= total_size) {
            // 找到合适的块,这里不分割,直接返回
            current->free = 0;
            return (void*)(current + 1); // 返回有效载荷的地址
        }
        current = current->next;
    }
    // 3. 没找到,向操作系统申请内存
    Block *new_block = (Block*) sbrk(total_size);
    if (new_block == (void*)-1) {
        return NULL; // sbrk 失败
    }
    new_block->size = total_size;
    new_block->free = 0;
    new_block->next = NULL;
    // 将新块加入链表
    if (heap_list == NULL) {
        heap_list = new_block;
    } else {
        current = heap_list;
        while (current->next) {
            current = current->next;
        }
        current->next = new_block;
    }
    return (void*)(new_block + 1); // 返回有效载荷的地址
}
// 简化的 free
void my_free(void *ptr) {
    if (ptr == NULL) return;
    // 1. 找到对应的块
    Block *block_to_free = (Block*)ptr - 1;
    // 2. 标记为空闲
    block_to_free->free = 1;
    // 3. 合并相邻的空闲块 (简化版,只合并下一个)
    Block *next_block = block_to_free->next;
    if (next_block && next_block->free) {
        block_to_free->size += next_block->size;
        block_to_free->next = next_block->next;
    }
}
int main() {
    int *p = (int*) my_malloc(sizeof(int) * 5);
    if (p) {
        p[0] = 100;
        printf("p[0] = %d\n", p[0]);
        my_free(p);
    }
    return 0;
}
特性 描述
核心功能 在堆上动态分配指定大小的连续内存。
返回值 指向分配内存起始地址的 void* 指针,失败时返回 NULL
内存管理 手动管理,程序员必须调用 free() 释放内存,否则造成内存泄漏。
内部机制 通过内存池(堆)减少系统调用开销;使用链表管理空闲内存块;通过分割合并技术来管理内存。
关键算法 首次适应、最佳适应等,用于在空闲块链表中查找合适的块。
性能考虑 分配速度、内存利用率、内部/外部碎片是 malloc 实现需要权衡的关键点。

现代的 malloc 实现(如 glibc 的 ptmalloc)非常复杂,它引入了多线程锁、多级分离的空闲链表、mmap 机制来处理大块内存等高级优化,但其核心思想仍然是:管理一个内存池,高效地分割和合并内存块

-- 展开阅读全文 --
头像
织梦附件上传后存放位置在哪?
« 上一篇 昨天
免费织梦模板安全吗?
下一篇 » 昨天

相关文章

取消
微信二维码
支付宝二维码

目录[+]