内存管理-用户篇:从 brk 到 malloc —— 用户态内存分配器深度解析

"用户程序调用 malloc 时,内核和 libc 如何协同工作?
本文将深入 brk/mmap 系统调用、dlmalloc 算法,并对比 glibc 的实现细节,
构建一个高效、低碎片的用户态内存分配器。"

引言:用户态内存管理的挑战

当用户程序调用 malloc(100) 时,看似简单的操作背后涉及用户态与内核态的复杂协作

  • 小内存(<128KB):通过 brk 扩展堆,libc 管理空闲链表
  • 大内存(≥128KB):通过 mmap 匿名映射,直接分配虚拟内存
  • 内存回收free 需合并相邻空闲块,避免碎片
  • 安全边界:内核需验证用户指针,防止越界访问

用户态内存分配器(如 glibc 的 ptmalloc)是性能与内存效率的关键,其设计直接影响应用性能。

本文将系统性地剖析用户态内存管理,从系统调用到 libc 实现,并深度对比 glibc 的 dlmalloc 算法,最终提供一个可运行的工业级框架。


第一章:brk 与 mmap 系统调用

1.1 brk 系统调用:堆管理基础

brk 的核心作用

  • 管理堆顶指针(Program Break)
  • 扩展/收缩进程数据段
  • 返回新的堆顶地址

系统调用接口

// 内核 brk 实现
void *sys_brk(void *new_brk) {
    task_t *task = current_task;
    
    if (!new_brk) {
        // 查询当前堆顶
        return (void*)task->heap_top;
    }
    
    // 1. 检查地址合法性
    if ((uint32_t)new_brk < task->heap_start || 
        (uint32_t)new_brk > USER_VIRTUAL_MAX) {
        return (void*)-1;
    }
    
    // 2. 计算新旧堆顶
    uint32_t old_heap = task->heap_top;
    uint32_t new_heap = (uint32_t)new_brk;
    
    if (new_heap > old_heap) {
        // 扩展堆:按需映射新页
        uint32_t vaddr = old_heap & ~0xFFF;
        while (vaddr < new_heap) {
            map_page(task->pgdir, vaddr, 0, 
                     PAGE_PRESENT | PAGE_RW | PAGE_USER);
            vaddr += PAGE_SIZE;
        }
    } else if (new_heap < old_heap) {
        // 收缩堆:取消映射(简化版不实现)
        // 实际需处理中间有数据的情况
    }
    
    task->heap_top = new_heap;
    return (void*)new_heap;
}

用户态封装

// libc/brk.c
extern void *heap_start; // 链接脚本提供

void *brk(void *addr) {
    __asm__ volatile (
        "int $0x80"
        : "=a"(result)
        : "a"(45), "b"(addr)
        : "memory"
    );
    return (void*)result;
}

void *sbrk(intptr_t increment) {
    void *old_brk = brk(NULL);
    if (old_brk == (void*)-1) return NULL;
    
    void *new_brk = (char*)old_brk + increment;
    if (brk(new_brk) == (void*)-1) return NULL;
    
    return old_brk;
}

1.2 mmap 系统调用:灵活的内存映射

mmap 的核心能力

  • 匿名映射:分配大块虚拟内存(MAP_ANONYMOUS
  • 文件映射:将文件映射到内存(MAP_SHARED/MAP_PRIVATE
  • 共享内存:进程间共享内存区域

系统调用接口

// 内核 mmap 实现
void *sys_mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset) {
    task_t *task = current_task;
    
    // 1. 对齐地址和长度
    length = ALIGN_UP(length, PAGE_SIZE);
    
    // 2. 查找空闲虚拟地址
    uint32_t vaddr = find_free_vma(task, length);
    if (!vaddr) return MAP_FAILED;
    
    // 3. 创建 VMA
    struct vm_area_struct *vma = kmalloc(sizeof(struct vm_area_struct));
    vma->vm_start = vaddr;
    vma->vm_end = vaddr + length;
    vma->vm_flags = (flags & MAP_ANONYMOUS) ? VM_ANONYMOUS : VM_FILE;
    vma->vm_file = (flags & MAP_ANONYMOUS) ? NULL : get_file(fd);
    vma->vm_pgoff = offset / PAGE_SIZE;
    
    // 4. 插入 VMA 链表
    insert_vma(task, vma);
    
    // 5. 按需映射(匿名页暂不分配物理页)
    if (flags & MAP_ANONYMOUS) {
        // Page Fault 时分配
    } else {
        // 文件映射:Page Fault 时加载
    }
    
    return (void*)vaddr;
}

用户态封装

// libc/mmap.c
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset) {
    __asm__ volatile (
        "int $0x80"
        : "=a"(result)
        : "a"(90), "b"(addr), "c"(length), "d"(prot), "S"(flags), "D"(fd)
        : "memory"
    );
    return (void*)result;
}

