先来了解一些背景知识。
进程地址空间的组成:
- 内核空间:这块区域包含两种数据,一种是对每个进程都一样的数据,如共享的内核代码和全局数据结构。另一种是每个进程都不一样的数据,如页表、内核在进程上下文中执行代码时使用的栈,以及记录该进程虚拟地址空间当前组织状态的各种数据结构。
BSS
(Block Started by Symbol
)通常是指用来存放程序中未初始化的全局变量和静态变量的一块内存区域。是可读写的。在程序执行之前BSS
段会自动清0
。所以,未初始的全局变量在程序执行之前已经成0
了)。- 数据段(
data segment
)通常是指用来存放程序中已初始化的全局变量的一块内存区域。数据段属于静态内存分配。 - 代码段(
code segment/text segment
)通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读, 某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。 - 堆(
heap
)是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc
函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free
函数释放内存时,被释放的内存从堆中被释放(堆被缩减) 。 - 栈(
stack
)又称堆栈, 是用户存放程序临时创建的局部变量,也就是说我们函数块中定义的变量(但不包括static
声明的变量,static
意味着在数据段中存放变量)。除此以外,在函数被调用时,其参数也会被压入发起调用的栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的后进先出特点,所以栈特别方便用来保存/
恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。 - 内存映射区:这块虚拟内存区域被映射到任何需要的对象身上,如共享库文件、磁盘上的普通文件和将要介绍的通过
malloc
分配的大块内存区域(大于128K
)。
注意这些区域实际上存在于进程的虚拟地址空间上,一开始并没有被装载到物理内存上,比如说bss
段,理论上是被初始化为零值,但并不是说在物理内存区域的对应段被填入零值,实际上,是将其映射到了零页,等到CPU
向这块区域内写数据时,通过引发缺页故障的形式,才被装载到物理内存,分配到初始化为零的页(所谓的写时复制)。
操作系统提供的两个系统调用函数:
1 |
|
sbrk
函数的作用是移动brk
指针扩展heap
的上界。函数的参数指示brk
指针移动的大小,返回申请之前的brk
地址。注意申请的内存地址中的值是随机的,即不要求为零。
mmap
函数第一种用法是映射磁盘文件到内存中;而malloc
使用的mmap
函数的第二种用法,即匿名映射,匿名映射不映射磁盘文件,而是向映射区申请一块全零页内存,相应的虚拟页面是被初始化为零的。
如上面的进程虚拟内存布局图所示,mmap
对应Memory Mapping Segment
,brk
对应Heap
。start_brk
是堆段的开始位置,brk(program break)
则是堆段的结束位置。
下面开始介绍malloc
的实现原理。
那么,既然brk/mmap
提供了内存分配的功能,直接使用brk/mmap
进行内存管理不是更简单吗,为什么需要malloc
呢?
我们知道,系统调用本身会产生软中断,导致程序从用户态陷入内核态,比较消耗资源。试想,如果频繁分配回收小块内存区,那么将有很大的性能耗费在系统调用中。因此,为了减少系统调用带来的性能损耗,
malloc
采用了内存池的设计,增加了一个代理层,每次内存分配,都优先从内存池中寻找,如果内存池中无法提供,再向操作系统申请。
当申请小于128K
内存时,malloc
使用sbrk
分配内存,当申请大于128K
内存时,使用mmap
函数申请内存。
下面主要讨论对于小块内存的申请malloc
所采用的内存池设计方案。分配器不但要管理已分配的内存块,还需要管理空闲的内存块。malloc
利用chunk
结构体来管理这些内存块,内存池就是由许多不同大小的chunk
链表组成的。
内存池保存在bins
这个长128
的数组中,每个元素都是一个双向链表。其中:
bins[0]
目前没有使用。bins[1]
的链表称为unsorted_list
,用于维护free
释放的chunk
。bins[2, 63)
的区间称为small_bins
,用于维护<512
字节的内存块,其中每个元素对应的链表中的chunk
大小相同,均为index*8
。bins[64,127)
称为large_bins
,用于维护>512
字节的内存块,每个元素对应的链表中的chunk
大小不同,index
越大,链表中chunk
的内存大小相差越大,例如: 下标为64
的chunk
大小介于[512, 512+64)
,下标为95
的chunk
大小介于[2k+1, 2k+512)
。同一条链表上的chunk
,按照从小到大的顺序排列。
结构体malloc_chunk
来描述这些块:
1 | struct malloc_chunk |
glibc
在内存池中查找合适的chunk
时,采用了最佳适应的伙伴算法。举例如下:
- 如果分配内存
< 512
字节,则通过内存大小定位到smallbins
对应的index
上。- 如果
smallbins[index]
为空,进入步骤3
- 如果
smallbins[index]
非空,直接返回第一个chunk
- 如果
- 如果分配内存
> 512
字节,则定位到largebins
对应的index
上。- 如果
largebins[index]
为空,进入步骤3
- 如果
largebins[index]
非空,扫描链表,找到第一个大小最合适的chunk
,如size=12.5K
,则使用chunk B
,剩下的0.5k
放入unsorted_list
中
- 如果
- 遍历
unsorted_list
,查找合适size
的chunk
,如果找到则返回;否则,将这些chunk
都归类放到smallbins
和largebins
里面。 index++
从更大的链表中查找,直到找到合适大小的chunk
为止,找到后将chunk
拆分,并将剩余的加入到unsorted_list
中。- 如果还没有找到,那么使用
top chunk
。
top chunk
如下图示: top chunk
是堆顶的chunk
,堆顶指针brk
位于top chunk
的顶部。移动brk
指针,即可扩充top chunk
的大小。
free
释放内存时,有两种情况:
chunk
和top chunk
相邻,则和top chunk
合并。chunk
和top chunk
不相邻,则直接插入到unsorted_list
中。
参考链接:
自己实现一个简单的
malloc
可参考[malloc的底层实现(ptmalloc)_牛客博客