内存相关术语
内存映射
内存映射,其实就是将虚拟内存地址映射到物理内存地址。为了完成内存映射,内核为每个进程都维护了一张页表,记录虚拟地址与物理地址的映射关系。
当进程访问的虚拟地址在页表中查不到时,系统会产生一个缺页异常
为决页表项过多的问题,Linux 提供了两种机制,也就是多级页表和大页(HugePage)
- 多级页表就是把内存分成区块来管理,将原来的映射关系改成区块索引和区块内的偏移。
- 大页,顾名思义,就是比普通页更大的内存块,常见的大小有 2MB 和 1GB。大页通常用在使用大量内存的进程上,比如 Oracle、DPDK 等。
虚拟内存空间分布
- 只读段,包括代码和常量等。
- 数据段,包括全局变量等。
- 堆,包括动态分配的内存,从低地址开始向上增长。
- 文件映射段,包括动态库、共享内存等,从高地址开始向下增长。
- 栈,包括局部变量和函数调用的上下文等。栈的大小是固定的,一般是 8 MB。
内存分配与回收
分配
-
对小块内存(小于 128K),C 标准库使用 brk() 来分配,也就是通过移动堆顶的位置来分配内存。这些内存释放后并不会立刻归还系统,而是被缓存起来,这样就可以重复使用。
brk() 方式的缓存,可以减少缺页异常的发生,提高内存访问效率。不过,由于这些内存没有归还系统,在内存工作繁忙时,频繁的内存分配和释放会造成内存碎片。
-
而大块内存(大于 128K),则直接使用内存映射 mmap() 来分配,也就是在文件映射段找一块空闲内存分配出去。
mmap() 方式分配的内存,会在释放时直接归还系统,所以每次 mmap 都会发生缺页异常。在内存工作繁忙时,频繁的内存分配会导致大量的缺页异常,使内核的管理负担增大。这也是 malloc 只对大块内存使用 mmap 的原因。
在内核空间,Linux 则通过 slab 分配器来管理小内存。
回收内存
-
回收缓存,比如使用 LRU(Least Recently Used)算法,回收最近使用最少的内存页面;
-
回收不常访问的内存,把不常用的内存通过交换分区直接写到磁盘中;
-
杀死进程,内存紧张时系统还会通过 OOM(Out of Memory),直接杀掉占用大量内存的进程。
-
直接内存回收,尽可能地满足新内存请求,进而回收一部分内存
-
kswapd0内核线程定时回收内存
kswapd0 定期扫描内存的使用情况,并根据剩余内存落在这三个阈值的空间位置,进行内存的回收操作。
剩余内存小于页最小阈值,说明进程可用内存都耗尽了,只有内核才可以分配内存。 剩余内存落在页最小阈值和页低阈值中间,说明内存压力比较大,剩余内存不多了。这时 kswapd0 会执行内存回收,直到剩余内存大于高阈值为止。 剩余内存落在页低阈值和页高阈值中间,说明内存有一定压力,但还可以满足新内存请求。 剩余内存大于页高阈值,说明剩余内存比较多,没有内存压力。
通过内核选项 /proc/sys/vm/min_free_kbytes来设置页低阈值,而其他两个阈值,都是根据页最小阈值计算生成的pages_low = pages_min*5/4, pages_high = pages_min*3/2
-
NUMA (Non-UniformMemory Access) 架构下的内存回收。
/proc/sys/vm/zone_reclaim_mode 来调整numa node的内存回收策略
默认的 0 ,也就是刚刚提到的模式,表示既可以从其他 Node 寻找空闲内存,也可以从本地回收内存。 1、2、4 都表示只回收本地内存,2 表示可以回写脏数据回收内存,4 表示可以用Swap 方式回收内存。
Buffers 和 Cache
Buffers 是内核缓冲区用到的内存,对应的是 /proc/meminfo 中的 Buffers 值。 Cache 是内核页缓存和 Slab 用到的内存,对应的是 /proc/meminfo 中的 Cached 与SReclaimable 之和。
Buffers 是对原始磁盘块的临时存储,也就是用来缓存磁盘的数据,通常不会特别大(20MB 左右)。这样,内核就可以把分散的写集中起来,统一优化磁盘的写入,比如可以把多次小的写合并成单次大的写等等。 Cached 是从磁盘读取文件的页缓存,也就是用来缓存从文件读取的数据。这样,下次访问这些文件数据时,就可以直接从内存中快速获取,而不需要再次访问缓慢的磁盘。
SReclaimable 是 Slab 的一部分。Slab 包括两部分,其中的可回收部分,用SReclaimable 记录;而不可回收部分,用 SUnreclaim 记录。
Buffer 是对磁盘数据的缓存,而 Cache 是文件数据的缓存,它们既会用在读请求中,也会用在写请求中。
缓存命中率
所谓缓存命中率,是指直接通过缓存获取数据的请求次数,占所有数据请求次数的百分比。命中率越高,表示使用缓存带来的收益越高,应用程序的性能也就越好。
内存泄漏
堆内存由应用程序自己来分配和管理。除非程序退出,这些堆内存并不会被系统自动释放,而是需要应用程序明确调用库函数 free() 来释放它们。如果应用程序没有正确释放堆内存,就会造成内存泄漏。
内存泄漏的危害非常大,这些忘记释放的内存,不仅应用程序自己不能访问,系统也不能把它们再次分配给其他应用。
文件页,脏页
属于可回收内存的缓存和缓冲区,是文件页
被应用程序修改过,并且暂时还没写入磁盘的数据就是脏页
应用程序动态分配的堆内存被称为匿名页
对文件页的回收,当然就是直接回收缓存,或者把脏页写回磁盘后再回收。 而对匿名页的回收,其实就是通过 Swap 机制,把它们写入磁盘后再释放内存。
swap
Swap 说白了就是把一块磁盘空间或者一个本地文件(以下讲解以磁盘为例),当成内存来使用。它包括换出和换入两个过程。
- 所谓换出,就是把进程暂时不用的内存数据存储到磁盘中,并释放这些数据占用的内存。
- 而换入,则是在进程再次访问这些内存的时候,把它们从磁盘读到内存中来。
/proc/sys/vm/swappiness 选项,用来调整使用 Swap 的积极程度。
swappiness 的范围是 0-100,数值越大,越积极使用 Swap,也就是更倾向于回收匿名页;数值越小,越消极使用 Swap,也就是更倾向于回收文件页。
在内存资源紧张时,Linux 会通过 Swap ,把不常访问的匿名页换出到磁盘中,下次访问的时候再从磁盘换入到内存中来。你可以设置 /proc/sys/vm/min_free_kbytes,来调整系统定期回收内存的阈值;也可以设置/proc/sys/vm/swappiness,来调整文件页和匿名页的回收倾向。
内存性能指标
内存性能指标
├── 系统内存指标
│ ├── 已用内存
│ ├── 剩余内存
│ ├── 可用内存
│ ├── 缺页异常
│ │ ├── 主缺页异常
│ │ └── 次缺页异常
│ ├── 缓存/缓冲区
│ └── Slabs
├── 进程内存指标
│ ├── 虚拟内存(VSS)
│ ├── 常驻内存(RSS)
│ ├── 按比例分配共享内存后的物理内存(PSS)
│ ├── 独占内存(USS)
│ ├── 共享内存
│ ├── SWAP内存
│ └── 缺页异常
│ │ ├── 主缺页异常
│ │ └── 次缺页异常
└── SWAP
├── 已用空间
├── 剩余空间
├── 换入速度
└── 换出速度
系统的已用内存、剩余内存、共享内存、可用内存、缓存和缓冲区的用量
- 已用内存和剩余内存很容易理解,就是已经使用和还未使用的内存。
- 共享内存是通过 tmpfs 实现的,所以它的大小也就是 tmpfs 使用的内存大小。tmpfs其实也是一种特殊的缓存。
- 可用内存是新进程可以使用的最大内存,它包括剩余内存和可回收缓存。
- 缓存包括两部分,一部分是磁盘读取文件的页缓存,用来缓存从磁盘读取的数据,可以加快以后再次访问的速度。另一部分,则是 Slab 分配器中的可回收内存。
- 缓冲区是对原始磁盘块的临时存储,用来缓存将要写入磁盘的数据。这样,内核就可以把分散的写集中起来,统一优化磁盘写入。
进程内存使用情况,比如进程的虚拟内存、常驻内存、共享内存以及 Swap 内存等。
- 虚拟内存,包括了进程代码段、数据段、共享内存、已经申请的堆内存和已经换出的内存等。这里要注意,已经申请的内存,即使还没有分配物理内存,也算作虚拟内存。
- 常驻内存是进程实际使用的物理内存,不过,它不包括 Swap 和共享内存。
- 共享内存,既包括与其他进程共同使用的真实的共享内存,还包括了加载的动态链接库以及程序的代码段等。
- Swap 内存,是指通过 Swap 换出到磁盘的内存。
缺页异常
- 可以直接从物理内存中分配时,被称为次缺页异常。
- 需要磁盘 I/O 介入(比如 Swap)时,被称为主缺页异常。
Swap 的使用情况,比如 Swap 的已用空间、剩余空间、换入速度和换出速度等。
- 已用空间和剩余空间很好理解,就是字面上的意思,已经使用和没有使用的内存空间。
- 换入和换出速度,则表示每秒钟换入和换出内存的大小。
内存性能工具
案例
在缓存和缓冲区的原理篇中,
我们通过 proc 文件系统,找到了内存指标的来源; 并通过 vmstat,动态观察了内存的变化情况。与 free 相比,vmstat 除了可以动态查看内存变化,还可以区分缓存和缓冲区、Swap 换入和换出的内存大小。
为了弄清楚缓存的命中情况,我们又用了 cachestat,查看整个系统缓存的读写命中情况,并用 cachetop 来观察每个进程缓存的读写命中情况。
在内存泄漏的案例中,我们用 vmstat,发现了内存使用在不断增长,又用memleak,确认发生了内存泄漏。通过 memleak 给出的内存分配栈,我们找到了内存泄漏的可疑位置。
在 Swap 的案例中,我们用 sar 发现了缓冲区和 Swap 升高的问题。通过cachetop,我们找到了缓冲区升高的根源;通过对比剩余内存跟 /proc/zoneinfo 的内存阈,我们发现 Swap 升高是内存回收导致的。案例最后,我们还通过 /proc 文件系统,找出了 Swap 所影响的进程。
根据指标找工具
内存指标 | 性能工具 |
---|---|
系统已用、可用、剩余内存 | free vmstat sar /proc/meminfo |
进程虚拟内存、常驻内存、共享内存 | ps top |
进程内存分布 | pmap |
进程Swap换出内存 | top /proc/pid/status |
进程缺页异常 | top |
系统换页情况 | sar free vmstat |
缓存/缓冲区用量 | sar cachestat |
缓存/缓冲区命中率 | cachetop free |
SWAP已用空间和剩余空间 | sar |
Swap换入换出 | vmstat memleak |
内存泄漏检测 | valgrind |
指定文件的缓存大小 | pcstat |
根据工具查指标
性能工具 | 内存指标 |
---|---|
free /proc/meminfo |
系统已用、可用、剩余内存以及缓存和缓冲区的使用量 |
top ps |
进程虚拟、常驻、共享内存以及缺页异常 |
vmstat | 系统剩余内存、缓存、缓冲区、换入、换出 |
sar | 系统内存换页情况、内存使用率、缓存和缓冲区用量以及Swap使用情况 |
cachestat | 系统缓存和缓冲区的命中率 |
cachetop | 进程缓存和缓冲区的命中率 |
slabtop | 系统Slab缓存使用情况 |
/proc/pid/status | 进程Swap内存等 |
/proc/pid/smaps | 进程地址空间和内存状态 |
pmap | 进程内存错误检查器,用来检测内存初始化、 |
valgrind | 泄漏、越界访问等各种内存错误 |
memleak | 内存泄漏检测 |
pcstat | 查看指定文件的缓存情况 |
快速分析内存的性能瓶颈
- 先用 free 和 top,查看系统整体的内存使用情况。
- 再用 vmstat 和 pidstat,查看一段时间的趋势,从而判断出内存问题的类型。
- 最后进行详细分析,比如内存分配分析、缓存 / 缓冲区分析、具体进程的内存使用分析 等。
分析流程图
- 通过 free,发现大部分内存都被缓存占用后
- 可以使用 vmstat 或者 sar观察一下缓存的变化趋势,确认缓存的使用是否还在继续增大。
- 如果继续增大,则说明导致缓存升高的进程还在运行,那你就能用缓存 / 缓冲区分析工具(比如 cachetop、slabtop 等),分析这些缓存到底被哪里占用。
- 如果发现系统可用内存不足时,首先要确认内存是否被缓存 / 缓冲区占用。排除缓存 / 缓冲区后,你可以继续用 pidstat 或者 top,定位占用内存最多的进程。
- 找出进程后,再通过进程内存空间工具(比如 pmap),分析进程地址空间中内存的使用情况就可以了。
- 通过 vmstat 或者 sar 发现内存在不断增长后,可以分析中是否存在内存泄漏的问题。比如你可以使用内存分配分析工具 memleak ,检查是否存在内存泄漏。如果存在内存泄漏问题,memleak 会为你输出内存泄漏的进程以及调用堆栈。
内存常见优化
- 最好禁止 Swap。如果必须开启 Swap,降低 swappiness 的值,减少内存回收时Swap 的使用倾向。
- 减少内存的动态分配。比如,可以使用内存池、大页(HugePage)等。
- 尽量使用缓存和缓冲区来访问数据。比如,可以使用堆栈明确声明内存空间,来存储需要缓存的数据;或者用 Redis 这类的外部缓存组件,优化数据的访问。
- 使用 cgroups 等方式限制进程的内存使用情况。这样,可以确保系统内存不会被异常进程耗尽。
- 通过 /proc/pid/oom_adj ,调整核心应用的 oom_score。这样,可以保证即使内存紧张,核心应用也不会被 OOM 杀死。
常见工具的使用
free
显示Linux系统中空闲的、已用的物理内存及swap内存,及被内核使用的buffer
常用命令
free
free -m
free -g
指标含义
total:总计物理内存的大小。
used:已使用多大。
free:可用有多少。
Shared:多个进程共享的内存总额。
Buffers/cached:磁盘缓存的大小。
available
cachestat
提供了整个操作系统缓存的读写命中情况。 需要安装 bcc 软件包 。
常用命令
cachestat 1 3
TOTAL MISSES HITS DIRTIES BUFFERS_MB CACHED_MB
2 0 2 1 17 279
2 0 2 1 17 279
2 0 2 1 17 279
指标含义
TOTAL 表示总的 I/O 次数
MISSES 表示缓存未命中的次数
HITS 表示缓存命中的次数
DIRTIES 表示新增到缓存中的脏页数
BUFFERS_MB 表示 Buffers 的大小,以 MB 为单位
CACHED_MB 表示 Cache 的大小,以 MB 为单位
cachetop
提供了每个进程的缓存命中情况。 需要安装 bcc 软件包 。
cachetop
11:58:50 Buffers MB: 258 / Cached MB: 347 / Sort: HITS / Order: ascending
PID UID CMD HITS MISSES DIRTIES READ_HIT% WRITE_HIT%
13029 root python 1 0 0 100.0% 0.0%
指标含义
默认按照缓存的命中次数(HITS)排序,
HITS 表示缓存命中的次数
MISSES 表示缓存未命中的次数
DIRTIES 表示新增到缓存中的脏页数
READ_HIT 表示读的缓存命中率
WRITE_HIT 表示写的缓存命中率
slabtop
以实时的方式显示内核“slab”缓冲区的细节信息
常用命令
slabtop
slabtop -d 1 # 1秒钟刷新一次数据
slabtop -s c # 按照cache size排序
pmap
用于显示一个或多个进程的内存状态
常用命令
pmap $PID
名称解释
Address: 内存开始地址
Kbytes: 占用内存的字节数(KB)
RSS: 保留内存的字节数(KB)
Dirty: 脏页的字节数(包括共享和私有的)(KB)
Mode: 内存的权限:read、write、execute、shared、private (写时复制)
Mapping: 占用内存的文件、或[anon](分配的内存)、或[stack](堆栈)
Offset: 文件偏移
Device: 设备名 (major:minor)
valgrind
valgrind是一个专业的程序优化和内存调试工具集。
常用的工具包括
memcheck
,cachegrind
,callgrind
,massif
,helgrind
和drd
。
安装
yum -y install make gcc autoconf automake
wget https://sourceware.org/pub/valgrind/valgrind-3.15.0.tar.bz2
tar -xjvf valgrind-3.15.0.tar.bz2
cd valgrind-3.15.0
./autogen.sh
./configure
make
make install
valgrind --version
常用命令
valgrind # 使用Memcheck(默认)工具显示program对内存使用情况的诊断
valgrind --leak-check=full --show-leak-kinds=all # 使用Memcheck详细输出program的所有可能的内存泄漏
valgrind --tool=cachegrind # 使用Cachegrind工具来分析和记录program的CPU缓存操作
valgrind --tool=massif --stacks=yes # 使用Massif工具来分析和记录program的堆内存和堆栈使用情况
memleak
memleak跟踪内存分配和回收请求,收集每次分配的调用栈。 属于bcc套件
/usr/share/bcc/tools/memleak -a -p ${pid}
pcstat
可以查看某个文件是否被缓存 https://github.com/tobert/pcstat
pcstat /bin/ls
参考 极客时间专栏《Linux 性能优化实战》