前言
本文主要介绍了ptmalloc对于内存分配的管理。结合网上的一些文章和个人的理解,对ptmalloc的实现原理做一些总结。
内存布局
介绍ptmalloc之前,我们先了解一下内存布局,以x86的32位系统为例:
从上图可以看到,栈至顶向下扩展,堆至底向上扩展, mmap 映射区域至顶向下扩展。 mmap 映射区域和堆相对扩展,直至耗尽虚拟地址空间中的剩余区域,这种结构便于 C 运行时库使用 mmap 映射区域和堆进行内存分配。
brk(sbrk)和mmap函数
首先,linux系统向用户提供申请的内存有brk(sbrk)和mmap函数。 下面我们先来了解一下这几个函数。
brk() 和 sbrk()
#include <unistd.h>
int brk( const void *addr )
void* sbrk ( intptr_t incr );
两者的作用是扩展heap的上界brk
brk()的参数设置为新的brk上界地址,成功返回1,失败返回0;
sbrk()的参数为申请内存的大小,返回heap新的上界brk的地址
mmap()
#include <sys/mman.h>
void *mmap(void *addr, size\_t length, int prot, int flags, int fd, off\_t offset);
int munmap(void *addr, size_t length);
mmap的第一种用法是映射此盘文件到内存中;第二种用法是匿名映射,不映射磁盘文件,而向映射区申请一块内存。
malloc使用的是mmap的第二种用法(匿名映射)。
Munmap函数用于释放内存。
主分配区和非主分配区(可以不看)
Allocate的内存分配器中,为了解决多线程锁争夺问题,分为主分配区main_area和非主分配区no_main_area。
1. 主分配区和非主分配区形成一个环形链表进行管理。
2. 每一个分配区利用互斥锁使线程对于该分配区的访问互斥。
3. 每个进程只有一个主分配区,也可以允许有多个非主分配区。
4. ptmalloc根据系统对分配区的争用动态增加分配区的大小,分配区的数量一旦增加,则不会减少。
5. 主分配区可以使用brk和mmap来分配,而非主分配区只能使用mmap来映射内存块
6. 申请小内存时会产生很多内存碎片,ptmalloc在整理时也需要对分配区做加锁操作。
当一个线程需要使用malloc分配内存的时候,会先查看该线程的私有变量中是否已经存在一个分配区。若是存在。会尝试对其进行加锁操作。若是加锁成功,就在使用该分配区分配内存,若是失败,就会遍历循环链表中获取一个未加锁的分配区。若是整个链表中都没有未加锁的分配区,则malloc会开辟一个新的分配区,将其加入全局的循环链表并加锁,然后使用该分配区进行内存分配。当释放这块内存时,同样会先获取待释放内存块所在的分配区的锁。若是有其他线程正在使用该分配区,则必须等待其他线程释放该分配区互斥锁之后才能进行释放内存的操作。
Malloc实现原理:
因为brk、sbrk、mmap都属于系统调用,若每次申请内存,都调用这三个,那么每次都会产生系统调用,影响性能;其次,这样申请的内存容易产生碎片,因为堆是从低地址到高地址,如果高地址的内存没有被释放,低地址的内存就不能被回收。
所以malloc采用的是内存池的管理方式(ptmalloc),ptmalloc 采用边界标记法将内存划分成很多块,从而对内存的分配与回收进行管理。为了内存分配函数malloc的高效性,ptmalloc会预先向操作系统申请一块内存供用户使用,当我们申请和释放内存的时候,ptmalloc会将这些内存管理起来&