十问 Linux 虚拟内存管理 (glibc)

Posted ty_laurel

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了十问 Linux 虚拟内存管理 (glibc)相关的知识,希望对你有一定的参考价值。

  最近在做 mysql 版本升级时( 5.1->5.5 ) , 发现了 mysqld 疑似“内存泄露”现象,但通过 valgrind 等工具检测后,并没发现类似的问题。因此,需要深入学习 Linux 的虚拟内存管理方面的内容来解释这个现象。 Linux 的虚拟内存管理有几个关键概念: 

1. 每个进程有独立的虚拟地址空间,进程访问的虚拟地址并不是真正的物理地址 
2. 虚拟地址可通过每个进程上页表与物理地址进行映射,获得真正物理地址 
3. 如果虚拟地址对应物理地址不在物理内存中,则产生缺页中断,真正分配物理地址,同时更新进程的页表;如果此时物理内存已耗尽,则根据内存替换算法淘汰部分页面至物理磁盘中。

基于以上认识,这篇文章通过本人以前对虚拟内存管理的疑惑由浅入深整理了以下十个问题,并通过例子和系统命令尝试进行解答。 
1. Linux 虚拟地址空间如何分布? 32 位和 64 位有何不同? 
2. malloc 是如何分配内存的? 
3. malloc 分配多大的内存,就占用多大的物理内存空间吗? 
4. 如何查看进程虚拟地址空间的使用情况? 
5. free 的内存真的释放了吗(还给 OS ) ? 
6. 程序代码中 malloc 的内存都有相应的 free ,就不会出现内存泄露了吗? 
7. 既然堆内内存不能直接释放,为什么不全部使用 mmap 来分配? 
8. 如何查看进程的缺页中断信息? 
9. 如何查看堆内内存的碎片情况? 
10. 除了 glibc 的 malloc/free ,还有其他第三方实现吗?

1.Linux 虚拟地址空间如何分布? 32 位和 64位有何不同?

  Linux 使用虚拟地址空间,大大增加了进程的寻址空间,由低地址到高地址分别为: 
