前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >彻底理解虚拟内存:从一个demo看free/ps/top内存含义

彻底理解虚拟内存:从一个demo看free/ps/top内存含义

原创
作者头像
Radar3
修改2020-06-01 10:48:30
2.2K2
修改2020-06-01 10:48:30
举报
文章被收录于专栏:巫山跬步巫山跬步

1,背景

笔者团队发现现网服务负载即将达到瓶颈,但cpu利用率并未达到瓶颈,基于充分利用机器资源的考量,研发同学提出:“降低nginx worker数,腾出一部分内存,随后提高业务程序worker数,从而提升业务处理能力”的解决方案。

为了确保方案的可靠实施,我们需要在充分理解free/ps/top等命令有关内存信息准确含义的前提下,分析机器当前的内存情况、以及各worker的内存占用情况,明确nginx和业务程序的worker数分别调低和调高多少,既不会出现内存紧张、又能充分利用机器资源,于是有了本文的几个demo实验。

2,概念

在展示代码之前,我们先一起回忆一下操作系统课学过的几个概念。

虚拟内存——虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。与没有使用虚拟内存技术的系统相比,使用这种技术的系统使得大型程序的编写变得更容易,对真正的物理内存(例如RAM)的使用也更有效率。[1]

Memory Overcommit——Memory overcommitment is a concept in computing that covers the assignment of more memory to virtual computing devices (or processes) than the physical machine they are hosted, or running on, actually has.[2]

缺页中断——页缺失(英语:Page fault,又名硬错误、硬中断、分页错误、寻页缺失、缺页中断、页故障等)指的是当软件试图访问已映射在虚拟地址空间中,但是目前并未被加载在物理内存中的一个分页时,由中央处理器的内存管理单元所发出的中断。[3]

3,demo

上面的概念大家或多或少都了解一些,下面我们从代码及进程启动后的内存占用情况来一一说明。

3.1,申请内存但不使用

代码语言:javascript
复制
//mem_never_use.cpp 
#include <unistd.h>
#include <stdlib.h>
#include <iostream>
int main(int argc, char* argv[]) {
    long long i = atoll(argv[1]);
    std::cout << "new mem: " << i << " bytes" << std::endl;
    char* s = new char[i];
    sleep(1024000);
    return 0;
}

编译执行结果为:

代码语言:javascript
复制
[root@VM_144_234_centos ~/demo/virt_mem_demo]# free
              total        used        free      shared  buff/cache   available
Mem:       16166056     1408952     3381420      288216    11375684    14362492
Swap:             0           0           0
[root@VM_144_234_centos ~/demo/virt_mem_demo]# ./mem_never_use 1000000000 &
[1] 32233
[root@VM_144_234_centos ~/demo/virt_mem_demo]# new mem: 1000000000 bytes

[root@VM_144_234_centos ~/demo/virt_mem_demo]# ps axu| head -n1 && ps axu|grep -v grep |grep mem_
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root     32233  0.0  0.0 994340  1120 pts/4    S    20:29   0:00 ./mem_never_use 1000000000
[root@VM_144_234_centos ~/demo/virt_mem_demo]# free
              total        used        free      shared  buff/cache   available
Mem:       16166056     1409060     3381308      288228    11375688    14362388
Swap:             0           0           0

这里先以manpage中的说明来解释下ps输出的含义(man ps)[4]

VSZ——virtual memory size of the process in KiB (1024-byte units).

RSS——resident set size, the non-swapped physical memory that a task has used (in kiloBytes).

结论

可以看出,进程的虚拟内存占用为994340KB,与入参申请分配的大小基本一致;而实际物理内存占用仅1MB,说明执行new分配内存只是操作系统内部做了一个标记,不会立刻实际分配。

3.2,多个进程申请超出总内存,但均不使用

既然new完不会立刻实际分配,那我们会想,最大能new多大的内存?现在我们继续用上节的代码实验一下:

代码语言:javascript
复制
[root@VM_144_234_centos ~/demo/virt_mem_demo]# ./mem_never_use 20000000000
new mem: 20000000000 bytes
terminate called after throwing an instance of 'std::bad_alloc'
  what():  std::bad_alloc