int munmap(void *addr, size_t length) {
    __asm__ volatile (
        "int $0x80"
        : "=a"(result)
        : "a"(91), "b"(addr), "c"(length)
        : "memory"
    );
    return result;
}

1.3 brk vs mmap 对比

特性 brk mmap
适用场景 小内存(堆) 大内存、文件映射
内存连续性 连续 可不连续
分配粒度 页对齐 页对齐
回收方式 sbrk(负值) munmap
碎片风险 高(中间无法释放) 低(可释放任意区域)
性能 快(单次系统调用) 慢(需 VMA 操作)

💡 glibc 默认阈值 128KB:小内存 brk,大内存 mmap


第二章:用户态 malloc 实现框架

2.1 malloc 设计目标

核心需求:

  1. 高性能:分配/释放 O(1) 时间
  2. 低内存开销:元数据 < 8 字节/块
  3. 低碎片:合并相邻空闲块
  4. 线程安全:支持多线程(本文简化为单线程)

设计决策:

  • 小内存(<128KB):使用 brk 扩展的堆 + 空闲链表
  • 大内存(≥128KB):使用 mmap 匿名映射
  • 空闲块管理边界标签法(Boundary Tag)

2.2 内存块结构设计

块头(Chunk Header)

#define CHUNK_SIZE_MASK (~7) // 低 3 位用于标志
#define PREV_INUSE      1    // 前一块在使用
#define IS_MMAPED       2    // 通过 mmap 分配
#define NON_MAIN_ARENA  4    // 非主 arena

struct malloc_chunk {
    size_t prev_size; // 前一块大小(仅当 PREV_INUSE=0 有效)
    size_t size;      // 当前块大小 + 标志
    
    struct malloc_chunk *fd; // 空闲块:前向指针
    struct malloc_chunk *bk; // 空闲块:后向指针
    
    // 数据区域(用户可用)
    // char data[0];
};

关键设计:

  • 低 3 位存储标志:节省内存
  • prev_size 字段:实现双向合并
  • fd/bk 指针:空闲块时用作链表指针

2.3 空闲链表管理

边界标签法原理

  • 每个块存储自身大小
  • 空闲块额外存储前一块大小
  • 通过 prev_size 实现向前合并

合并算法

// 向前合并
static struct malloc_chunk *merge_prev(struct malloc_chunk *chunk) {
    if (chunk->size & PREV_INUSE) {
        return chunk; // 前一块在使用
    }
    
    struct malloc_chunk *prev = (void*)chunk - chunk->prev_size;
    prev->size += chunk->size & CHUNK_SIZE_MASK;
    return prev;
}

// 向后合并
static struct malloc_chunk *merge_next(struct malloc_chunk *chunk) {
    size_t size = chunk->size & CHUNK_SIZE_MASK;
    struct malloc_chunk *next = (void*)chunk + size;
    
    if (!(next->size & PREV_INUSE)) {
        // 下一块空闲,合并
        chunk->size += next->size & CHUNK_SIZE_MASK;
        // 从空闲链表移除 next
        unlink_chunk(next);
    }
    return chunk;
}

空闲链表操作

