北大信科计算机系面试时问到了这个问题,没答出来。这个问题涉及到操作系统的内存管理和系统调用功能。
一、简单理解
1. 堆内存管理
我们常说的 malloc 函数是 glibc 提供的库函数。glibc 的内存管理使用的方法是 ptmalloc,除此之后还有很多其他内存管理方案,比如 tcmalloc (golang 使用的就是 tcmalloc)。
ptmalloc 对于申请内存小于 128KB 时,分配是在堆段,使用系统调用 brk() 或者 sbrk()。如果大于 128 KB 的话,分配在映射区,使用系统调用 mmap()。
2. brk, sbrk
在堆段申请的话,使用系统调用 brk 或者 sbrk。
int brk(const void *addr);
void *sbrk(intptr_t incr);
void *malloc(size_t size) {
void *p = sbrk(0);
void *request = sbrk(size);
if (request == (void*) -1) {
return NULL; // sbrk failed.
} else {
assert(p == request); // Not thread safe.
return p;
}
}
3. mmap
#include
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
mmap 的 flags 可选多种参数,当选择 MAP_ANONYMOUS 时,不需要传入文件描述符,malloc 使用的就是 MAP_ANONYMOUS 模式。mmap 申请的内存在操作系统的映射区。比如 32 位系统,映射区从 3G 虚拟地址粗向下生长,但是因为程序的其他段也会占用空间(比如代码段必须以特定的地址开始),所以并不能申请 3G 的大小。
4. malloc 和物理内存有关系吗?
可以说没关系,malloc 申请的地址是线性地址,申请的时候并没有进行映射。访问到的时候触发缺页异常,这个时候才会进行物理地址映射。
5. ptmalloc
1.每个进程只有一个主分配区(main arena),可能存在多个非主分配区(non main arena), ,ptmalloc根据系统对分配区的争用情况动态增加非主分配区的数量,主分配区与非主分配区用环形链表进行管理.
2.主分配区可以访问进程的heap区域和mmap映射区域,也就是说主分配区可以使用sbrk和mmap向操作系统申请虚拟内存。而非主分配区只能访问进程的mmap映射区域,非主分配区每次使用mmap()向操作系统“批发”HEAP_MAX_SIZE(32位系统上默认为1MB,64位系统默认为64MB)大小的虚拟内存,当用户向非主分配区请求分配内存时再切割成小块“零售”出去.
3 特别大的内存分配总是使用mmap,使用mmap分配的内存在释放时直接归还给操作系统
4. 小块的内存分配使用brk,因为用mmap映射匿名页,当发生缺页异常时,linux内核为缺页分配一个新物理页,并将该物理页清0,一个mmap的内存块需要映射多个物理页,导致多次清0操作,很浪费系统资源,所以引入了mmap分配阈值动态调整机制,保证在必要的情况下才使用mmap分配内存。
5. 尽量只缓存临时使用的空闲小内存块,对大内存块在释放时都直接归还给操作系统。
6. 对空闲的小内存块只会在malloc和free的时候进行合并,free时空闲内存块可能放入pool中,不一定归还给操作系统。
7. 收缩堆的条件是当前free的块大小加上前后能合并chunk的大小大于64KB、,并且堆顶的大小达到阈值,才有可能收缩堆,把堆最顶端的空闲内存返回给操作系统。
8. 为了支持多线程,多个线程可以从同一个分配区(arena)中分配内存,ptmalloc假设线程A释放掉一块内存后,线程B会申请类似大小的内存,但是A释放的内存跟B需要的内存不一定完全相等,可能有一个小的误差,就需要不停地对内存块作切割和合并,这个过程中可能产生内存碎片
二、细节理解
1、理解方式1
在linux系统下是怎么程序中的堆的。在linux系统下面一个程序的堆的管理是通过内存块进行管理的,也就是将堆分成了很多大小不一的内存块。这些块怎么管理尼,比如怎么查询块的大小,怎么查询块是否正在被程序使用,怎么知道这个块的地址。为了解决内存块的管理所以要设计一个管理内存块的数据结构,详细的数据结构如下:
/**内存控制块数据结构,用于管理所有的内存块
* is_available: 标志着该块是否可用。1表示可用,0表示不可用
* size: 该块的大小
**/
struct mem_control_block {
int is_available;
int size;
};
有了管理内存块的数据结构,那么在内存中堆的组织形式也好理解了,也就是堆是由很多内存块组成的,所以有了如下的示意图:
2、理解方式2
为了支持多线程并行处理时对于内存的并发请求操作,malloc的实现中把全局用户堆(heap)划分成很多子堆(sub-heap)。这些子堆是按照循环单链表的形式组织起来的。每一个子堆利用互斥锁(mutex)使线程对于该子堆的访问互斥。当某一线程需要调用malloc分配内存空间时,该线程搜索循环链表试图获得一个没有加锁的子堆。如果所有的子堆都已经加锁,那么malloc会开辟一块新的子堆,对于新开辟的子堆默认情况下是不加锁的,因此线程不需要阻塞就可以获得一个新的子堆并进行分配操作。在回收free操作中,线程同样试图获得待回收块所在子堆的锁,如果该子堆正在被别的线程使用,则需要等待直到其他线程释放该子堆的互斥锁之后才可以进行回收操作
有疑问加站长微信联系(非本文作者)