Administrator
发布于 2025-09-09 / 13 阅读
0
0

malloc 底层原理详解

malloc 的底层实现依赖于 C 库,通常使用 ptmalloc(glibc 默认分配器)、tcmalloc(Google)或 jemalloc(FreeBSD 等)等。

下文详细阐述 glibc 的 ptmalloc 机制下的 malloc 的底层实现


基本概念

内存分配器:管理堆内存,处理内存的分配和释放。

块(Chunk):内存分配的基本单位,每个分配的内存块都有一个头部的元数据。

空闲链表(Free List):用于管理空闲块,常见的有单向链表和双向链表。

箱(Bins):用于存放不同大小的空闲块,提高分配效率。

内存布局

在 glibc 中,malloc 管理的内存通常通过 brk 或 mmap 系统调用从操作系统获取。

  • brk:扩展堆的顶部,用于较小内存分配。

  • mmap:创建匿名内存映射,用于大内存分配(通常超过 MMAP_THRESHOLD,默认为 128KB)。

块(Chunk)结构

每个内存块(包括已分配和空闲的)都有一个头部的元数据,定义如下:

struct malloc_chunk {
  size_t      prev_size;  /* 前一个块的大小(如果前一个块是空闲的) */
  size_t      size;       /* 当前块的大小和状态标志 */

  struct malloc_chunk* fd; /* 双向链表的前驱指针(仅空闲块有效) */
  struct malloc_chunk* bk; /* 双向链表的后继指针(仅空闲块有效) */
};
  • prev_size:如果前一个块是空闲的,这个字段存储前一个块的大小;如果前一个块是已分配的,则这个字段可以被前一个块使用(作为用户数据的一部分)。

  • size:当前块的大小,由于内存对齐,大小的低3位用作标志位:

    • PREV_INUSE (最低位):前一个块是否已分配。

    • IS_MMAPPED (次低位):当前块是否通过 mmap 分配。

    • NON_MAIN_ARENA (第三位):是否不属于主分配区。

空闲块管理

空闲块通过双向链表连接,这些链表称为“bins”。ptmalloc 有多种类型的 bins:

  1. Fast bins:单向链表,管理小内存块(默认小于 64B),后进先出,不合并空闲块。

  2. Small bins:双向链表,管理小于 512B 的块,每个 bin 中的块大小相同,先进先出。

  3. Large bins:双向链表,管理大于等于 512B 的块,每个 bin 中的块大小在一个范围内,按大小排序。

  4. Unsorted bin:双向链表,存放刚刚释放的块,用于快速重新分配。

分配流程

  1. 小内存分配(< 64B)

    • 首先在 fast bins 中查找对应大小的空闲块。

    • 如果找到,则取出并返回。

  2. 中等内存分配(64B ~ 512B)

    • 在 small bins 中查找。

    • 如果 small bin 为空,则从 unsorted bin 中查找并分割。

  3. 大内存分配(>= 512B)

    • 在 large bins 中查找最合适的块。

    • 如果找不到,则从更大的 large bin 中分割,或者从 top chunk 分割。

  4. 如果上述步骤都失败

    • 扩展 top chunk(通过 brkmmap)并从中分配。

    • 如果需要分配的内存很大(超过 MMAP_THRESHOLD),则直接使用 mmap 分配。

释放流程

  1. 检查前后块是否空闲,如果空闲则合并,避免碎片。

  2. 放入 unsorted bin,以便下次分配时快速重用。

  3. 如果块大小适合 fast bin,则放入 fast bin(不合并)。

1. malloc 的基本流程

当调用 malloc(size) 时,底层会执行以下操作:

  1. 检查请求大小

    • size == 0,可能返回 NULL 或一个特殊指针(依赖实现)。

    • size 过大(如超过 MMAP_THRESHOLD,默认通常为 128KB),直接使用 mmap 分配。

  2. 从空闲内存链表中查找

    • 在堆内存的空闲块(free list)中搜索足够大的内存块。

    • 使用算法(如 ptmalloc 的隐式链表或显式链表)匹配最佳块。

  3. 分割或分配新内存

    • 若找到的块比需求大,可能分割剩余部分放回空闲链表。

    • 若没有足够空间,调用 sbrkmmap 向操作系统申请更多内存。

  4. 返回内存地址

    • 返回分配的内存地址(通常位于堆区),用户可通过指针访问。


2. 底层内存分配方式

malloc 最终依赖两种系统调用分配内存:

(1) brk/sbrk(小内存分配)

  • 原理:通过移动堆顶指针(program break)扩展堆内存。

  • 特点

    • 适用于小内存请求(如 < 128KB)。

    • 分配速度快,但频繁调用可能导致内存碎片。

  • 示例

    c复制代码

    void *malloc(size_t size) {
        if (size <= MMAP_THRESHOLD) {
            return sbrk(size);  // 扩展堆空间
        }
        // ...
    }

(2) mmap(大内存分配)

  • 原理:直接映射匿名内存页到进程地址空间(不依赖堆)。

  • 特点

    • 适用于大内存请求(如 >= 128KB)。

    • 避免堆碎片,但系统调用开销较大。

  • 示例

    c复制代码

    void *malloc(size_t size) {
        if (size >= MMAP_THRESHOLD) {
            return mmap(NULL, size, PROT_READ | PROT_WRITE, 
                       MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
        }
        // ...
    }

3. 内存管理算法

不同 malloc 实现(如 glibcptmallocjemalloctcmalloc)使用不同算法优化性能:

(1) ptmalloc(glibc 默认)

  • 空闲链表(Bins)

    • 将空闲块按大小分类(如 fast binssmall binslarge bins)。

    • 快速匹配小内存请求,减少搜索时间。

  • 线程本地缓存(tcache

    • 每个线程维护独立的内存池,避免全局锁竞争。

(2) jemalloc(Facebook/FreeBSD)

  • 多级内存池

    • 按线程和大小分层管理,减少碎片和锁冲突。

  • 适用于高并发场景

(3) tcmalloc(Google)

  • 线程缓存 + 全局缓存

    • 小对象优先从线程缓存分配,大对象走全局堆。

  • 减少锁争用,适合多线程应用。


4. 内存碎片问题

(1) 内部碎片

  • 原因:分配的内存块比实际需求大(如对齐填充)。

  • 示例:请求 10B,但分配 16B(因对齐要求)。

(2) 外部碎片

  • 原因:频繁分配/释放导致内存块分散,无法合并成大块。

  • 解决方案

    • 合并相邻空闲块(coalescing)。

    • 使用 mmap 分配大块内存避免堆碎片。


5. free 的工作原理

调用 free(ptr) 时:

  1. 检查指针有效性:若 ptr == NULL,直接返回。

  2. 判断内存来源

    • 若来自 mmap,直接调用 munmap 释放。

    • 若来自堆,将内存块标记为空闲并放回链表(可能合并相邻块)。

  3. 延迟归还操作系统

    • 空闲内存可能保留在进程堆中,供后续 malloc 复用,而非立即归还系统(通过 M_TRIM_THRESHOLD 控制)。


6. 性能优化建议

  1. 避免频繁小内存分配

    • 预分配大块内存,自行管理(如对象池)。

  2. 选择合适分配器

    • 多线程应用优先用 jemalloctcmalloc

  3. 监控内存使用

    • 工具:valgrindmalloc_stats()pmap


评论