0Pricing
C Academy · 课时

malloc 的工作原理

堆和空闲列表。

malloc 的工作原理 是 CoddyKit 上的免费 C Academy 课时。 这是第 1 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 C Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 C Academy 课程共包含 4 节课。

malloc 真正做了什么

调用 malloc(n) 时,C 库会为您提供一个指针,指向至少有 n 个可用字节的区域。但堆只是进程内存中的一块区域,由分配器代您管理。

分配器的工作是维护记录:跟踪哪些字节正在使用、哪些字节空闲,以及如何高效地重新利用已释放的内存。

堆来自 OS

分配器不会凭空创建内存。它会通过系统调用向操作系统请求大块内存,例如 brk/sbrk 或 mmap。

然后,它将这些大块内存切分成较小的内存块,供您的 malloc 调用使用。向操作系统请求内存开销很大,因此分配器会批量申请并循环利用内存。

/* Conceptual: grow the heap by 4096 bytes */
void *base = sbrk(4096);
if (base == (void *)-1) {
    /* out of memory */
}

sbrk 与程序断点

sbrk(n) 将“程序断点”向上移动 n 个字节,并返回移动前的断点。新暴露出的区域就成为可用的堆空间。

这种方式线性且简单,但不容易归还中间的内存。现代分配器更倾向于使用 mmap 处理大型请求。

void *prev_break = sbrk(0);   /* current break */
sbrk(1024);                   /* grow by 1 KB */
/* prev_break now points to fresh memory */

内存块元数据

对于每次分配,分配器都会在数据旁边存储一个小型头部,其中记录大小以及该内存块是否空闲。这个头部让 free 只凭您传回的数据指针就能工作。

您从 malloc 收到的指针指向头部之后的位置,因此元数据对您保持隐藏。

typedef struct block {
    size_t size;
    int free;
    struct block *next;
} block_t;

紧跟在头部之后的指针

一种常见技巧是指针运算:用户指针就是 header + 1。给定一个用户指针后,头部就在它之前一个 block_t 的位置。

这样,free(p) 无需您额外传递大小,就能恢复所分配内存块的大小。

block_t *hdr = (block_t *)user_ptr - 1;
printf("block size = %zu\n", hdr->size);

小型头部布局演示

让我们在一个静态缓冲区上放置一个头部,然后将其读回。这展示了真实分配器如何把一个区域划分为头部和有效载荷。

整个过程不涉及 OS 调用,因此可以在任何地方运行。

#include <stdio.h>
#include <stddef.h>

typedef struct { size_t size; int free; } block_t;
static char buffer[256];

int main(void) {
    block_t *h = (block_t *)buffer;
    h->size = 64;
    h->free = 0;
    void *payload = (char *)buffer + sizeof(block_t);
    printf("header bytes = %zu\n", sizeof(block_t));
    printf("payload offset = %ld\n", (long)((char *)payload - buffer));
    printf("size field = %zu\n", h->size);
    return 0;
}

空闲链表的概念

许多分配器会将空闲内存块串成链表。当您调用 malloc 时,分配器会遍历此链表,寻找足够大的内存块。

当您调用 free 时,该内存块会被标记为空闲并放回链表,以便日后重用,从而避免再次向 OS 请求内存。

block_t *find_free(block_t *head, size_t size) {
    block_t *b = head;
    while (b && !(b->free && b->size >= size))
        b = b->next;
    return b;
}

free 必须完成什么

free(p) 会找到 p 对应的头部,将其标记为空闲,并且理想情况下将它与相邻的空闲内存块合并(合并),以缓解碎片化。

对同一个指针调用两次 free,或释放一个不属于堆的指针,都会产生未定义行为,因为这会破坏元数据。

void my_free(void *p) {
    if (!p) return;
    block_t *hdr = (block_t *)p - 1;
    hdr->free = 1;
    /* real allocators coalesce neighbors here */
}

碎片化

随着时间推移,释放和分配不同大小的内存会留下间隙。外部碎片是指存在空闲内存,但它被分散成多个过小的片段,无法满足某个请求。

内部碎片是指一个内存块大于实际需求,导致块内部的空间被浪费,通常由对齐或取整造成。

对齐要求

malloc 必须返回适合存储任意类型的对齐内存。在大多数 64 位系统中,这意味着 16 字节对齐,从而满足 max_align_t 的要求。

未对齐的指针可能会在某些 CPU 上导致崩溃,或在其他 CPU 上降低访问速度,因此分配器总会将有效载荷向上取整到对齐边界。

#include <stdalign.h>
/* alignof(max_align_t) is the strictest required alignment */
size_t a = alignof(max_align_t);

整合起来

因此,一个最小分配器需要:内存来源(静态缓冲区、sbrk 或 mmap)、每个内存块的头部、查找空闲空间的策略,以及对齐处理。

在接下来的课程中,我们将构建这些部分:先构建 bump 分配器,然后构建空闲链表,最后处理对齐和内存块拆分。

/* The four pillars of a custom allocator */
/* 1. memory source   2. block headers */
/* 3. free-block search   4. alignment */

快速检查

检验您对分配器内部机制的理解。

回顾

malloc 通过 sbrk 或 mmap 管理由 OS 提供的堆,并将其切分成带有隐藏头部的内存块;头部记录大小和空闲状态。

空闲链表支持内存重用,对齐可以满足所有类型的要求,而碎片化是核心挑战。这些概念构成了我们接下来要构建的分配器的基础。

常见问题解答

「malloc 的工作原理」课时是免费的吗?

是的 — 「malloc 的工作原理」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 C Academy 课程的其余内容,请升级到 CoddyKit PRO。 C Academy 课程共包含 4 节课。

「malloc 的工作原理」这节课中我会学到什么?

堆和空闲列表。 你通过在浏览器中直接运行的动手代码来练习 C Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 C Academy 需要有经验吗?

无需任何先前经验。CoddyKit 上的 C Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 1 节课,共 4 节。

「malloc 的工作原理」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 C Academy 课中编写并运行代码吗?

能。每节 C Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. malloc 的工作原理
  2. 简单的碰撞分配器
  3. 空闲列表与复用
  4. 对齐与拆分
← 返回 C Academy