最近在做 MySQL 版本升级时( 5.1->5.5 ) , 发现了 mysqld 疑似“内存泄露”现象,但通过 valgrind 等工具检测后,并没发现类似的问题。因此,需要深入学习 Linux 的虚拟内存管理方面的内容来解释这个现象。
基于以上认识,这篇文章通过本人以前对虚拟内存管理的疑惑由浅入深整理了以下十个问题,并通过例子和系统命令尝试进行解答。
Linux 使用虚拟地址空间,大大增加了进程的寻址空间,由低地址到高地址分别为:
下图是 32 位系统典型的虚拟地址空间分布(来自《深入理解计算机系统》)。
32 位系统有 4G 的地址空间,其中0x08048000~0xbfffffff
是用户空间, 0xc0000000~0xffffffff
是内核空间,包括内核代码和数据、与进程相关的数据结构(如页表、内核栈)等。另外, %esp 执行栈顶,往低地址方向变化; brk/sbrk 函数控制堆顶往高地址方向变化。
可通过以下代码验证进程的地址空间分布,其中 sbrk(0) 函数用于返回栈顶指针。
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int global_num = 0;
char global_str_arr [65536] = {'a'};
int main(int argc, char** argv)
{
char* heap_var = NULL;
int local_var = 0;
printf("Address of function main 0x%lx\n", main);
printf("Address of global_num 0x%lx\n", &global_num);
printf("Address of global_str_arr 0x%lx ~ 0x%lx\n", &global_str_arr[0], &global_str_arr[65535]);
printf("Top of stack is 0x%lx\n", &local_var);
printf("Top of heap is 0x%lx\n", sbrk(0));
heap_var = malloc(sizeof(char) * 127 * 1024);
printf("Address of heap_var is 0x%lx\n", heap_var);
printf("Top of heap after malloc is 0x%lx\n", sbrk(0));
free(heap_var);
heap_var = NULL;
printf("Top of heap after free is 0x%lx\n", sbrk(0));
return 1;
}
32 位系统的结果如下,与上图的划分保持一致,并且栈顶指针在 mallloc 和 free 一个 127K 的存储空间时都发生了变化(增大和缩小)。
Address of function main 0x8048474
Address of global_num 0x8059904
Address of global_str_arr 0x8049900 ~ 0x80598ff
Top of stack is 0xbfd0886c
Top of heap is 0x805a000
Address of heap_var is 0x805a008
Top of heap after malloc is 0x809a000
Top of heap after free is 0x807b000
但是, 64 位系统结果怎样呢? 64 位系统是否拥有 2^64 的地址空间吗?
64 位系统运行结果如下:
Address of function main 0x400594
Address of global_num 0x610b90
Address of global_str_arr 0x600b80 ~ 0x610b7f
Top of stack is 0x7fff2e9e4994
Top of heap is 0x8f5000
Address of heap_var is 0x8f5010
Top of heap after malloc is 0x935000
Top of heap after free is 0x916000
从结果知,与上图的分布并不一致。而事实上, 64 位系统的虚拟地址空间划分发生了改变:
0x0000000000000000~0x00007fffffffffff
表示用户空间,0xFFFF800000000000~ 0xFFFFFFFFFFFFFFFF
表示内核空间,共提供 256TB(2^48) 的寻址空间。这两个区间的特点是,第 47 位与 48~63 位相同,若这些位为 0 表示用户空间,否则表示内核空间。malloc 是 glibc 中内存分配函数,也是最常用的动态内存分配函数,其内存必须通过 free 进行释放,否则导致内存泄露。
关于 malloc 获得虚存空间的实现,与 glibc 的版本有关,但大体逻辑是:
其中 sbrk 就是修改栈顶指针位置,而 mmap 可用于生成文件的映射以及匿名页面的内存,这里指的是匿名页面。
而这个 128k ,是 glibc 的默认配置,可通过函数 mallopt 来设置,可通过以下例子说明。
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>
#include <malloc.h>
void print_info(
char* var_name,
char* var_ptr,
size_t size_in_kb
)
{
printf("Address of %s(%luk) 0x%lx, now heap top is 0x%lx\n",
var_name, size_in_kb, var_ptr, sbrk(0));
}
int main(int argc, char** argv)
{
char *heap_var1, *heap_var2, *heap_var3 ;
char *mmap_var1, *mmap_var2, *mmap_var3 ;
char *maybe_mmap_var;
printf("Orginal heap top is 0x%lx\n", sbrk(0));
heap_var1 = malloc(32*1024);
print_info("heap_var1", heap_var1, 32);
heap_var2 = malloc(64*1024);
print_info("heap_var2", heap_var2, 64);
heap_var3 = malloc(127*1024);
print_info("heap_var3", heap_var3, 127);
printf("\n");
maybe_mmap_var = malloc(128*1024);
print_info("maybe_mmap_var", maybe_mmap_var, 128);
//mmap
mmap_var1 = malloc(128*1024);
print_info("mmap_var1", mmap_var1, 128);
// set M_MMAP_THRESHOLD to 64k
mallopt(M_MMAP_THRESHOLD, 64*1024);
printf("set M_MMAP_THRESHOLD to 64k\n");
mmap_var2 = malloc(64*1024);
print_info("mmap_var2", mmap_var2, 64);
mmap_var3 = malloc(127*1024);
print_info("mmap_var3", mmap_var3, 127);
return 1;
}
这个例子很简单,通过 malloc 申请多个不同大小的动态内存,同时通过接口 print_info 打印变量大小和地址等相关信息,其中 sbrk(0) 可返回堆顶指针位置。另外,粗体部分是将 MMAP 分配的临界点由 128k 转为 64k ,再打印变量地址的不同。
下面是 Linux 64 位机器的执行结果(后文所有例子都是通过 64 位机器上的测试结果)。
Orginal heap top is 0x17da000
Address of heap_var1(32k) 0x17da010, now heap top is 0x1803000
Address of heap_var2(64k) 0x17e2020, now heap top is 0x1803000
Address of heap_var3(127k) 0x17f2030, now heap top is 0x1832000
Address of maybe_mmap_var(128k) 0x1811c40, now heap top is 0x1832000
Address of mmap_var1(128k) 0x7f4a0b1f2010, now heap top is 0x1832000
set M_MMAP_THRESHOLD to 64k
Address of mmap_var2(64k) 0x7f4a0b1e1010, now heap top is 0x1832000
Address of mmap_var3(127k) 0x7f4a0b1c1010, now heap top is 0x1832000
我们知道, malloc 分配的的内存是虚拟地址空间,而虚拟地址空间和物理地址空间使用进程页表进行映射,那么分配了空间就是占用物理内存空间了吗?
首先,进程使用多少内存可通过 ps aux 命令 查看,其中关键的两信息(第五、六列)为:
可通过一个例子说明这个问题:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>
#include <malloc.h>
char ps_cmd[1024];
void print_info(
char* var_name,
char* var_ptr,
size_t size_in_kb
)
{
printf("Address of %s(%luk) 0x%lx, now heap top is 0x%lx\n",
var_name, size_in_kb, var_ptr, sbrk(0));
system(ps_cmd);
}
int main(int argc, char** argv)
{
char *non_set_var, *set_1k_var, *set_5k_var, *set_7k_var;
pid_t pid;
pid = getpid();
sprintf(ps_cmd, "ps aux | grep %lu | grep -v grep", pid);
non_set_var = malloc(32*1024);
print_info("non_set_var", non_set_var, 32);
set_1k_var = malloc(64*1024);
memset(set_1k_var, 0, 1024);
print_info("set_1k_var", set_1k_var, 64);
set_5k_var = malloc(127*1024);
memset(set_5k_var, 0, 5*1024);
print_info("set_5k_var", set_5k_var, 127);
set_7k_var = malloc(64*1024);
memset(set_1k_var, 0, 7*1024);
print_info("set_7k_var", set_7k_var, 64);
return 1;
}
该代码扩展了上一个例子print_info
能力,处理打印变量信息,同时通过 ps aux 命令获得当前进程的 VSZ 和 RSS 值。并且程序 malloc 一块内存后,会 memset 内存的若干 k 内容。
执行结果为
Address of non_set_var(32k) 0x502010, now heap top is 0x52b000
mysql 12183 0.0 0.0 2692 452 pts/3 S+ 20:29 0:00 ./test_vsz
Address of set_1k_var(64k) 0x50a020, now heap top is 0x52b000
mysql 12183 0.0 0.0 2692 456 pts/3 S+ 20:29 0:00 ./test_vsz
Address of set_5k_var(127k) 0x51a030, now heap top is 0x55a000
mysql 12183 0.0 0.0 2880 464 pts/3 S+ 20:29 0:00 ./test_vsz
Address of set_7k_var(64k) 0x539c40, now heap top is 0x55a000
mysql 12183 0.0 0.0 2880 472 pts/3 S+ 20:29 0:00 ./test_vsz
由以上结果知:
进程地址空间被分为了代码段、数据段、堆、文件映射区域、栈等区域,那怎么查询这些虚拟地址空间的使用情况呢?
Linux 提供了 pmap 命令来查看这些信息,通常使用 pmap -d $pid
(高版本可提供 pmap -x $pid
)查询,如下所示:
mysql@ TLOG_590_591:~/vin/test_memory> pmap -d 17867
17867: test_mmap
START SIZE RSS DIRTY PERM OFFSET DEVICE MAPPING
00400000 8K 4K 0K r-xp 00000000 08:01 /home/mysql/vin/test_memory/test_mmap
00501000 68K 8K 8K rw-p 00001000 08:01 /home/mysql/vin/test_memory/test_mmap
00512000 76K 0K 0K rw-p 00512000 00:00 [heap]
0053e000 256K 0K 0K rw-p 0053e000 00:00 [anon]
2b3428f97000 108K 92K 0K r-xp 00000000 08:01 /lib64/ld-2.4.so
2b3428fb2000 8K 8K 8K rw-p 2b3428fb2000 00:00 [anon]
2b3428fc1000 4K 4K 4K rw-p 2b3428fc1000 00:00 [anon]
2b34290b1000 8K 8K 8K rw-p 0001a000 08:01 /lib64/ld-2.4.so
2b34290b3000 1240K 248K 0K r-xp 00000000 08:01 /lib64/libc-2.4.so
2b34291e9000 1024K 0K 0K ---p 00136000 08:01 /lib64/libc-2.4.so
2b34292e9000 12K 12K 12K r--p 00136000 08:01 /lib64/libc-2.4.so
2b34292ec000 8K 8K 8K rw-p 00139000 08:01 /lib64/libc-2.4.so
2b34292ee000 1048K 36K 36K rw-p 2b34292ee000 00:00 [anon]
7fff81afe000 84K 12K 12K rw-p 7fff81afe000 00:00 [stack]
ffffffffff600000 8192K 0K 0K ---p 00000000 00:00 [vdso]
Total: 12144K 440K 96K
从这个结果可以看到进程虚拟地址空间的使用情况,包括起始地址、大小、实际使用内存、脏页大小、权限、偏移、设备和映射文件等。 pmap 命令就是基于下面两文件内容进行解析的:
/proc/$pid/maps
/proc/$pid/smaps
并且对于上述每个内存块区间,内核会使用一个 vm_area_struct 结构来维护,同时通过页面建立与物理内存的映射关系,如下图所示。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。