已放弃
[root@VM_144_234_centos ~/demo/virt_mem_demo]# free
              total        used        free      shared  buff/cache   available
Mem:       16166056     1409300     3366564      288228    11390192    14362008
Swap:             0           0           0
[root@VM_144_234_centos ~/demo/virt_mem_demo]# ./mem_never_use 10000000000 &
[2] 7313
[root@VM_144_234_centos ~/demo/virt_mem_demo]# new mem: 10000000000 bytes

[root@VM_144_234_centos ~/demo/virt_mem_demo]# ./mem_never_use 10000000000 &
[3] 7316
[root@VM_144_234_centos ~/demo/virt_mem_demo]# new mem: 10000000000 bytes

[root@VM_144_234_centos ~/demo/virt_mem_demo]# ./mem_never_use 10000000000 &
[4] 7318
[root@VM_144_234_centos ~/demo/virt_mem_demo]# new mem: 10000000000 bytes

[root@VM_144_234_centos ~/demo/virt_mem_demo]# ps axu| head -n1 && ps axu|grep -v grep |grep mem_
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root      7313  0.0  0.0 9783404 1124 pts/4    S    11:19   0:00 ./mem_never_use 10000000000
root      7316  0.0  0.0 9783404 1124 pts/4    S    11:19   0:00 ./mem_never_use 10000000000
root      7318  0.0  0.0 9783404 1120 pts/4    S    11:19   0:00 ./mem_never_use 10000000000
[root@VM_144_234_centos ~/demo/virt_mem_demo]# free
              total        used        free      shared  buff/cache   available
Mem:       16166056     1409980     3365896      288228    11390180    14361352
Swap:             0           0           0

结论

1,机器总内存16GB,一次性申请new 20GB内存,会申请失败。

2,三个进程,分别new 10GB内存,没问题。

3,free命令的输出无明显变化。“虚拟内存”的占用,在free命令无法展示出来。(这个结论很重要)

3.3,单个进程分次申请超出总内存,但均不使用

接上节,我们再来验证一下,如果分多次申请10GB的行为在同一个进程内,会出现什么结果?

代码语言:javascript
复制
//mem_never_use_twice.cpp
#include <unistd.h>
#include <stdlib.h>
#include <iostream>
#include <string.h>
int main(int argc, char* argv[]) {
    long long i = atoll(argv[1]);
    std::cout << "new mem: " << i << " bytes first time" << std::endl;
    char* s0 = new char[i];
    std::cout << "new mem: " << i << " bytes second time" << std::endl;
    char* s1 = new char[i];
    sleep(1024000);
    return 0;
}

编译执行结果如下:

代码语言:javascript
复制
[root@VM_144_234_centos ~/demo/virt_mem_demo]# free
              total        used        free      shared  buff/cache   available
Mem:       16166056     1413396     9884072      288228     4868588    14360040
Swap:             0           0           0
[root@VM_144_234_centos ~/demo/virt_mem_demo]# ./mem_never_use_twice 10000000000 &
[1] 6939
[root@VM_144_234_centos ~/demo/virt_mem_demo]# new mem: 10000000000 bytes first time
new mem: 10000000000 bytes second time

[root@VM_144_234_centos ~/demo/virt_mem_demo]# ps axu| head -n1 && ps axu|grep -v grep |grep mem_
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root      6939  0.0  0.0 19549032 1124 pts/4   S    11:30   0:00 ./mem_never_use_twice 10000000000
[root@VM_144_234_centos ~/demo/virt_mem_demo]# free
              total        used        free      shared  buff/cache   available
Mem:       16166056     1413620     9883808      288228     4868628    14359820
Swap:             0           0           0

说明上节的结论依然有效,单个进程new过的内存20GB,已经超出机器总内存。

3.4,overcommit概述及演示

目前我们已经涉及到overcommit和OOM killer的概念,操作系统允许特定条件下申请内存数大于系统内存,本质上它是系统为了充分利用资源而提供的一种特性。

commit(或overcommit)针对的是内存申请,内存申请不等于内存分配,内存只在实际用到的时候才分配。[5]关于更详细的解释,大家可以去看这篇参考文献,这里举一个关闭overcommit的例子:

