本博客所有的代码在github中。
1. 问题
我们的程序有几十个线程,每个线程拥有一个std::map,每个线程都要向自己的std::map中插入大量的数据,但每个数据只有几十字节;当使用完std::map,调用map.clear(),删除map里的所有元素,发现std::map所占内存没有返还给操作系统;甚至std::map析构后,内存仍然没有返还给操作系统(map析构不返还内存,不一定100%重现)。
了解了glibc malloc/free原理后,我设计了几个实验,目的是辅助理解。所有测试结果基于Red Hat Enterprise 7.0,glibc版本2.17。
1.1 实验--1
编译需要提前安装openssl, openssl-devel库rpm包。
编译:
[root@mydev-rosvile-redhat 0001]# sh build.sh
Complile mytest success
运行:
[root@mydev-rosvile-redhat 0001]# ./mytest map
----------------------------------------------------------------------------------------------
test_map() 1
At the beginning, map.size=0
Output of 'top':
26503 root 20 0 22900 1528 1172 S 0.0 0.0 0:00.00 mytest
----------------------------------------------------------------------------------------------
test_map() 2
Insert all FPs into std::map, map.size=500000, cost time = 1 seconds
Output of 'top':
26503 root 20 0 54052 32716 1280 S 0.0 0.4 0:00.80 mytest
-------------------------------------------------------------------------------------------------
test_map() 3
Lookup all FPs from std::map, map.size=500000, cost time = 1 seconds
-----------------------------------------------------------------------------------------------
test_map() 4
Delete all FPs from std::map, map.size=0, cost time = 0 seconds
Sleep 15 seconds, Output of 'top':
26503 root 20 0 54052 32928 1320 S 0.0 0.4 0:01.68 mytest
----------------------------------------------------------------------------------------------
test_map() 5
Now the process wil exit and die:
Output of 'top':
26503 root 20 0 54052 32928 1320 S 0.0 0.4 0:01.68 mytest
-----------------------------------------------------------------------------------------------
小提示:'top'输出的第6列表示某程序使用的物理内存大小。
在实验--1里:
1)我们向std::map插入500,000个数据 (插入数据代码) 来模拟我们的业务场景(一个md5值作为key,对应一个uint64_t值作为value)。
2)可以发现map.clear()删除数据后 (删除数据代码), 没有返还内存给操作系统(占用32928 KB)。
3)甚至map析构后 (map析构后), 仍然没有返还内存给操作系统(占用32928 KB)。 map析构不返还内存,不一定100%重现。
思考:
因为即使C++,其new/delete也是基于malloc/free的封装,这难道是glibc malloc/free的特点?实验--2和实验--3将为我们揭晓答案。
1.2 实验--2
运行:
root@mydev-rosvile-redhat 0001]# ./mytest malloc-free
----------------------------------------------------------------------------------------------
test_malloc_free 1
At the beginning:
Output of 'top':
3417 root 20 0 26692 1532 1168 S 0.0 0.0 0:00.00 mytest
----------------------------------------------------------------------------------------------
test_malloc_free 2
Malloc: number = 500000
Output of 'top':
3417 root 20 0 534496 513128 1220 S 0.0 6.4 0:00.43 mytest
----------------------------------------------------------------------------------------------
test_malloc_free 3
Free: number = 500000
Sleep 15 seconds, Output of 'top':
3417 root 20 0 26692 5620 1224 S 0.0 0.1 0:00.55 mytest
----------------------------------------------------------------------------------------------
Now the process wil exit and die:
Output of 'top':
3417 root 20 0 26692 5620 1224 S 0.0 0.1 0:00.55 mytest
-----------------------------------------------------------------------------------------------
在实验--2里:
1)我们用malloc分配一些内存空间(500,000个1KB),存入数据(全0),(分配空间,存入数据代码)。
2)用free释放了500,000个1KB内存空间后(释放空间代码) ; 可以发现free后,返还了内存给操作系统。
思考: 看来这个实验里,free立即把内存还给了操作系统。 在了解了glibc malloc/free的原理后,基于实验--2,我又设计了实验--3.
1.3 实验--3
[root@mydev-rosvile-redhat 0001]# ./mytest malloc-free-top-chunk
----------------------------------------------------------------------------------------------
test_malloc_free 1
At the beginning:
Output of 'top':
4017 root 20 0 26692 1536 1164 S 0.0 0.0 0:00.00 mytest
----------------------------------------------------------------------------------------------
test_malloc_free 2
Malloc: number = 500000
Output of 'top':
4017 root 20 0 534496 513136 1220 S 0.0 6.4 0:00.25 mytest
----------------------------------------------------------------------------------------------
test_malloc_free 3
Free: number = 500000
Sleep 15 seconds, Output of 'top':
4017 root 20 0 534496 513308 1224 S 0.0 6.4 0:00.35 mytest
----------------------------------------------------------------------------------------------
Now the process wil exit and die:
Output of 'top':
4017 root 20 0 534496 513308 1224 S 0.0 6.4 0:00.35 mytest
-----------------------------------------------------------------------------------------------
在实验--3里:
1)在free了500,000个1KB内存空间后(相关代码) ,内存并没有返还给操作系统(占用513308 KB)
2)在程序将要退出前(相关代码), 内存仍然没有返还给操作系统(占用513308 KB)
3)和实验--2唯一的不同是: 实验--3故意malloc了1 Byte空间,却不去释放,(1 Byte memory leak代码)。
思考:
这个行为和实验--1,map调用clear后,甚至map析构后,内存不返还给操作系统的行为是一样的。
为什么这1 Byte的故意的memory leak严重影响了free的行为?在第3节ptmalloc2中会展开讲述。
1.4 约定
因为用std::map做实验不够直观,所以后续大部分实验都直接基于malloc/free。
2. 内存分配基础知识
本章节部分节选自参考文献 1,淘宝工程师力作,有改动。
2.1 x86_64位下内存布局
上图是 X86_64 下 Linux 进程的默认内存布局形式,这只是一个示意图, 当前内核默认 配置下,进程的stack和 mmap映射区域并不是从一个固定地址开始,并且每次启动时的值都 不一样, 这是程序在启动时随机改变这些值的设置,使得使用缓冲区溢出进行攻击更加困难。可以用如下命令禁止该特性:sudo sysctl -w kernel.randomize_va_space=0
2.2 内存分配/释放的相关系统调用和函数
从2.1中我们知道,heap和mmap region都是提供给用户程序的虚拟内存空间,那么如何获得该区域的内存呢?
对heap的操作,Linux提供了brk()系统调用,另外glibc对brk()进行了封装,提供了sbrk()函数; 对mmap region,Linux提供了mmap()和munmap()系统调用。