Linux | 堆内存管理

进程的地址空间
如何查看Linux进程的地址空间?
答:
- pmap
- /proc/$PID/maps
/proc文件系统
动态内核信息: /proc 是一个虚拟文件系统,主要提供内核和正在运行的进程的信息。它不是存储在磁盘上的真实文件,而是在运行时动态生成的。
- 进程信息: 每个正在运行的进程在 /proc 中都有一个以其 PID 命名的目录,里面包含了该进程的状态、内存、文件描述符等详细信息。
- 系统参数: /proc 中包含许多系统运行时参数,例如 /proc/cpuinfo(CPU 信息)、/proc/meminfo(内存信息)、/proc/uptime(系统运行时间)等。
pmap 是一个用于显示进程内存映射情况的工具,pmap 的实现主要依赖于 Linux 内核提供的/proc 文件系统包括读取读取/proc/<pid>/maps或读取 /proc/<pid>/smaps(包含更详细的内存统计,该文件提供了每个内存区域的内存使用情况,包括私有内存、共享内存、脏页、换页信息等。)
pmap $PID
输出案例
33498: ./test
000059cef6580000 4K r---- test
000059cef6581000 4K r-x-- test
000059cef6582000 4K r---- test
000059cef6583000 4K r---- test
000059cef6584000 4K rw--- test
000059cf32a73000 132K rw--- [ anon ]
0000734aa0e00000 102404K rw--- [ anon ]
0000734aa7400000 160K r---- libc.so.6
0000734aa7428000 1620K r-x-- libc.so.6
0000734aa75bd000 352K r---- libc.so.6
0000734aa7615000 4K ----- libc.so.6
0000734aa7616000 16K r---- libc.so.6
0000734aa761a000 8K rw--- libc.so.6
0000734aa761c000 52K rw--- [ anon ]
0000734aa7652000 12K rw--- [ anon ]
0000734aa7669000 8K rw--- [ anon ]
0000734aa766b000 8K r---- ld-linux-x86-64.so.2
0000734aa766d000 168K r-x-- ld-linux-x86-64.so.2
0000734aa7697000 44K r---- ld-linux-x86-64.so.2
0000734aa76a3000 8K r---- ld-linux-x86-64.so.2
0000734aa76a5000 8K rw--- ld-linux-x86-64.so.2
00007ffdb8f1e000 136K rw--- [ stack ]
00007ffdb8ffa000 16K r---- [ anon ]
00007ffdb8ffe000 8K r-x-- [ anon ]
ffffffffff600000 4K --x-- [ anon ]
total 105188K
当然可以直接cat /proc/$PID/maps,会看到一些更原始的输出:
59cef6580000-59cef6581000 r--p 00000000 08:03 1835085 /home/cilinmengye/tmp/test
59cef6581000-59cef6582000 r-xp 00001000 08:03 1835085 /home/cilinmengye/tmp/test
59cef6582000-59cef6583000 r--p 00002000 08:03 1835085 /home/cilinmengye/tmp/test
59cef6583000-59cef6584000 r--p 00002000 08:03 1835085 /home/cilinmengye/tmp/test
59cef6584000-59cef6585000 rw-p 00003000 08:03 1835085 /home/cilinmengye/tmp/test
59cf32a73000-59cf32a94000 rw-p 00000000 00:00 0 [heap]
734aa0e00000-734aa7201000 rw-p 00000000 00:00 0
734aa7400000-734aa7428000 r--p 00000000 08:03 395403 /usr/lib/x86_64-linux-gnu/libc.so.6
734aa7428000-734aa75bd000 r-xp 00028000 08:03 395403 /usr/lib/x86_64-linux-gnu/libc.so.6
734aa75bd000-734aa7615000 r--p 001bd000 08:03 395403 /usr/lib/x86_64-linux-gnu/libc.so.6
734aa7615000-734aa7616000 ---p 00215000 08:03 395403 /usr/lib/x86_64-linux-gnu/libc.so.6
734aa7616000-734aa761a000 r--p 00215000 08:03 395403 /usr/lib/x86_64-linux-gnu/libc.so.6
734aa761a000-734aa761c000 rw-p 00219000 08:03 395403 /usr/lib/x86_64-linux-gnu/libc.so.6
734aa761c000-734aa7629000 rw-p 00000000 00:00 0
734aa7652000-734aa7655000 rw-p 00000000 00:00 0
734aa7669000-734aa766b000 rw-p 00000000 00:00 0
734aa766b000-734aa766d000 r--p 00000000 08:03 395381 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
734aa766d000-734aa7697000 r-xp 00002000 08:03 395381 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
734aa7697000-734aa76a2000 r--p 0002c000 08:03 395381 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
734aa76a3000-734aa76a5000 r--p 00037000 08:03 395381 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
734aa76a5000-734aa76a7000 rw-p 00039000 08:03 395381 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7ffdb8f1e000-7ffdb8f40000 rw-p 00000000 00:00 0 [stack]
7ffdb8ffa000-7ffdb8ffe000 r--p 00000000 00:00 0 [vvar]
7ffdb8ffe000-7ffdb9000000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]
每列从左到右的含义为:
- 地址范围
- 权限标识:权限部分有四个字符,典型的形式如 r--p、r-xp、rw-p:
- r:可读(readable)。
- w:可写(writable)。
- x:可执行(executable)。
- -:相应的权限缺失。
- p 表示私有(private,即写时复制),s 表示共享。
- 偏移量:紧接在权限后面的字段(例如 00000000、00001000)表示该段内存在对应映射文件中的偏移量(以 16 进制计)。
比如59cef6580000-59cef6581000这段虚拟内存空间的内容来源于(或者说对应于映射文件)``/home/cilinmengye/tmp/test+偏移量00000000`开始 - 设备号:类似 08:03 的字段表示设备的主设备号和次设备号。
- inode 号:例如 1835094 表示文件系统中该映射文件的 inode 编号。inode 为 0 表示这段映射不是由文件提供的(如匿名映射、堆、栈)。
- 映射文件或标签:
- 如果是某个文件(如 /home/cilinmengye/tmp/test2、/usr/lib/x86_64-linux-gnu/libc.so.6 等),说明该内存区域映射自该文件。
- [heap]、[stack]、[vdso]、[vvar]、[vsyscall] 等标识显示特殊用途的内存区域。
- [ anon ] 表示匿名映射,即不是直接从磁盘文件映射而来的内存区域。
如何管理Linux进制的地址空间
malloc()/free()库函数, 它们的作用是在用户程序的堆区中申请/释放一块内存区域. 堆区的使用情况是由libc来进行管理的, 但堆区的大小却需要通过系统调用向操作系统提出更改.
调整堆区大小是通过sbrk()库函数来实现的, 它的原型是:
void* sbrk(intptr_t increment);
用于将用户程序的program break增长increment字节, 其中increment可为负数.
所谓program break, 就是用户程序的数据段(data segment)结束的位置. 我们知道可执行文件里面有代码段和数据段, 链接的时候ld会默认添加一个名为_end的符号, 来指示程序的数据段结束的位置. 用户程序开始运行的时候, program break会位于_end所指示的位置, 意味着此时堆区的大小为0.
malloc()被第一次调用的时候, 会通过sbrk(0)来查询用户程序当前program break的位置, 之后就可以通过后续的sbrk()调用来动态调整用户程序program break的位置了.
当前program break和和其初始值之间的区间就可以作为用户程序的堆区, 由malloc()/free()进行管理. 注意用户程序不应该直接使用sbrk(), 否则将会扰乱malloc()/free()对堆区的管理记录.
mmap
现代 libc(例如 glibc)的 malloc 在分配大块内存时,会调用mmap,而不是依赖于 sbrk 来扩展堆。此时分配到的内存区域会显示为匿名映射([ anon ]),而不会标记为 “[ heap ]”, 此时分配的内存空间在Memory Map Segment

在状态机(进程)状态上增加/删除/修改一段可访问的内存:

实验分析
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
printf("Press Enter to allocate memory...\n");
getchar();
void *heap_mem = malloc(100 * 1024 * 1024); // 分配 100MB 内存
if (!heap_mem) {
perror("malloc failed");
return 1;
}
printf("Allocated 100MB on heap. Press Enter to exit...\n");
getchar();
free(heap_mem);
return 0;
}
运行上述代码,使用strace(sudo strace -p $PID)和pmap分析在malloc之前和之后进程的地址空间变化:
运行malloc之前:
4414: ./test
0000564aa69b2000 4K r---- test
0000564aa69b3000 4K r-x-- test
0000564aa69b4000 4K r---- test
0000564aa69b5000 4K r---- test
0000564aa69b6000 4K rw--- test
0000564aaa120000 132K rw--- [ anon ]
000076a64e200000 160K r---- libc.so.6
000076a64e228000 1620K r-x-- libc.so.6
000076a64e3bd000 352K r---- libc.so.6
000076a64e415000 4K ----- libc.so.6
000076a64e416000 16K r---- libc.so.6
000076a64e41a000 8K rw--- libc.so.6
000076a64e41c000 52K rw--- [ anon ]
000076a64e4dd000 12K rw--- [ anon ]
000076a64e4f4000 8K rw--- [ anon ]
000076a64e4f6000 8K r---- ld-linux-x86-64.so.2
000076a64e4f8000 168K r-x-- ld-linux-x86-64.so.2
000076a64e522000 44K r---- ld-linux-x86-64.so.2
000076a64e52e000 8K r---- ld-linux-x86-64.so.2
000076a64e530000 8K rw--- ld-linux-x86-64.so.2
00007ffe93cac000 136K rw--- [ stack ]
00007ffe93dc1000 16K r---- [ anon ]
00007ffe93dc5000 8K r-x-- [ anon ]
ffffffffff600000 4K --x-- [ anon ]
total 2784K
可以看到初始给我们分配了132K的堆,000076a64e200000~000076a64e530000为Memory Map Segment
堆和.bss之间是有空隙的, 可见他们的地址分别为0000564aaa120000, 0000564aa69b6000
运行malloc之后:
# strace 输出
mmap(NULL, 104861696, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7c0f54e00000
...
munmap(0x7c0f54e00000, 104861696) = 0
exit_group(0) = ?
# pmap输出
0000564aa69b2000 4K r---- test
0000564aa69b3000 4K r-x-- test
0000564aa69b4000 4K r---- test
0000564aa69b5000 4K r---- test
0000564aa69b6000 4K rw--- test
0000564aaa120000 132K rw--- [ anon ]
000076a647c00000 102404K rw--- [ anon ]
000076a64e200000 160K r---- libc.so.6
000076a64e228000 1620K r-x-- libc.so.6
000076a64e3bd000 352K r---- libc.so.6
000076a64e415000 4K ----- libc.so.6
000076a64e416000 16K r---- libc.so.6
000076a64e41a000 8K rw--- libc.so.6
000076a64e41c000 52K rw--- [ anon ]
000076a64e4dd000 12K rw--- [ anon ]
000076a64e4f4000 8K rw--- [ anon ]
000076a64e4f6000 8K r---- ld-linux-x86-64.so.2
000076a64e4f8000 168K r-x-- ld-linux-x86-64.so.2
000076a64e522000 44K r---- ld-linux-x86-64.so.2
000076a64e52e000 8K r---- ld-linux-x86-64.so.2
000076a64e530000 8K rw--- ld-linux-x86-64.so.2
00007ffe93cac000 136K rw--- [ stack ]
00007ffe93dc1000 16K r---- [ anon ]
00007ffe93dc5000 8K r-x-- [ anon ]
ffffffffff600000 4K --x-- [ anon ]
total 105188K
注意:malloc 在分配大块内存时,会调用mmap,且分配的内存空间在Memory Map Segment。正如上述000076a647c00000 102404K rw--- [ anon ]
子进程的地址空间在哪?
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
void* threadFunc(void *arg)
{
printf("Before malloc in thread 1\n");
getchar();
char* addr = (char*)malloc(1000);
printf("After malloc and before free in thread 1\n");
getchar();
free(addr);
printf("After free in thread 1\n");
getchar();
}
int main()
{
pthread_t t1;
void *s;
int ret;
char* addr;
printf("Welcome to per thread arena example: %d\n", getpid());
printf("Before malloc in main thread\n");
getchar();
addr = (char*) malloc(1000);
printf("After malloc and before free in main thread\n");
getchar();
free(addr);
printf("After free in main thread\n");
getchar();
ret = pthread_create(&t1, NULL, threadFunc, NULL);
if (ret)
{
printf("Thread creation error\n");
return -1;
}
ret = pthread_join(t1, &s);
if (ret)
{
printf("THread join error\n");
return -1;
}
return 0;
}
# main thread before malloc
41981: ./test2
000062d6816fa000 4K r---- test2
000062d6816fb000 4K r-x-- test2
000062d6816fc000 4K r---- test2
000062d6816fd000 4K r---- test2
000062d6816fe000 4K rw--- test2
000062d6b5e8e000 132K rw--- [ anon ]
00007d6087800000 160K r---- libc.so.6
00007d6087828000 1620K r-x-- libc.so.6
00007d60879bd000 352K r---- libc.so.6
00007d6087a15000 4K ----- libc.so.6
00007d6087a16000 16K r---- libc.so.6
00007d6087a1a000 8K rw--- libc.so.6
00007d6087a1c000 52K rw--- [ anon ]
00007d6087aaf000 12K rw--- [ anon ]
00007d6087ac6000 8K rw--- [ anon ]
00007d6087ac8000 8K r---- ld-linux-x86-64.so.2
00007d6087aca000 168K r-x-- ld-linux-x86-64.so.2
00007d6087af4000 44K r---- ld-linux-x86-64.so.2
00007d6087b00000 8K r---- ld-linux-x86-64.so.2
00007d6087b02000 8K rw--- ld-linux-x86-64.so.2
00007ffc168e3000 136K rw--- [ stack ]
00007ffc1697e000 16K r---- [ anon ]
00007ffc16982000 8K r-x-- [ anon ]
ffffffffff600000 4K --x-- [ anon ]
total 2784K
# main thread after malloc
41981: ./test2
000062d6816fa000 4K r---- test2
000062d6816fb000 4K r-x-- test2
000062d6816fc000 4K r---- test2
000062d6816fd000 4K r---- test2
000062d6816fe000 4K rw--- test2
000062d6b5e8e000 132K rw--- [ anon ]
00007d6087800000 160K r---- libc.so.6
00007d6087828000 1620K r-x-- libc.so.6
00007d60879bd000 352K r---- libc.so.6
00007d6087a15000 4K ----- libc.so.6
00007d6087a16000 16K r---- libc.so.6
00007d6087a1a000 8K rw--- libc.so.6
00007d6087a1c000 52K rw--- [ anon ]
00007d6087aaf000 12K rw--- [ anon ]
00007d6087ac6000 8K rw--- [ anon ]
00007d6087ac8000 8K r---- ld-linux-x86-64.so.2
00007d6087aca000 168K r-x-- ld-linux-x86-64.so.2
00007d6087af4000 44K r---- ld-linux-x86-64.so.2
00007d6087b00000 8K r---- ld-linux-x86-64.so.2
00007d6087b02000 8K rw--- ld-linux-x86-64.so.2
00007ffc168e3000 136K rw--- [ stack ]
00007ffc1697e000 16K r---- [ anon ]
00007ffc16982000 8K r-x-- [ anon ]
ffffffffff600000 4K --x-- [ anon ]
total 2784K
这里主进程只进行了小额malloc分配, 在malloc前后实际上pmap的输出结果是一样的。
我们初始就有132K的堆,这 132KB 的堆空间叫做arena,此时因为是主线程分配的,所以叫做 main arena(每个 arena 中含有多个 chunk,这些 chunk 以链表的形式加以组织)。
由于 132KB 比 1000 bytes 大很多,所以主线程后续再申请堆空间的话,就会先从这 132KB 的剩余部分中申请,直到用完或不够用的时候,再通过增加 program break location 的方式来增加 main arena 的大小。
使用strace跟踪这段代码,也会发现并没有sbrk和mmap之类的系统调用发生。
之后子进程被创建了,在这个过程中strace捕捉到了:mmap(NULL, 8392704, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0) = 0x7d6086e00000
pmap输出为:
41981: ./test2
000062d6816fa000 4K r---- test2
000062d6816fb000 4K r-x-- test2
000062d6816fc000 4K r---- test2
000062d6816fd000 4K r---- test2
000062d6816fe000 4K rw--- test2
000062d6b5e8e000 132K rw--- [ anon ]
00007d6086e00000 4K ----- [ anon ]
00007d6086e01000 8192K rw--- [ anon ]
00007d6087800000 160K r---- libc.so.6
00007d6087828000 1620K r-x-- libc.so.6
00007d60879bd000 352K r---- libc.so.6
00007d6087a15000 4K ----- libc.so.6
00007d6087a16000 16K r---- libc.so.6
00007d6087a1a000 8K rw--- libc.so.6
00007d6087a1c000 52K rw--- [ anon ]
00007d6087aaf000 12K rw--- [ anon ]
00007d6087ac6000 8K rw--- [ anon ]
00007d6087ac8000 8K r---- ld-linux-x86-64.so.2
00007d6087aca000 168K r-x-- ld-linux-x86-64.so.2
00007d6087af4000 44K r---- ld-linux-x86-64.so.2
00007d6087b00000 8K r---- ld-linux-x86-64.so.2
00007d6087b02000 8K rw--- ld-linux-x86-64.so.2
00007ffc168e3000 136K rw--- [ stack ]
00007ffc1697e000 16K r---- [ anon ]
00007ffc16982000 8K r-x-- [ anon ]
ffffffffff600000 4K --x-- [ anon ]
total 10980K
注意这段内容:
00007d6086e00000 4K ----- [ anon ]
00007d6086e01000 8192K rw--- [ anon ]
地址在Memory Map Segment,且大小为\(8192 * 1024 + 4 * 1024 = 8392704\) ,这段空间正是子进程的空间!Linux 子线程的地址空间是由 mmap 创建的
在子进程malloc之后:
41981: ./test2
000062d6816fa000 4K r---- test2
000062d6816fb000 4K r-x-- test2
000062d6816fc000 4K r---- test2
000062d6816fd000 4K r---- test2
000062d6816fe000 4K rw--- test2
000062d6b5e8e000 132K rw--- [ anon ]
00007d6080000000 132K rw--- [ anon ]
00007d6080021000 65404K ----- [ anon ]
00007d6086e00000 4K ----- [ anon ]
00007d6086e01000 8192K rw--- [ anon ]
00007d6087800000 160K r---- libc.so.6
00007d6087828000 1620K r-x-- libc.so.6
00007d60879bd000 352K r---- libc.so.6
00007d6087a15000 4K ----- libc.so.6
00007d6087a16000 16K r---- libc.so.6
00007d6087a1a000 8K rw--- libc.so.6
00007d6087a1c000 52K rw--- [ anon ]
00007d6087aaf000 12K rw--- [ anon ]
00007d6087ac6000 8K rw--- [ anon ]
00007d6087ac8000 8K r---- ld-linux-x86-64.so.2
00007d6087aca000 168K r-x-- ld-linux-x86-64.so.2
00007d6087af4000 44K r---- ld-linux-x86-64.so.2
00007d6087b00000 8K r---- ld-linux-x86-64.so.2
00007d6087b02000 8K rw--- ld-linux-x86-64.so.2
00007ffc168e3000 136K rw--- [ stack ]
00007ffc1697e000 16K r---- [ anon ]
00007ffc16982000 8K r-x-- [ anon ]
ffffffffff600000 4K --x-- [ anon ]
total 76516K
变化主要在:
00007d6080000000 132K rw--- [ anon ]
00007d6080021000 65404K ----- [ anon ]
00007d6086e00000 4K ----- [ anon ]
00007d6086e01000 8192K rw--- [ anon ]

浙公网安备 33010602011771号