代码语言:javascript
复制
[root@VM_144_234_centos ~/demo/virt_mem_demo]# cat /proc/sys/vm/overcommit_memory #笔者注:本行到此才结束
0
[root@VM_144_234_centos ~/demo/virt_mem_demo]# echo 2 > /proc/sys/vm/overcommit_memory #笔者注:本行到此才结束
[root@VM_144_234_centos ~/demo/virt_mem_demo]# ./mem_never_use 10000000000 &
[1] 10692
[root@VM_144_234_centos ~/demo/virt_mem_demo]# new mem: 10000000000 bytes
terminate called after throwing an instance of 'std::bad_alloc'
  what():  std::bad_alloc

[1]+  已放弃               ./mem_never_use 10000000000
[root@VM_144_234_centos ~/demo/virt_mem_demo]# free
              total        used        free      shared  buff/cache   available
Mem:       16166056     1409804     3365220      288228    11391032    14361488
Swap:             0           0           0

overcommit_memory这个内核参数用于控制内核对overcommit的处理行为,最常用的是默认值0,上面我改成2(代表禁止overcommit)再去申请分配10GB内存会立刻报失败。这个值在多数场景下取默认值比较好。现在改回默认值继续下文实验。

3.5,申请内存并全部使用

代码语言:javascript
复制
//mem_use_all.cpp
#include <unistd.h>
#include <stdlib.h>
#include <iostream>
#include <string.h>
int main(int argc, char* argv[]) {
    long long i = atoll(argv[1]);
    std::cout << "new mem: " << i << " bytes" << std::endl;
    char* s = new char[i];
    std::cout << "now memset the memory" << std::endl;
    memset(s, 0, i);
    sleep(1024000);
    return 0;
}

编译执行如下:

代码语言:javascript
复制
[root@VM_144_234_centos ~/demo/virt_mem_demo]# free
              total        used        free      shared  buff/cache   available
Mem:       16166056     1409512     3362872      288228    11393672    14361724
Swap:             0           0           0
[root@VM_144_234_centos ~/demo/virt_mem_demo]# ./mem_use_all 1000000000 &
[1] 3233
[root@VM_144_234_centos ~/demo/virt_mem_demo]# new mem: 1000000000 bytes
now memset the memory

[root@VM_144_234_centos ~/demo/virt_mem_demo]# ps axu| head -n1 && ps axu|grep -v grep |grep mem_
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root      3233  2.5  6.0 994340 977656 pts/4   S    19:28   0:00 ./mem_use_all 1000000000
[root@VM_144_234_centos ~/demo/virt_mem_demo]# free
              total        used        free      shared  buff/cache   available
Mem:       16166056     2389516     2382876      288228    11393664    13381716
Swap:             0           0           0

结论

1,ps的VSZ和RSS都是900多MB,与入参1GB相符,说明memset的内存写操作触发了真实物理内存的占用。

2,free命令展示的used、free、available值在程序执行前后分别变动了900多MB,与结论1相印证。

细说free

老规矩,先上manpage[6]:(中文为笔者注)(单位都是KB)

used—— Used memory (calculated as total - free - buffers - cache)(用户已使用的物理内存,可以看到其中排除了buff/cache)

free—— Unused memory (MemFree and SwapFree in /proc/meminfo)(整机未使用的物理内存)

buffers—— Memory used by kernel buffers (Buffers in /proc/meminfo)(内核用的)

cache—— Memory used by the page cache and slabs (Cached and Slab in /proc/meminfo)(内核用的)

buff/cache—— Sum of buffers and cache(可以理解为内核用来作缓存的一部分内存,可以随时释放出来给用户用)

available—— Estimation of how much memory is available for starting new applications, without swapping.(一个估算值,真正可供用户进程使用的内存值,比free那列更有意义)

总结:used + free + buff + cache = total

现在我们再回头看上面free执行的结果:

1,used变多900多MB,用户自己进程用掉的。

2,free变少900多MB,说明机器直接从free里面取的,而没有释放buff/cache,这可以理解,因为free还够用。

