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

最近在做 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 ,还有其他第三方实现吗?

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

Linux 使用虚拟地址空间,大大增加了进程的寻址空间,由低地址到高地址分别为:

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

下图是 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 位系统的虚拟地址空间划分发生了改变:

  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. 用户空间由低地址到高地址仍然是只读段、数据段、堆、文件映射区域和栈

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

malloc 是 glibc 中内存分配函数,也是最常用的动态内存分配函数,其内存必须通过 free 进行释放,否则导致内存泄露。

关于 malloc 获得虚存空间的实现,与 glibc 的版本有关,但大体逻辑是:

  1. 若分配内存小于 128k ,调用 sbrk() ,将堆顶指针向高地址移动,获得新的虚存空间。
  2. 若分配内存大于 128k ,调用 mmap() ,在文件映射区域中分配匿名虚存空间。
  3. 这里讨论的是简单情况,如果涉及并发可能会复杂一些,不过先不讨论。

其中 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 分配多大的内存,就占用多大的物理内存空间吗?

我们知道, malloc 分配的的内存是虚拟地址空间,而虚拟地址空间和物理地址空间使用进程页表进行映射,那么分配了空间就是占用物理内存空间了吗?

首先,进程使用多少内存可通过 ps aux 命令 查看,其中关键的两信息(第五、六列)为:

  1. VSZ , virtual memory size ,表示进程总共使用的虚拟地址空间大小,包括进程地址空间的代码段、数据段、堆、文件映射区域、栈、内核空间等所有虚拟地址使用的总和,单位是 K
  2. RSS , resident set size ,表示进程实际使用的物理内存空间, RSS 总小于 VSZ 。

可通过一个例子说明这个问题:

#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

由以上结果知:

  1. VSZ 并不是每次 malloc 后都增长,是与上一节说的堆顶没发生变化有关,因为可重用堆顶内剩余的空间,这样的 malloc 是很轻量快速的。
  2. 但如果 VSZ 发生变化,基本与分配内存量相当,因为 VSZ 是计算虚拟地址空间总大小。
  3. RSS 的增量很少,是因为 malloc 分配的内存并不就马上分配实际存储空间,只有第一次使用,如第一次 memset 后才会分配。
  4. 由于每个物理内存页面大小是 4k ,不管 memset 其中的 1k 还是 5k 、 7k ,实际占用物理内存总是 4k 的倍数。所以 RSS 的增量总是 4k 的倍数。
  5. 因此,不是 malloc 后就马上占用实际内存,而是第一次使用时发现虚存对应的物理页面未分配,产生缺页中断,才真正分配物理页面,同时更新进程页面的映射关系。这也是 Linux 虚拟内存管理的核心概念之一。

四. 如何查看进程虚拟地址空间的使用情况?

进程地址空间被分为了代码段、数据段、堆、文件映射区域、栈等区域,那怎么查询这些虚拟地址空间的使用情况呢?

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 结构来维护,同时通过页面建立与物理内存的映射关系,如下图所示。

原创声明,本文系作者授权云+社区-专栏发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏丑胖侠

Zookeeper开源客户端Curator之事件监听详解

Curator对Zookeeper典型场景之事件监听进行封装,提供了使用参考。这篇博文笔者带领大家了解一下Curator的实现方式。 引入依赖 对于Curato...

3127
来自专栏知识分享

关于STM32的外部引脚中断的问题

今天想用自己以前的比较干净的工程模板做一个东西,,,,,,,在添加上引脚中断的时候,,突然想知道自己配置的中断优先级是否正确执行,,,,, 以前刚学习32的时候...

3246
来自专栏老马寒门IT

Node入门教程(10)第八章:Node 的事件处理

Node中大量运用了事件回调,所以Node对事件做了单独的封装。所有能触发事件的对象都是 EventEmitter 类的实例,所以上一篇我们提到的文件操作的可读...

2606
来自专栏一名合格java开发的自我修养

storm 1.0版本滑动窗口的实现及原理

滑动窗口在监控和统计应用的场景比较广泛,比如每隔一段时间(10s)统计最近30s的请求量或者异常次数,根据请求或者异常次数采取相应措施。在storm1.0版本之...

713
来自专栏西安-晁州

rabbitmq消息队列——"topic型交换器"

在之前的章节中我们改进了我们的日志系统,我们使用direct型交换器代替了只能盲目广播消息的fanout型交换器,这使得我们可以有选择性地接收日志。 尽管使用d...

1860
来自专栏转载gongluck的CSDN博客

第16章 Sun RPC

RPC:远程过程调用 ? ? ? ? 默认情况下服务器并不多线程化: rpcgen -C data.x -DDEBUG gcc server.c data_s...

3477
来自专栏ImportSource

非阻塞编程核心设计之Selector

1. 铺垫 在本文中,我们将探讨Java NIO的Selector组件。 Selector是一个定义在java.nio.channels包中的抽象类。 选择器(...

2879
来自专栏Android机动车

从源码角度看广播

几乎每个安卓应用都无可避免的使用到广播。例如监听WIFI的开启状态、时间的获取,甚至是我们最常用的闹钟功能,都是结合着AlarmManager与广播来实现的。理...

874
来自专栏岑玉海

Hbase 学习(五) 调优

1.垃圾回收器调优 当我们往hbase写入数据,它首先写入memstore当中,当menstore的值大于hbase.hregion.memstore.fl...

36213
来自专栏Youngxj

安卓四大组件之Service-服务

1373

扫码关注云+社区