借助crash工具理解linux系统的内存分配

背景

Linux下访问匿名页发生的神奇“化学反应”,我看完这篇文章感觉对linux的内存分配又多了一点点理解,因此也想推荐给你读下。

原文提出了一个问题:向mmap系统调用申请的内存”读时”没有导致物理可用内存减少,但是”随后写时”发现物理可用内存减少。

原作者借助”分析内核源码”的收到来剖析问题原因,最终得到结论:第一次读匿名页后,然后写匿名页,先只读方式映射到0页,然后发生写时复制,分配物理页,虚拟页以可读可写的方式映射到此物理页。

我自己总结一下,就是按照时间顺序来看,当对”可读可写的vma”先读后写时,”对应的页表项”应该是:

  • 在第一次读之前不存在
  • 在第一次读之后,第一次写之前,映射到固定位置,此时页表属性是只读
  • 在第一次写之后,映射到实际分配的物理内存,此时页表属性是可读可写

可能这个结论你听着还是有点绕,并且我想你可能和我一样,也不想一下子就去研究”内核源码”来理解到底是怎么回事。

那怎么能搞清楚原文的问题和结论呢?我发现一个神器crash,实际操作一番后,我感觉文章结论其实很好理解。下面就和我一起玩一玩crash,理解mmap分配的内存到底是怎么回事,也验证一下原文的结论是否正确。

crash是一个可以用来调试linux内核崩溃的工具,也可以调试正在运行中的内核。它的安装和使用都很简单,理解本篇文章也不需要你之前接触过这个工具。

过程

  • 准备源码

    我们在”读内存之前”、”读内存后写内存前”、”写内存之后”这三个时刻分别调用getchar()停顿一下,方便我们用crash工具观察”页表项”

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    [root@instance-fj5pftdp tmp]# cat e.c
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <sys/mman.h>

    int main(){
    char* addr = mmap (NULL, 1024, PROT_READ|PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS , - 1 , 0);

    printf ("addr: %lu pid:%d\n", addr, getpid()); // 在读内存之前:页表项不存在
    getchar();

    printf ("1:%s \n", addr); // 在读内存之后:映射了页表,页表项属性为"只读"
    getchar();

    strcpy(addr, "AAAABBBB"); // 在写内存之后:页表项属性为"可读可写"

    printf ("2:%s \n", addr);
    getchar();

    return 0;
    }

    上面的c程序编译运行后,我们就可以来分析”读内存之前”、”读内存后写内存前”、”写内存之后”这三个时刻的”页表项”是什么了。

  • 使用crash分析”页表项”的变化

    首先简单说一下crash的安装和使用。

    如果你是centos系统,就可以用以下命令来安装

    1
    2
    yum install crash  // 安装crash工具
    debuginfo-install kernel-debuginfo-`uname -r` // 安装crash工具需要的内核文件

    安装好后,使用下面的命令就可以调试”运行中的内核了”。vmlinux是带调试符号的内核文件。

    1
    [root@instance-fj5pftdp ~]# crash /usr/lib/debug/usr/lib/modules/3.10.0-1160.11.1.el7.x86_64/vmlinux /dev/mem

    下面来看一下”页表项”的变化:我在程序三次getchar()时,使用crashvtop命令查看”虚拟地址”对应的页表

    image

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    crash> set 31880    // 31880是进程号
    PID: 31880
    COMMAND: "a.out"
    TASK: ffff9396f6a7a100 [THREAD_INFO: ffff93983cbe4000]
    CPU: 1
    STATE: TASK_INTERRUPTIBLE
    crash> px 139812061335552 // 十进制转成十六进制。139812061335552是mmap的返回值
    $1 = 0x7f2888405000
    crash> vtop 0x7f2888405000 // vtop指令用来查看"虚拟地址"对应的"物理地址"
    VIRTUAL PHYSICAL
    7f2888405000 (not mapped) // 这里可以看出来,此时没有映射到对应的"物理地址"

    PGD: 16abd67f0 => 16b7d7067
    PUD: 16b7d7510 => 170f95067
    PMD: 170f95210 => 160fd5067
    PTE: 160fd5028 => 0 // "页表项"为0,其中的P标志位为0,表示"页表项"不存在,没有映射到对应的"物理地址"

    ...

    crash> vtop 0x7f2888405000
    VIRTUAL PHYSICAL
    7f2888405000 117d65000
    ....
    PTE: 160fd5028 => 8000000117d65225
    PAGE: 117d65000

    PTE PHYSICAL FLAGS
    8000000117d65225 117d65000 (PRESENT|USER|ACCESSED|NX) // 此时"页表项"有了映射关系,说明此时只能读不能写

    crash> vtop 0x7f2888405000
    ...

    PTE PHYSICAL FLAGS
    80000003b0cf5867 3b0cf5000 (PRESENT|RW|USER|ACCESSED|DIRTY|NX) // RW标志表明此时可以写
    ...

    如上面指令结果中的注释:执行三次vtop指令时,页表项(PTE)中的”FLAGS”都不断变化,读写权限确实像”问题背景”中那样变化。可以看出来,结论是没有问题的

    怎么样,crash验证这个结论是不是很简单?

总结

crash很好用,如果你想了解更多crash工具的应用场景,可以参考 解决Linux内核问题实用技巧之 - Crash工具结合/dev/mem任意修改内存