3,available变少900多MB,buff/cache基本没动,那自然总的可用值要减少900多MB。

3.6,申请大量内存并全部使用

现在继续用上节的代码,我们来看一下用户进程占走buff/cache的情况,我new 5GB内存:

代码语言:javascript
复制
[root@VM_144_234_centos ~/demo/virt_mem_demo]# free
              total        used        free      shared  buff/cache   available
Mem:       16166056     1412020     3359124      288228    11394912    14359260
Swap:             0           0           0
[root@VM_144_234_centos ~/demo/virt_mem_demo]# ./mem_use_all 5000000000 &
[1] 7644
[root@VM_144_234_centos ~/demo/virt_mem_demo]# new mem: 5000000000 bytes
now memset the memory

[root@VM_144_234_centos ~/demo/virt_mem_demo]# ps axu| head -n1 && ps axu|grep -v grep |grep mem_
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root      7644 26.4 30.2 4900592 4884064 pts/4 S    19:50   0:02 ./mem_use_all 5000000000
[root@VM_144_234_centos ~/demo/virt_mem_demo]# free
              total        used        free      shared  buff/cache   available
Mem:       16166056     6299272      103932      288228     9762852     9472172
Swap:             0           0           0

used和available分别变多和变少约5GB,不用多说。我们重点看free和buff/cache:

free变少3GB,buff/cache变少2GB。

说明进程新占用的5GB内存,有3GB来自原来的空闲物理内存,有2GB来自内核从缓存中释放出来的内存。

内核的策略是优先用free里面的,用到剩差不多100MB的时候,开始从缓存中释放内存给用户进程使用。

3.7,申请但只使用部分内存

本文讲到这里,相信读者可以自行判断下面这个demo的执行结果了

代码语言:javascript
复制
//mem_use_one_fourth.cpp
#include <unistd.h>
#include <stdlib.h>
#include <iostream>
#include <string.h>
int main(int argc, char* argv[]) {
    long long i = atoll(argv[1]);
    std::cout << "new mem: " << i << " bytes" << std::endl;
    char* s = new char[i];
    std::cout << "now memset 1/4 of the memory" << std::endl;
    memset(s, 0, i/4);
    sleep(1024000);
    return 0;
}

编译执行如下:

代码语言:javascript
复制
[root@VM_144_234_centos ~/demo/virt_mem_demo]# free
              total        used        free      shared  buff/cache   available
Mem:       16166056     1407576     4994720      296420     9763760    14355616
Swap:             0           0           0
[root@VM_144_234_centos ~/demo/virt_mem_demo]# ./mem_use_one_fourth 4000000000 &
[1] 9364
[root@VM_144_234_centos ~/demo/virt_mem_demo]# new mem: 4000000000 bytes
now memset 1/4 of the memory

[root@VM_144_234_centos ~/demo/virt_mem_demo]# ps axu| head -n1 && ps axu|grep -v grep |grep mem_                                         
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root      9364 10.2  6.0 3924028 977656 pts/4  S    19:59   0:00 ./mem_use_one_fourth 4000000000
[root@VM_144_234_centos ~/demo/virt_mem_demo]# free
              total        used        free      shared  buff/cache   available
Mem:       16166056     2385940     4016344      296420     9763772    13377248
Swap:             0           0           0

VSZ是4GB,RSS是1GB;used增加1GB,free减少1GB,buff/cache不变,available减少1GB。

3.8,单次可申请的最大内存

我们看到前文的内存申请有时失败,有时成功。那么成功与否的条件到底是什么,是内核Heuristic overcommit算法在每次申请时计算出来的一个值[5],不考虑swap的话这个值大致就是available列的值。

代码语言:javascript
复制
[root@VM_144_234_centos ~/demo/virt_mem_demo]# free
              total        used        free      shared  buff/cache   available
Mem:       16166056     1413328     9882304      288228     4870424    14359988
Swap:             0           0           0
[root@VM_144_234_centos ~/demo/virt_mem_demo]# ./mem_never_use 14000000000 
new mem: 14000000000 bytes
^C
[root@VM_144_234_centos ~/demo/virt_mem_demo]# ./mem_never_use 15000000000 
new mem: 15000000000 bytes
terminate called after throwing an instance of 'std::bad_alloc'
  what():  std::bad_alloc