// 将块加入空闲链表
static void link_chunk(struct malloc_chunk *chunk) {
    chunk->fd = free_list;
    chunk->bk = NULL;
    if (free_list) {
        free_list->bk = chunk;
    }
    free_list = chunk;
}

// 从空闲链表移除块
static void unlink_chunk(struct malloc_chunk *chunk) {
    if (chunk->bk) {
        chunk->bk->fd = chunk->fd;
    } else {
        free_list = chunk->fd;
    }
    if (chunk->fd) {
        chunk->fd->bk = chunk->bk;
    }
}

第三章:malloc/free 核心算法实现

3.1 malloc 实现

主分配函数

void *malloc(size_t size) {
    if (size == 0) return NULL;
    
    // 1. 对齐大小(至少 8 字节)
    size_t aligned_size = ALIGN_UP(size + sizeof(struct malloc_chunk), 8);
    
    // 2. 大内存:直接 mmap
    if (aligned_size >= MMAP_THRESHOLD) { // 128KB
        return mmap_alloc(aligned_size);
    }
    
    // 3. 小内存:从堆分配
    return heap_alloc(aligned_size);
}

堆分配(heap_alloc)

static void *heap_alloc(size_t size) {
    // 1. 查找合适空闲块
    struct malloc_chunk *best = NULL;
    struct malloc_chunk *chunk = free_list;
    
    while (chunk) {
        if ((chunk->size & CHUNK_SIZE_MASK) >= size) {
            if (!best || (chunk->size < best->size)) {
                best = chunk;
            }
        }
        chunk = chunk->fd;
    }
    
    if (best) {
        // 2. 分割块(如果过大)
        size_t best_size = best->size & CHUNK_SIZE_MASK;
        if (best_size >= size + MIN_CHUNK_SIZE) {
            // 分割
            struct malloc_chunk *remainder = (void*)best + size;
            remainder->size = best_size - size;
            remainder->size |= best->size & PREV_INUSE; // 继承标志
            
            // 更新 best
            best->size = size | (best->size & ~CHUNK_SIZE_MASK);
            
            // 将 remainder 加入空闲链表
            link_chunk(remainder);
        } else {
            // 3. 移除块
            unlink_chunk(best);
        }
        
        // 4. 标记下一块的 PREV_INUSE
        struct malloc_chunk *next = (void*)best + (best->size & CHUNK_SIZE_MASK);
        next->size |= PREV_INUSE;
        
        return (void*)(best + 1);
    }
    
    // 4. 无合适块,扩展堆
    return expand_heap(size);
}

扩展堆(expand_heap)

static void *expand_heap(size_t size) {
    // 1. 计算需要扩展的大小(包含块头)
    size_t total_size = size + sizeof(struct malloc_chunk);
    total_size = ALIGN_UP(total_size, PAGE_SIZE);
    
    // 2. 调用 sbrk 扩展堆
    void *old_brk = sbrk(0);
    if (sbrk(total_size) == (void*)-1) {
        return NULL; // 内存不足
    }
    
    // 3. 初始化新块
    struct malloc_chunk *new_chunk = (void*)old_brk;
    new_chunk->prev_size = 0; // 堆底
    new_chunk->size = size | PREV_INUSE;
    
    // 4. 标记下一块(堆顶)的 PREV_INUSE
    struct malloc_chunk *top = (void*)new_chunk + size;
    top->size = 0 | PREV_INUSE; // 堆顶标记
    
    return (void*)(new_chunk + 1);
}

mmap 分配(mmap_alloc)

static void *mmap_alloc(size_t size) {
    // 1. 分配虚拟内存
    void *addr = mmap(NULL, size, 
                      PROT_READ | PROT_WRITE, 
                      MAP_PRIVATE | MAP_ANONYMOUS, 
                      -1, 0);
    if (addr == MAP_FAILED) return NULL;
    
    // 2. 初始化块头(标记为 mmap)
    struct malloc_chunk *chunk = (void*)addr;
    chunk->prev_size = 0;
    chunk->size = size | IS_MMAPED;
    
    return (void*)(chunk + 1);
}

3.2 free 实现

主释放函数

