关于malloc的一些想法

时间:2022-10-10 17:58:39

之前一段时间翻阅过一些内存分配的资料,这次终于能将其整理记录下来了。

c标准库里有两个耳熟能详的函数,用于对堆空间的内存进行分配和释放,它们分别是:

  1. malloc。负责分配一个指定大小的一块内存给调用的程序,函数返回一个指向这块内存的指针。
  2. free。对函数参数指向的内存块进行释放操作。

需要说明的是,不管是malloc还是free,这些函数都是c标准库提供给我们的,而不是操作系统的API。对于堆上的内存管理,操作系统(Linux)提供以下两个接口:

  1. sbrk。用于扩张和收缩堆,本质上就是移动指向堆顶的指针。把指针向高地址移动就是在扩张堆、向低地址移动就是在收缩堆。
  2. mmap、munmap。用于内存映射和取消内存映射,对于大块的内存申请,直接调用mmap申请一个内存段更加高效。

显然,malloc的实现是基于操作系统提供的API的,也就是上面提到的那两个syscall。而且,一般考虑到会频繁的进行内存分配,我们往往希望能尽量减少syscall的次数。于是,通常的策略是如下。

一般地,实现会在内部维持了一个空闲内存块的链表,当我们通过malloc申请一块内存,我们先在这个链表里进行搜索,如果找到符合大小的内存块,就将这个内存块返回给调用程序。否则,当在空闲链表中搜索不到满足要求的内存块,说明当前堆的大小不够用了,调用syscall以扩张堆的大小,并重新搜索内存块。当通过free释放内存的时候,则将内存块返回给这个空闲链表。空闲链表可能会有一些策略,用来在未来的某一个时候,通过syscall将这些空闲的内存重新归还给操作系统。

不同的malloc/free的实现,主要是在如何组织这个空闲内存块的链表上有所不同。关于glibc的malloc可以参考这篇文章:Glibc内存管理

在清楚malloc内部做了什么之后,就可以搞明白一些诡异的问题了。比如说,重现引用一块已经free掉的内存,为什么有时候会报段错误,有时候会崩溃,有时候什么事都没发生?

首先要说明的是操作系统的内存管理是段页式的,既分段也分页。而通常一个程序的内存段有:代码段、静态数据全局数据段、堆、栈等,而程序所能访问的地址必须是在OS已经分配的内存段中。如果访问的地址不在已存在的段内,就会报我们熟悉的段错误。当通过free将内存释放时,分配程序有可能为了重新利用这块内存而缓存着它,也有可能将这块内存真正的归还给操作系统。如果内存块被真正的归还给操作系统,比如free操作导致了堆的收缩,或是在free中调用了munmap取消了一个内存映射。这时候如果有一个指针指向这块内存地址,对它引用就会导致段错误。

不过大多数情况下,free掉的内存块不是立即归还给OS,而是被分配程序(malloc)插入到空闲内存块链表中缓存起来,等待再次被利用。有的实现(例如glibc的ptmalloc)为了内存空间的复用,会在这个内存块里面记录下指向链表前后节点的指针等信息。这时候如果我们对free掉的内存块重新执行写操作就有可能会改写这部分信息,导致这个内存块被写坏,破坏了空闲链表的结构,因此就有可能引起崩溃。