内存管理-用户篇:从 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 设计目标
核心需求:
- 高性能:分配/释放 O(1) 时间
- 低内存开销:元数据 < 8 字节/块
- 低碎片:合并相邻空闲块
- 线程安全:支持多线程(本文简化为单线程)
设计决策:
- 小内存(<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 流程:
- 检查 fastbins:小块直接分配
- 检查 unsorted bin:快速重用
- 检查 small/large bins:查找合适块
- 使用 top chunk:无合适块时分割 top
- 扩展堆/mmap:top 不足时
free 流程:
- 检查 fastbins:小块直接放入
- 合并相邻块:向前/向后合并
- 放入 unsorted bin:合并后的块
- 整理 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源码 - 使用
valgrind、massif分析内存使用 - 通过
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) |