本文介绍如何用c语言实现一个简单的内存分配器,可替换glibc中的 malloc(), calloc(), realloc(), free().
这是一篇入门级别的文章,所以不会介绍所有的细节。 代码实现主要为了演示内存分配器的基本工作原理,所以和工业级内存分配器相比,缺少非常多的性能优化,分配内存时也不会按页对齐,但是至少,我们构建的内存分配器是可以工作的。
在构建内存分配器之前,需要先介绍程序的内存布局。操作系统中的进程运行在它自己独有的虚拟地址空间,不同进程间的虚拟地址空间是独立、相互隔离的。虚拟地址空间一般由以下5部分组成:
代码段(Text section):包含被处理器执行的二进制指令 数据段(Data section):包含程序中已手动初始化的全局静态变量和局部静态变量 BSS(Block Started by Symbol)段:包含了程序中未手动初始化的全局变量和局部静态变量。这部分数据会被初始化为零值。 堆:包含动态申请的内存 栈:包含了局部变量,函数参数等
如上图所示,栈和堆的增长方向是相反的。 有时数据段、BSS段、堆三个区域会统称为"数据段"(data segment), 它的尾部被一个叫做brk(program break)的指针所指向。 也就是说,brk指向堆的尾部。
如果我们想在堆上申请更多的内存,我们需要向操作系统请求增长brk。同理,释放内存时需要向操作系统请求减小brk。
在Linux系统(或其他Unix-like系统)下,我们需要使用sbrk()
系统调用来操作程序的brk指针。
调用sbrk(0)
返回当前brk的位置。 调用sbrk(x)
并传入正数表示对brk增长x字节大小,即分配内存。 调用sbrk(-x)
并传入负数表示对brk减少x字节大小,即释放内存。 如果函数调用失败,sbrk()将返回(void*)-1
。
除了sbrk(),还可以使用mmap()申请内存。sbrk()并不是真正的线程安全。并且它只能按后进先出的顺序增长和收缩。 如果你执行man 2 sbrk
命令查看帮助手册,它会告诉你:
brk和sbrk是早期虚拟内存管理出现之前的历史遗留产物。
然而,glibc中的malloc依然使用sbrk()来申请小块内存,具体可以见The GNU C Library: Malloc Tunable Parameters。 所以,我们也使用sbrk()来实现我们的简单版本内存分配器。
malloc()
malloc(size)函数申请size字节大小的内存并返回指向所申请内存的指针。 第一版代码实现如下:
void *malloc(size_t size)
{
void *block;
block = sbrk(size);
if (block == (void*)