只读段:该部分空间只能读,不可写,包括代码段、 rodata 段( C 常量字符串和 #define定义的常量) 
数据段:保存全局变量、静态变量的空间 
堆:就是平时所说的动态内存, malloc/new 大部分都来源于此。其中堆顶的位置可通过函数 brk 和 sbrk 进行动态调整。 
文件映射区域:如动态库、共享内存等映射物理空间的内存,一般是 mmap 函数所分配的虚拟地址空间。 
栈:用于维护函数调用的上下文空间,一般为 8M,可通过 ulimit –s ((kbytes, -s) 8192) 查看。 
内核虚拟空间:用户代码不可见的内存区域,由内核管理。

下图是 32 位系统典型的虚拟地址空间分布(来自《深入理解计算机系统》)。


32位系统典型虚拟地址空间分布 
32 位系统有 4G 的地址空间,其中 0x08048000~0xbfffffff 是用户空间,0xc0000000~0xffffffff 是内核空间,包括内核代码和数据、与进程相关的数据结构(如页表、内核栈)等。另外, %esp 执行栈顶,往低地址方向变化; brk/sbrk 函数控制堆顶往高地址方向变化。可通过以下代码验证进程的地址空间分布,其中 sbrk(0) 函数用于返回栈顶指针。

 
  
   #include<stdio.h>
  
  
   #include <stdlib.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%p\\n", main);
  
  
       printf("Address of global_num 0x%p\\n", &global_num);
  
  
       printf("Address of global_str_arr 0x%p ~ 0x%p\\n", &global_str_arr[0], &global_str_arr[65535]);
  
  
       printf("Top of stack is 0x%p\\n", &local_var);
  
  
       printf("Top of heap is 0x%p\\n", sbrk(0));
  
  
    
  
  
       heap_var = malloc(sizeof(char) * 127 * 1024);
  
  
    
  
  
       printf("Address of heap_var is 0x%p\\n", heap_var);
  
  
       printf("Top of heap after malloc is 0x%p\\n", sbrk(0));
  
  
    
  
  
       free(heap_var);
  
  
    
  
  
       heap_var = NULL;
  
  
       printf("Top of heap after free is 0x%p\\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 0x0x40060d
  
  
   Address of global_num 0x0x611084
  
  
   Address of global_str_arr 0x0x601080 ~ 0x0x61107f
  
  
   Top of stack is 0x0x7fff6b800d04
  
  
   Top of heap is 0x0x1c86000
  
  
   Address of heap_var is 0x0x1c86010
  
  
   Top of heap after malloc is 0x0x1cc6000
  
  
   Top of heap after free is 0x0x1ca7000
  
 

从结果知,与上图的分布并不一致。而事实上, 64 位系统的虚拟地址空间划分发生了改变: 
1. 地址空间大小不是 2^32 ,也不是 2^64 ,而一般是 2^48 。因为并不需要 2^64 这么大的寻址空间,过大空间只会导致资源的浪费。 64 位 Linux 一般使用 48 位来表示虚拟地址空间, 40 位表示物理地址,这可通过 /proc/cpuinfo 来查看address sizes : 40 bits physical, 48 bits virtual 
2. 其中, 0x0000000000000000~0x00007fffffffffff 表示用户空间,0xFFFF800000000000~ 0xFFFFFFFFFFFFFFFF 表示内核空间,共提供 256TB(2^48) 的寻址空间。这两个区间的特点是,第 47 位与 48~63 位相同,若这些位为 0 表示用户空间,否则表示内核空间。 
3. 用户空间由低地址到高地址仍然是只读段、数据段、堆、文件映射区域和栈

2.malloc 是如何分配内存的?

  malloc 是 glibc 中内存分配函数,也是最常用的动态内存分配函数,其内存必须通过 free进行释放,否则导致内存泄露。关于 malloc 获得虚存空间的实现,与 glibc 的版本有关,但大体逻辑是: 
1. 若分配内存小于 128k ,调用 sbrk() ,将堆顶指针向高地址移动,获得新的虚存空间。 
2. 若分配内存大于 128k ,调用 mmap() ,在文件映射区域中分配匿名虚存空间。 
3. 这里讨论的是简单情况,如果涉及并发可能会复杂一些,不过先不讨论。其中 sbrk 就是修改栈顶指针位置,而 mmap 可用于生成文件的映射以及匿名页面的内存,这里指的是匿名页面。而这个 128k ,是 glibc 的默认配置,可通过函数 mallopt 来设置,

如下引自malloc的man手册。

 
  1. Normally, malloc() allocates memory from the heap, and adjusts the size of the heap as required, using sbrk(2).
  2. When allocating blocks of memory larger than MMAP_THRESHOLD bytes, the glibc malloc() implementation allocates the memory as a private anonymous mapping using mmap(2).
  3. MMAP_THRESHOLD is 128 kB by default, but is adjustable using mallopt(3). Allocations performed using mmap(2) are unaffected by the RLIMIT_DATA resource limit (see getrlimit(2))

可通过以下例子说明上述所说:

 
  
   #include<stdio.h>
  
  
   #include <stdlib.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) %p,  now heap top is %p\\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 %p\\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 0x1344000
  
  
   Address of heap_var1(32k) 0x1344010,  now heap top is 0x136d000
  
  
   Address of heap_var2(64k) <

以上是关于十问 Linux 虚拟内存管理 (glibc)的主要内容,如果未能解决你的问题,请参考以下文章

Linux虚拟内存管理(glibc)

Linux glibc内存管理:用户态内存分配器——ptmalloc实现原理

从上到下看linux内存管理--glibc malloc

glibc内存管理那些事儿

Linux系列:glibc程序设计规范与内存管理思想

Linux系列:glibc程序设计规范与内存管理思想