# 【NO.421】腾讯面试：linux内存性能优化总结

## **1.内存映射**

Linux 内核给每个进程都提供了一个独立且连续的虚拟地址空间，以便进程可以方便地访问虚拟内存；虚拟地址空间的内部又被分为内核空间和用户空间两部分，不同字长的处理器，地址空间的范围也不同；图示为 32 位和 64 位系统的虚拟地址空间；

![img](https://pic2.zhimg.com/80/v2-ab1833d585eda9af7e4bb47e032ff469_720w.webp)

内存映射是将虚拟内存地址映射到物理内存地址，内核为每个进程都维护了一张页表，记录虚拟地址与物理地址的映射关系；

![img](https://pic1.zhimg.com/80/v2-f07715157891438f24777269ac166fc8_720w.webp)

页表存储在 CPU 的内存管理单元 MMU 中，正常情况下，处理器就可以直接通过硬件，找出要访问的内存；当进程访问的虚拟地址在页表中不存在时，系统会产生一个缺页异常，进入内核空间分配物理内存、更新进程页表，最后再返回用户空间，恢复进程的运行；

TLB (Translation Lookaside Buffer，转译后备缓冲器) 是 MMU 中页表的高速缓存，由于进程的虚拟地址空间是独立的，而 TLB 的访问速度又比 MMU 快得多，因此通过减少进程的上下文切换，减少 TLB 的刷新次数，可以提高 TLB 缓存的使用率，进而提高 CPU 的内存访问性能；

MMU 规定了内存映射的最小单位，即页，通常是 4 KB 大小，每一次内存映射，都需要关联 4KB 或者 4KB 整数倍的内存空间；

多级页表就是把内存分成区块来管理，将原来的映射关系改成区块索引和区块内的偏移；由于虚拟内存空间通常只用了很少一部分，多级页表就只保存这些使用中的区块，从而大大地减少页表的项数，Linux 四级页表管理内存页图示

![img](https://pic1.zhimg.com/80/v2-ff1e9eeae280ec21775d10ff7295fa38_720w.webp)

大页，即比普通页更大的内存块，常见的大小有 2MB 和 1GB；



## 2.虚拟内存空间分布

32 位系统中用户空间的分段示意图

![img](https://pic4.zhimg.com/80/v2-3a9cb107f5f54eb9c6c6bd7b1909e96f_720w.webp)

- \1. 只读段，包括代码和常量等
- \2. 数据段，包括全局变量等
- \3. 堆，包括动态分配的内存，从低地址开始向上增长
- \4. 文件映射段，包括动态库、共享内存等，从高地址开始向下增长
- \5. 栈，包括局部变量和函数调用的上下文等，栈的大小是固定的，一般是 8 MB

## 3.内存的分配与回收

**内存分配**

小块内存 (小于 128K)，使用 brk() 来分配，即通过移动堆顶的位置来分配内存，这些内存释放后并不会立刻归还系统，而是被缓存起来，以便重复使用；

- brk() 方式分配内存，可以减少缺页异常的发生，提高内存访问效率；由于这些内存没有归还系统，在内存工作繁忙时，频繁的内存分配和释放会造成内存碎片；

大块内存 (大于 128K)，使用内存映射 mmap() 来分配，即在文件映射段找一块空闲内存分配出去

- mmap() 方式分配内存，会在释放时直接归还系统，因此 mmap 会发生缺页异常；在内存工作繁忙时，频繁的内存分配会导致大量的缺页异常，使内核的管理负担增大；

**内存回收**

回收缓存，如使用 LRU (Least Recently Used) 算法，回收最近使用最少的内存页面；

回收不常访问的内存，把不常用的内存通过交换分区直接写到磁盘中；

- 交换分区 (Swap) 即把一块磁盘空间当成内存来用；把进程暂时不用的数据存储到磁盘中(换出)，当进程访问这些内存时，再从磁盘读取这些数据到内存中 (换入)

杀死进程，内存紧张时系统还会通过 OOM (Out of Memory)，直接杀掉占用大量内存的进程；

内核的一种保护机制，监控进程的内存使用情况，并且使用 oom_score 为每个进程的内存使用情况进行评分；

- 一个进程消耗的内存越大，oom_score 就越大；
- 一个进程运行占用的 CPU 越多，oom_score 就越小；
- 管理员可以通过 /proc 文件系统，手动设置进程的 oom_adj，从而调整进程的 oom_score；

oom_adj 的范围是 [-17, 15]，数值越大，表示进程越容易被 OOM 杀死；数值越小，表示进程越不容易被 OOM 杀死，其中 -17 表示禁止 OOM；

## 4.buffer/cache

Buffer 是内核缓冲区用到的内存，对应的是 /proc/meminfo 中的 Buffers 值；

Cache 是内核页缓存和 Slab 用到的内存，对应的是 /proc/meminfo 中的 Cached 与 SReclaimable 之和；

- Buffers 是对原始磁盘块的临时存储，即用来缓存磁盘的数据，通常不会特别大 (20MB 左右)；从而内核可以把分散的写集中起来，统一优化磁盘的写入，比如可以把多次小的写合并成单次大的写等等；

Buffer 既可以用作 “将要写入磁盘数据的缓存”，也可以用作 “从磁盘读取数据的缓存”

- Cached 是从磁盘读取文件的页缓存，即用来缓存从文件读取的数据；从而，下次访问这些文件数据时，就可以直接从内存中快速获取，而不需要再次访问缓慢的磁盘；

实际上，Cache 也会缓存写文件时的数据

Cache 既可以用作 “从文件读取数据的页缓存”，也可以用作 “写文件的页缓存”

- SReclaimable 是 Slab 的一部分，Slab 包括两部分，其中的可回收部分，用 SReclaimable 记录；而不可回收部分，用 SUnreclaim 记录；

## 5.内存泄漏

栈内存由系统自动分配和管理，一旦程序运行超出了这个局部变量的作用域，栈内存就会被系统自动回收，不会产生内存泄漏的问题；

堆内存由应用程序分配和管理，除非程序退出，这些堆内存并不会被系统自动释放，而是需要应用程序明确调用库函数 free() 释放，如果应用程序没有正确释放堆内存，就会造成内存泄漏；

只读段，包括程序的代码和常量，由于是只读的，不会再去分配新的内存，不会产生内存泄漏；

数据段，包括全局变量和静态变量，这些变量在定义时就已经确定了大小，不会产生内存泄漏；

内存映射段，包括动态链接库和共享内存，其中共享内存由程序动态分配和管理，若程序在分配后忘了回收，就会导致泄漏问题；

## 6.Swap 知识点

Swap 即把一块磁盘空间或者一个本地文件当成内存来使用，包括换出和换入两个过程；

- 换出，即把进程暂时不用的内存数据存储到磁盘中，并释放这些数据占用的内存；
- 换入，即在进程再次访问这些内存的时候，把它们从磁盘读到内存中来；

**NUMA 与 Swap**

NUMA (Non-Uniform Memory Access) 架构，在 NUMA 架构下，多个处理器被划分到不同 Node 上，且每个 Node 都拥有自己的本地内存空间，而同一个 Node 内部的内存空间，又可以进一步分为不同的内存域(Zone)；

某个 Node 内存不足时，系统可以从其他 Node 寻找空闲内存，也可以从本地内存中回收内存；可以通过 
/proc/sys/vm/zone_reclaim_mode 来选择模式，支持以下几个选项；

- 默认的 0，表示既可以从其他 Node 寻找空闲内存，也可以从本地回收内存；
- 1、2、4 都表示只回收本地内存，2 表示可以回写脏数据回收内存，4 表示可以用 Swap 方式回收内存；

**swappiness**

- 对文件页的回收，即直接回收缓存，或者把脏页写回磁盘后再回收；
- 对匿名页的回收，即通过 Swap 机制，把它们写入磁盘后再释放内存；

Linux 提供了 /proc/sys/vm/swappiness 选项，用来调整使用 Swap 的积极程度，swappiness 的范围是 0-100，数值越大，越积极使用 Swap，即更倾向于回收匿名页；数值越小，越消极使用 Swap，即更倾向于回收文件页；

降低 Swap 的使用，可以提高系统的整体性能

\1. 禁止 Swap，现在服务器的内存足够大，所以除非有必要，禁用 Swap 即可，随着云计算的普及，大部分云平台中的虚拟机都默认禁止 Swap；

\2. 若实在需要用到 Swap，可以尝试降低 swappiness 的值，减少内存回收时 Swap 的使用倾向；

\3. 响应延迟敏感的应用，如果它们可能在开启 Swap 的服务器中运行，你还可以用库函数 mlock() 或者 mlockall() 锁定内存，阻止它们的内存换出；

## 7.Linux 回收内存的时机

在内存资源紧张时，Linux 通过直接内存回收和定期扫描的方式，释放文件页和匿名页，以便把内存分配给更需要的进程使用；

直接内存回收，存在新的大块内存分配请求，但是剩余内存不足，此时系统就需要回收一部分内存，进而尽可能地满足新内存请求；

kswapd0 内核线程，用于定期回收内存，kswapd0 定义了三个内存阈值，分别是页最小阈值 (pages_min)、页低阈值(pages_low) 和页高阈值(pages_high)；pages_free 表示剩余内存；

![img](https://pic3.zhimg.com/80/v2-df2a2063fda8e7f5852ba0a9435febb2_720w.webp)

- 剩余内存小于页最小阈值，说明进程可用内存都耗尽了，只有内核才可以分配内存；
- 剩余内存落在页最小阈值和页低阈值中间，说明内存压力比较大，剩余内存不多了；这时 kswapd0 会执行内存回收，直到剩余内存大于高阈值为止；
- 剩余内存落在页低阈值和页高阈值中间，说明内存有一定压力，但还可以满足新内存请求；
- 剩余内存大于页高阈值，说明剩余内存比较多，没有内存压力；

页低阈值可以通过内核选项 
/proc/sys/vm/min_free_kbytes 来间接设置，min_free_kbytes 设置了页最小阈值，而其他两个阈值，都是根据页最小阈值计算生成，如下

```text
pages_low  = pages_min * 5 / 4
pages_high = pages_min * 3 / 2
```

## 8.性能指标与工具总结

![img](https://pic4.zhimg.com/80/v2-84440bba3c1a4c7e8e27985a7a6398db_720w.webp)

![img](https://pic3.zhimg.com/80/v2-b20f785c7cd311a1ce26c71045e04a36_720w.webp)

## 9.实战记录

**【1】free 命令**

![img](https://pic2.zhimg.com/80/v2-244e6595b82838de53571f34b5c25765_720w.webp)

- total 总内存大小；
- used 已使用内存的大小，包含了共享内存；
- free 未使用内存的大小；
- shared 共享内存的大小；
- buff/cache 缓存和缓冲区的大小；
- available 新进程可用内存的大小；

**【2】top 命令**

![img](https://pic4.zhimg.com/80/v2-be84fec554997abc962eac015e1e4103_720w.webp)

VIRT 是进程虚拟内存的大小，只要是进程申请过的内存，即便还没有真正分配物理内存，也会计算在内；

RES 是常驻内存的大小，即进程实际使用的物理内存大小，但不包括 Swap 和共享内存；

SHR 是共享内存的大小，比如与其他进程共同使用的共享内存、加载的动态链接库以及程序的代码段等；

%MEM 是进程使用物理内存占系统总内存的百分比；

注意

- \1. 虚拟内存通常并不会全部分配物理内存；
- \2. 共享内存 SHR 并不一定是共享的，如程序的代码段、非共享的动态链接库，也都算在 SHR 里；

**【3】磁盘和文件写案例**

【3.1】案例一，写文件

```text
测试命令
$ dd if=/dev/urandom of=/tmp/file bs=1M count=500
 
监控命令
vmstat 2
```

![img](https://pic3.zhimg.com/80/v2-f64b38b129cde37e5a1d61cf29b2f796_720w.webp)

- \1. 在 Cache 刚开始增长时，块设备 I/O 很少，而过一段时间后，才会出现大量的块设备写；
- \2. 当 dd 命令结束后，Cache 不再增长，但块设备写还会持续一段时间，并且多次 I/O 写的结果加起来是 dd 要写的 500M 的数据；

**【3.2】案例二，写磁盘**

```text
测试命令
# 运行dd命令向磁盘分区/dev/sdb1写入2G数据
$ dd if=/dev/urandom of=/dev/sdb1 bs=1M count=2048
 
监控命令
vmstat 2
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
1  0      0 7584780 153592  97436    0    0   684     0   31  423  1 48 50  2  0
 1  0      0 7418580 315384 101668    0    0     0     0   32  144  0 50 50  0  0
 1  0      0 7253664 475844 106208    0    0     0     0   20  137  0 50 50  0  0
 1  0      0 7093352 631800 110520    0    0     0     0   23  223  0 50 50  0  0
 1  1      0 6930056 790520 114980    0    0     0 12804   23  168  0 50 42  9  0
 1  0      0 6757204 949240 119396    0    0     0 183804   24  191  0 53 26 21  0
 1  1      0 6591516 1107960 123840    0    0     0 77316   22  232  0 52 16 33  0
```

- \1. 写磁盘时 (即 bo 大于 0 时)，Buffer 和 Cache 都在增长，但显然 Buffer 的增长快得多；

**【4】磁盘和文件读案例**

【4.1】案例一、读文件

```text
测试命令
# 运行dd命令读取文件数据
$ dd if=/tmp/file of=/dev/null
 
监控命令
vmstat 2
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 0  1      0 7724164   2380 110844    0    0 16576     0   62  360  2  2 76 21  0
 0  1      0 7691544   2380 143472    0    0 32640     0   46  439  1  3 50 46  0
 0  1      0 7658736   2380 176204    0    0 32640     0   54  407  1  4 50 46  0
 0  1      0 7626052   2380 208908    0    0 32640    40   44  422  2  2 50 46  0
```

- 读取文件时 (即 bi 大于 0 时)，Buffer 保持不变，而 Cache 则在不停增长；

【4.2】案例二、读磁盘

```text
测试命令
# 运行dd命令读取文件
$ dd if=/dev/sda1 of=/dev/null bs=1M count=1024
 
监控命令
vmstat 2
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
0  0      0 7225880   2716 608184    0    0     0     0   48  159  0  0 100  0  0
 0  1      0 7199420  28644 608228    0    0 25928     0   60  252  0  1 65 35  0
 0  1      0 7167092  60900 608312    0    0 32256     0   54  269  0  1 50 49  0
 0  1      0 7134416  93572 608376    0    0 32672     0   53  253  0  0 51 49  0
 0  1      0 7101484 126320 608480    0    0 32748     0   80  414  0  1 50 49  0
```

- 读磁盘时 (也就是 bi 大于 0 时)，Buffer 和 Cache 都在增长，但显然 Buffer 的增长快很多；

**附录**

【1】文件系统与磁盘的区别

磁盘是一个存储设备(块设备)，可以被划分为不同的磁盘分区，而在磁盘或者磁盘分区上，还可以再创建文件系统，并挂载到系统的某个目录中，这样，系统就可以通过这个挂载目录，来读写文件；即磁盘是存储数据的块设备，也是文件系统的载体；文件系统需要通过磁盘，来保证数据的持久化存储；

在读写普通文件时，I/O 请求会首先经过文件系统，然后由文件系统负责，来与磁盘进行交互；在读写块设备文件时，会跳过文件系统，直接与磁盘交互，也就是所谓的 “裸 I/O”；

【2】统计所有进程的物理内存使用量

每个进程的 PSS ，是指把共享内存平分到各个进程后，再加上进程本身的非共享内存大小的和；

```text
# 使用grep查找Pss指标后，再用awk计算累加值
$ grep Pss /proc/[1-9]*/smaps | awk '{total+=$2}; END {printf "%d kB\n", total }'
391266 kB
```

原文地址：https://zhuanlan.zhihu.com/p/431297893

作者：linux