已放弃 

4,环境说明

本文演示运行环境为CentOS 7, Linux 3.10.107-1,8C16G机器。

top命令内存信息跟ps基本一样,具体可自行查阅manpage。

5,不同系统输出差异

部分老版本系统的命令输出可能会有一些差异,这里再以CentOS 6, Linux 2.6.32.43上的输出为例进行一点补充说明。

代码语言:javascript
复制
[root@VM_1_1_centos ~/demo/cpp]# free
             total       used       free     shared    buffers     cached
Mem:       8191216    3226828    4964388          0     320988    1691800
-/+ buffers/cache:    1214040    6977176
Swap:      2097144     590204    1506940

这个展示就没有新版内核那么好理解了,而且含义有差异。

Mem那行的公式是:total = used + free,这个used代表整机已使用的内存,基本等于新内核里used + buff + cache;free含义没变化。

buff/cache那行的公式也是:total = used + free,这个used代表:刨去buff/cache后用户使用的内存,即新内核里used的含义;free代表:Mem行的free + buff/cache,有点类似新内核的available(注意只是类似)。

现在你再看buff/cache那行开头的“-/+”符号,明白它的含义了吗?[7]分别代表Mem行的used减去、free加上(buff+cache)后的值。即公式:

3226828 - 320988 - 1691800 = 1214040

4964388 + 320988 + 1691800 = 6977176

6,总结

看完demo,该回正题了,调整worker数时候到底该关注哪个内存指标?

我的观点是,不能只看available,它只代表当前瞬时的可用内存;还要关注你的代码行为预期。

如果你的程序是python等GC型编程语言,那你不能只关注瞬时情况,还需要对程序的内存占用情况进行一段时间观察,尤其是GC期间的内存波动情况,可能出现短时大量虚拟内存发生缺页占用物理内存的情况;

如果是c++等自主管理内存的程序,那你应该对自己程序的内存占用有一个清晰的预判:多个进程情况下总overcommit多少内存是ok的,超出多少会有多大概率发生OOM。

想清楚上面的事情,相信你对机器上的内存申请量会有自己的一个合理规划了。

7,关键词

缺页、Minor Fault、Major Fault、sar、overcommit、OOM、brk、mmap、SWAP

8,参考

[1], https://zh.wikipedia.org/wiki/%E8%99%9A%E6%8B%9F%E5%86%85%E5%AD%98

[2], https://en.wikipedia.org/wiki/Memory_overcommitment

[3], https://zh.wikipedia.org/wiki/%E9%A1%B5%E7%BC%BA%E5%A4%B1

[4], http://man7.org/linux/man-pages/man1/ps.1.html

[5], 《理解LINUX的MEMORY OVERCOMMIT》,http://linuxperf.com/?p=102

[6], https://www.man7.org/linux/man-pages/man1/free.1.html

[7], https://serverfault.com/questions/85470/meaning-of-the-buffers-cache-line-in-the-output-of-free

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1,背景
  • 2,概念
  • 3,demo
    • 3.1,申请内存但不使用
      • 结论
    • 3.2,多个进程申请超出总内存,但均不使用
      • 结论
    • 3.3,单个进程分次申请超出总内存,但均不使用
      • 3.4,overcommit概述及演示
        • 3.5,申请内存并全部使用
          • 结论
          • 细说free
        • 3.6,申请大量内存并全部使用
          • 3.7,申请但只使用部分内存
            • 3.8,单次可申请的最大内存
            • 4,环境说明
            • 5,不同系统输出差异
            • 6,总结
            • 7,关键词
            • 8,参考
            相关产品与服务
            弹性伸缩
            弹性伸缩(Auto Scaling,AS)为您提供高效管理计算资源的策略。您可设定时间周期性地执行管理策略或创建实时监控策略,来管理 CVM 实例数量,并完成对实例的环境部署,保证业务平稳顺利运行。在需求高峰时,弹性伸缩自动增加 CVM 实例数量,以保证性能不受影响;当需求较低时,则会减少 CVM 实例数量以降低成本。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档