void free(void *ptr) {
    if (!ptr) return;
    
    // 1. 获取块头
    struct malloc_chunk *chunk = (struct malloc_chunk*)ptr - 1;
    
    // 2. 检查是否 mmap 分配
    if (chunk->size & IS_MMAPED) {
        munmap_alloc(chunk);
        return;
    }
    
    // 3. 合并相邻空闲块
    chunk = merge_prev(chunk);
    chunk = merge_next(chunk);
    
    // 4. 加入空闲链表
    link_chunk(chunk);
}

mmap 释放(munmap_alloc)

static void munmap_alloc(struct malloc_chunk *chunk) {
    size_t size = chunk->size & CHUNK_SIZE_MASK;
    munmap(chunk, size);
}

第四章:glibc ptmalloc 深度对比

4.1 glibc 内存分配器演进

版本 分配器 特点
glibc 2.0 dlmalloc 单线程,边界标签
glibc 2.1+ ptmalloc2 多线程,arena 分区
glibc 2.23+ ptmalloc3 优化碎片,tcache

ptmalloc2 核心改进:

  • 多 Arena:每个线程独立 arena,减少锁竞争
  • Bins 分类:fastbins(小块)、bins(普通块)、top chunk
  • 线程缓存(tcache):glibc 2.26+,进一步减少锁

4.2 ptmalloc2 核心数据结构

Arena 结构

// malloc.c
struct malloc_state {
    mutex_t mutex;          // 锁
    mchunkptr top;          // 顶块
    mchunkptr last_remainder; // 最后余块
    mchunkptr bins[NBINS * 2 - 2]; // 空闲链表
    unsigned int binmap[BINMAPSIZE]; // bin 位图
    // ...
};

Bins 分类:

  • Fastbins:16-80 字节,LIFO,无合并
  • Unsorted bin:刚释放的块,快速重用
  • Small bins:<512 字节,FIFO,合并
  • Large bins:≥512 字节,按大小排序

4.3 ptmalloc2 分配流程

malloc 流程:

  1. 检查 fastbins:小块直接分配
  2. 检查 unsorted bin:快速重用
  3. 检查 small/large bins:查找合适块
  4. 使用 top chunk:无合适块时分割 top
  5. 扩展堆/mmap:top 不足时

free 流程:

  1. 检查 fastbins:小块直接放入
  2. 合并相邻块:向前/向后合并
  3. 放入 unsorted bin:合并后的块
  4. 整理 bins:定期将 unsorted bin 移到对应 bin

4.4 tcache 优化(glibc 2.26+)

tcache 设计:

  • 每个线程独立缓存:64 个 bin(1-1032 字节)
  • 无锁操作:分配/释放完全无锁
  • 缓存大小限制:每个 bin 最多 7 块

性能提升:

  • 小内存分配提速 2-3 倍
  • 减少 arena 锁竞争

第五章:高级特性与安全机制

5.1 内存对齐与最小块大小

对齐要求:

  • 8 字节对齐:满足 double、指针对齐
  • 块头 8 字节:prev_size + size

最小块大小:

#define MIN_CHUNK_SIZE (sizeof(struct malloc_chunk))
// 通常 16 字节(32 位)或 32 字节(64 位)

请求大小对齐:

size_t request2size(size_t req) {
    if (req + MIN_CHUNK_SIZE < MIN_CHUNK_SIZE) {
        return MIN_CHUNK_SIZE; // 溢出保护
    }
    return (req + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK;
}

5.2 安全机制:防止堆溢出

块头保护:

  • size 字段校验:防止覆盖
  • magic number(可选):检测块头损坏

glibc 安全特性:

  • FORTIFY_SOURCE:编译时检查
  • _FORTIFY_SOURCE=2:运行时检查
  • malloc_check:环境变量启用额外检查

5.3 内存泄漏检测

malloc_stats:

// glibc 提供
void malloc_stats(void);
/*
输出示例:
Arena 0:
system bytes     = 135168
in use bytes     = 12288
Total (incl. mmap):
system bytes     = 135168
in use bytes     = 12288
max mmap regions = 0
max mmap bytes   = 0
*/

malloc_info:

  • XML 格式详细统计
  • 可用于内存分析工具

第六章:与内核的交互安全

6.1 copy_from_user / copy_to_user

问题:内核如何安全访问用户内存?

  • 用户指针可能无效(NULL、内核地址)
  • 用户指针可能未映射(触发 Page Fault)

安全验证函数:

// 内核 copy_from_user
long copy_from_user(void *to, const void __user *from, unsigned long n) {
    // 1. 验证地址范围
    if (!access_ok(from, n)) {
        return n; // 失败
    }
    
    // 2. 处理 Page Fault
    if (copy_from_user_nofault(to, from, n)) {
        return n; // 失败
    }
    
    return 0; // 成功
}

// 验证用户地址
bool access_ok(const void __user *addr, unsigned long size) {
    return (uint32_t)addr + size <= USER_VIRTUAL_MAX;
}

系统调用中的使用:

// sys_write
ssize_t sys_write(int fd, const void __user *buf, size_t count) {
    // 1. 验证用户指针
    if (!access_ok(buf, count)) {
        return -1;
    }
    
    // 2. 临时映射(或直接 copy_from_user)
    char kernel_buf[512];
    size_t copied = 0;
    
    while (copied < count) {
        size_t chunk = min(count - copied, sizeof(kernel_buf));
        if (copy_from_user(kernel_buf, buf + copied, chunk)) {
            return -1;
        }
        // 写入文件
        vfs_write(fd, kernel_buf, chunk);
        copied += chunk;
    }
    
    return copied;
}

6.2 用户态指针验证

malloc 中的指针验证:

// free 时验证指针
void free(void *ptr) {
    if (!ptr) return;
    
    // 1. 检查是否在堆范围内
    if (ptr < heap_start || ptr >= sbrk(0)) {
        // 可能是 mmap 分配,检查 mmap 区域
        if (!is_mmap_pointer(ptr)) {
            abort(); // 无效指针
        }
    }
    
    // 2. 检查块头 magic(可选)
    // ...
    
    // 正常释放
}

⚠️ 生产环境 malloc 库通常不验证指针(性能考虑),依赖 valgrind 等工具检测


结论:用户态内存管理的工程权衡

用户态内存分配器是性能、内存效率、复杂度的完美平衡:

  • brk/mmap 分离:小内存高效,大内存灵活
  • 边界标签法:O(1) 合并,低碎片
  • 多级缓存(tcache):无锁分配,极致性能

理解 malloc 不仅有助于应用开发,更能培养内存安全意识

  • 避免内存泄漏:及时 free
  • 防止堆溢出:边界检查
  • 合理使用内存:避免大内存频繁分配

对于希望深入 glibc 的开发者,建议:

  • 阅读 malloc/malloc.c 源码
  • 使用 valgrindmassif 分析内存使用
  • 通过 MALLOC_TRACE 跟踪分配轨迹

用户态内存管理的故事,仍在继续。随着 jemalloc、tcmalloc 等高性能分配器的出现,这一古老而关键的组件将持续演进。


附录:关键数据结构与函数速查

核心数据结构

| 结构 | 作用 | 文件 | |——|——|——| | struct malloc_chunk | 内存块头 | malloc.h | | struct malloc_state | Arena 状态 | malloc.c | | tcache_perthread_struct | 线程缓存 | malloc.c |

关键函数

| 函数 | 功能 | 文件 | |——|——|——| | __libc_malloc | malloc 入口 | malloc.c | | sysmalloc | 大内存分配 | malloc.c | | int_free | free 核心 | malloc.c | | malloc_stats | 内存统计 | malloc.c |

调试环境变量

| 变量 | 用途 | |——|——| | MALLOC_TRACE | 跟踪分配轨迹 | | MALLOC_CHECK_ | 启用安全检查 | | TCMALLOC_SAMPLE_PARAMETER | tcmalloc 采样 | | LD_PRELOAD | 替换分配器(如 jemalloc) |