转载:https://cloud.tencent.com/developer/article/1173720
操作系统内存布局
各种malloc的内存分配管理方式离不开操作系统的内存布局策略。
32位经典内存布局
32位系统下经典内存布局如上,程序起始的1GB地址为内核空间,接下来是向下增长的栈空间和由0x40000000向上增长的mmap地址。而堆地址是从底部开始,去除ELF、数据段、代码段、常量段之后的地址并向上增长。但是这种布局有几个问题,首先是容易遭受溢出攻击;其次是,堆地址空间只有不到1G有木有?如果mmap内存比较少地址很浪费有木有?所以后来就有了另一种内存布局。
32位默认内存布局
这幅图描述地比较清楚也比较完整。首先是加入了几种Random offset随机偏移,导致内存溢出攻击不那么容易了,其次是堆仍然向上,但是mmap向下增长。但是这样的话栈空间就不是动态增长的了,所以现在的操作系统都有栈大小限制来着(Windows好像默认是2MB对吧?Linux可以ulimit –s查看)。这种内存布局地址利用度比较高。
64位内存布局
64位系统的寻址空间比较大,所以仍然沿用了32位的经典布局,但是加上了随机的mmap起始地址,以防止溢出攻击。反正一时半会是用不了这么大的内存地址了,所以至少N多年不会变了(话说要生产出40TB+的内存条,把堆内存地址用光,一时半会也搞不定吧)。
总结
纵观各种内存布局,对于大内存各种malloc基本上都是直接mmap的。而对于小数据,则通过向操作系统申请扩大堆顶,这时候操作系统会把需要的内存分页映射过来,然后再由这些malloc管理这些堆内存块,减少系统调用。而在free内存的时候,不同的malloc有不同的策略,不一定会把内存真正地还给系统,所以很多时候,如果访问了free掉的内存,并不会立即Run Time Error,只有访问的地址没有对应的内存分页,才会崩掉。
Ptmalloc
Ptmalloc采用主-从分配区的模式,当一个线程需要分配资源的时候,从链表中找到一个没加锁的分配区,在进行内存分配。
小内存分配
在ptmalloc内部,内存块采用chunk管理,并且将大小相似的chunk用链表管理,一个链表被称为一个bin。前64个bin里,相邻的bin内的chunk大小相差8字节,称为small bin,后面的是large bin,large bin里的chunk按先大小,再最近使用的顺序排列,每次分配都找一个最小的能够使用的chunk。
Chunk的结构如上所示,A位表示是不是在主分配区,M表示是不是mmap出来滴,P表示上一个内存紧邻的chunk是否在使用,如果没在使用,则size of previous chunk是上一个chunk的大小,否则无意义(而且被用作被分配出去的内存了),正式根据P标记位和size of previous chunk在free内存块的时候来进行chunk合并的。当然,如果chunk空闲,mem里还记录了一些指针用于索引临近大小的chunk的,实现原理就不复述了,知道大致作用就行。
在free的时候,ptmalloc会检查附近的chunk,并尝试把连续空闲的chunk合并成一个大的chunk,放到unstored bin里。但是当很小的chunk释放的时候,ptmalloc会把它并入fast bin中。同样,某些时候,fast bin里的连续内存块会被合并并加入到一个unsorted bin里,然后再才进入普通bin里。所以malloc小内存的时候,是先查找fast bin,再查找unsorted bin,最后查找普通的bin,如果unsorted bin里的chunk不合适,则会把它扔到bin里。
大内存分配
Ptmalloc的分配的内存顶部还有一个top chunk,如果前面的bin里的空闲chunk都不足以满足需要,就是尝试从top chunk里分配内存。如果top chunk里也不够,就要从操作系统里拿了。
还有就是特别大的内存,会直接从系统mmap出来,不受chunk管理,这样的内存在回收的时候也会munmap还给操作系统。
简而言之,就是:**
**小内存: [获取分配区(arena)并加锁] -> fast bin -> unsorted bin -> small bin -> large bin -> top chunk -> 扩展堆
大内存: 直接mmap
总结
释放的时候,几乎是和分配反过来,再加上可一些chunk合并和从一个bin转移到另一个bin的操作。并且如果顶部有足够大的空闲chunk,则收缩堆顶并还给操作系统。
介于此,对于ptmalloc的内存分配使用有几个注意事项:
- Ptmalloc默认后分配内存先释放,因为内存回收是从top chunk开始的。
- 避免多线程频繁分配和释放内存,会造成频繁加解锁。
- 不要分配长生命周期的内存块,容易造成内碎片,影响内存回收。