浅谈内存管理
这里记录一下看书所得,应用程序的内存管理,即glibc的malloc和相关函数
内存管理的重要性
过去Linux通过默认的dlmalloc来申请内存,但不支持多线程,这导致的竞争和性能降低,而ptmalloc2支持多线程,也成为了Linux新的默认方式
ptmalloc2通过为每个线程单独维护一个帧,这样每个线程的申请和释放可以并行执行,但当线程过多时,线程也将共享帧。这种帧叫做per thread arena
主线程和线程的内存申请方式
主线程中使用brk来申请大块内存,其实brk不过是修改了寄存器的值。而且返回的大小通常要比malloc申请的多,如同测试中的在主线程中申请100个字节大小的内存,返回了132 KB。
1 | 55f7b402c000-55f7b404d000 rw-p 00000000 00:00 0 [heap] |
而在线程中,malloc使用mmap来申请内存,返回大小也比申请大小多,测试中的在线程中申请100字节,返回了64M的地址空间, 但只有8MB是可用的堆内存。
1 | 7fb738000000-7fb738021000 rw-p 00000000 00:00 0 |
主线程中申请的地址大于Data Section的地址,说明使用的brk,而后者申请时用了mmap,对比附录文档可以看出32位和64位区别比较大
arena
主线程和线程使用不同的arena,这样主线程和线程同时申请内存时不存在竞争。但不是所有的线程都独享一份arena, arena的个数限制如下
1 | For 32 bit systems: |
当线程个数太多时,会公用主线程的arena
main arena 和 per thread arena
main arena 指在主线程(main)中使用的,而后者是子线程中使用,两者有很大区别
主线程只有一块heap,所以没有heapinfo结构,而子线程有。主线程的Arena header是全局变量,所以存在于libc.so的Data Segment中
线程的arena含有多个heap, 每个heap都有heapinfo结构,多个heapinfo组成链表。线程的多个heap共享一个malloc_state 也称为Arena header
Arena header包含了bin、top chunk等信息
每个heap有多个chunk,每个chunk有个malloc_chunk结构,其管理chunk的状态和其在heap中的偏移大小,有以下几种状态
- Allocated chunk
- Free chunk
- Top chunk
- Last Remainder chunk
Top chunk 处于arena的顶部,在freelist没有合适大小的chunk时使用,如果Top chunk比申请的大,则又分为两部分(新Top chunk和用户申请的chunk)
Last Remainder chunk, 是当用户请求small chunk时,会先遍历small bin和Unsorted bin, 如果都无法满足,则遍历其他bin。
如果找到的chunk还有剩余,则这个chunk变为Last Remainder chunk,且将被加入到Unsorted bin
这样的好处是多次申请small chunk时,会先向Unsorted bin的Last Remainder chunk申请, 所以小内存块会集中分布在Last Remainder chunk,提高小块内存的局部性
bin
当malloc的内存被free后(仅仅是例子中的100字节chunk),并不会释放给内核,而是进入到glibc的bin中,也叫做freelist。
当下次申请内存时,会从bin中查找合适的chunk,而只有当没有合适的大小时,才向内核申请。
所以bin用来保存free过的chunk, 基于chunk的大小分为
- Fast bin
- Unsorted bin
- Small bin
- Large bin
但不同的bin都使用链表来管理这些chunk
测试方法
获取程序运行时的pid, 假设为949293, 那么通过以下命令可以获取程序的内存结构
1 | cat /proc/949632/maps |
- address 是起始地址-结束地址,差值为空间大小
- perm 是指权限,分为rwx[p,s], [p,s]指最后一位可以是private或者shared, 这些能通过
mprotect命令修改(c中的const实现) - offset 仅对mmap的区域有效,指映射偏移量
- pathname 如果此区域是映射的文件,则显示文件名。如果映射的是匿名映射则显示空白, 匿名映射包括线程通过mmap申请的堆内存,而[heap]是main函数的堆
所以通过计算address可以知道glibc申请的内存情况
malloc的返回值有意义吗
在我本机(gcc (Debian 10.2.1-6) 10.2.1 20210110 12GB)上测试了一段程序
- 如果一次性申请13GB,malloc返回失败
- 如果每次只申请2GB但不释放,第10次申请(20GB)malloc返回成功
- 如果每次只申请2GB但不释放且
访问,第三次申请(6GB)malloc返回成功,但访问时OOM
所以说,malloc的返回值是没有太大意义的
1 | void foo(size_t large) { |
其他
内存管理不仅存在应用程序(或者说glibc)中,Linux内核也有内存管理。也不只有glibc的内存malloc一种实现,还有
- dlmalloc – General purpose allocator
- ptmalloc2 – glibc
- jemalloc – FreeBSD 的libc实现, 相比其他第三方,这个比较流行
- tcmalloc – Google 为多线程场景开发
- libumem – Solaris
malloc是glibc对ptmalloc2的包装,而ptmalloc2是dlmalloc的重构,区别很大
最后总结,glibc的malloc通过两个系统调用函数brk和mmap来向内核申请内存,但内存释放后不会归还内核,而是放到freelist等待再次被利用
引用
https://man7.org/linux/man-pages/man5/proc.5.html
https://sploitfun.wordpress.com/2015/02/10/understanding-glibc-malloc/comment-page-1/
https://lemire.me/blog/2021/10/27/in-c-how-do-you-know-if-the-dynamic-allocation-succeeded/