Linux 内存原理分析
- 内存性能指标
- 系统内存
- 已用内存
- 剩余内存
- 可用内存
- 缺页异常
- 主缺页异常
- 次缺页异常
- 缓存/缓冲区
- 使用量
- 命中率
- Slabs
- 进程内存
- 虚拟内存(VSS)
- 常驻内存(RSS)
- 按比例分配共享内存后的物理内存(PSS)
- 独占内存(USS)
- 共享内存
- SWAP 内存
- 缺页异常
- 主缺页异常
- 次缺页异常
- SWAP
- 已用空间
- 剩余空间
- 换入速度
- 换出速度
通常使用命令 free
可以查看系统内存的总体情况:
1 | free |
使用命令 top
或 ps
查看进程的内存使用情况:
1 | top |
其每列的含义:
VIRT:虚拟内存,只要是进程申请过的内存,即便还没有真正分配物理内存也会计算在内。
RES:常驻内存,进程实际使用的物理内存大小,不包括 Swap 和共享内存。
SHR:共享内存,比如与其他进程共同使用的共享内存、加载的动态链接库以及程序的代码段等。
%MEM:使用物理内存占系统总内存的百分比。
需要注意:
虚拟内存通常不会全部分配物理内存(从上面的输出可见每个进程的虚拟内存都比常驻内存大得多)。
在计算多个进程的内存使用时,不应把所有进程的 SHR 直接相加得出结果。共享内存 SHR 并不一定是共享的,比如程序的代码段、非共享的动态链接库都算在 SHR。
工作原理
内存映射
Linux 内核为每个进程提供独立的、连续的虚拟地址空间,以便应用程序访问内存。
其中虚拟地址空间又分为内核空间和用户空间,分别对应进程的内核态和用户态,进程在用户态时只能访问用户空间内存,只有进入内核态后才可以访问内核空间内存。其地址范围因 CPU 字长而异:
1 | +-------------+ 0xFFFFFFFF |
用户空间和内核空间都对应同一块物理内存,但很显然物理内存的总大小是远小于进程的虚拟内存大小的,因此只有被实际使用的虚拟内存才会被分配物理内存,两者的关系被称为 内存映射,由 CPU 的 MMU(内存管理单元)管理:
MMU 以 页(Page) 为单位管理内存映射,一般为 4KB。但为了提高管理大内存块的效率,也提供 2MB、1GB 的大页 Huge Page)。
内存页存放在多级页表中(可节省页表空间),每级的表项用于在下一级表中选择页,最后一个索引表示页内偏移,最终 以起始地址和偏移量构成物理地址。
每当应用程序访问的虚拟地址在页表中没有找到,就会触发 缺页异常。在内核空间分配物理内存、更新页表后再返回用户空间,才能继续执行。

其中缺页异常又分为两种场景:
次缺页异常:可以直接从物理内存中分配。
主缺页异常:需要磁盘 I/O 介入(比如 Swap)。
CPU 使用 TLB(转移后备缓冲区)提高地址翻译效率,内存映射翻译的结果存放在 TLB 中。因此减少进程的上下文切换,减少 TLB 刷新次数,就可以提高 TLB 缓存的利用率,从而提高 CPU 的内存访问性能。
虚拟内存布局
虚拟内存布局基本如下,其中用户空间内存被分为多个段,从低位到高位:
只读段:包括代码和常量等。
数据段:包括全局变量等。
堆:包括动态分配的内存,从低地址开始向上增长。
文件映射段:动态分配,包括动态库、共享内存等,从高地址开始向下增长。
栈:包括局部变量,函数调用上下文等(大小固定,一般是 8MB)。
1 | +----------+ 0xFFFFFFFF |
内存管理
内存分配:malloc()
是 C 标准库提供的内存分配函数,对应到系统调用上有两种方式:
brk()
:小块内存(< 128K)通过移动堆顶的位置来分配,释放后会被缓存起来(用户空间),可重复使用。可以减少缺页异常的发生,提高内存访问效率。但由于内存没有归还系统,在内存工作繁忙时频繁的内存分配和释放会造成内存碎片。mmap()
:大块内存(> 128K)使用内存映射分配,即在文件映射段找一块空闲内存分配出去。由于在释放时直接归还系统,每次 mmap 都会发生缺页异常。在内存工作繁忙时大量的缺页中断会使内核的管理负担增大。
无论采用何种调用分配内存,都只有在首次访问、发生缺页异常时才会进入内核分配。
内存碎片 是内存分配时会出现的现象,其分为:
外部碎片:零散的内存块不足以满足一次内存分配请求,但其总和却大于请求分配的内存大小,则这些内存块为外部碎片。该情况可通过 伙伴系统(合并相邻的页)来解决。
内部碎片:请求的内存比一个页更小,则分配的内存没有被充分利用,其中分配出去、但未被利用的部分称为内部碎片。对于小内存,则使用 slab 分配器 来分配和释放(可理解为伙伴系统的缓存)。
内存回收:在分配给应用程序的内存使用完毕后,应当调用 free()
或 unmap()
函数来释放。同时操作系统也会限制进程使用内存,在内存紧张时:
基于一定规则回收,比如 LRU(Least Recently Used),Second Chance 等。
回收不常访问的内存,把不常用的内存通过 交换分区(Swap) 直接写到磁盘中(换出,需要访问时再从磁盘读取)。
基于 zproject(zcache、zswap、zram 等)的 内存压缩(当需要访问时再解压,效果比落盘好)。
基于 oom_score 为进程评分,分数越高则通过 OOM(Out of Memory)异常 直接杀死(消耗内存越大 oom_score 越大,占用 CPU 越多 oom_score 越小)。
也可以通过
/proc
手动设置 oom_adj(范围 [-17, 5],数值越大越容易被 OOM)手动设置进程的 oom_score。比如不希望一个进程被杀死,可以:
echo -16 > /proc/$(pidof sshd)/oom_adj
Buffer 与 Cache
在 free
命令的输出中,有一列名为 buff/cache。其中 buffer 和 cache 的含义是(参考 man proc
):
Buffers:磁盘数据的缓存。内核缓冲区用到的内存,对应的是
/proc/meminfo
中的 Buffers 值。通常不会特别大(20MB 左右),内核把分散的写集中以统一优化磁盘的写入,比如把多次小写合并成单次大写等。Cache:文件数据的缓存。包括内核 页缓存 和 Slab 用到的内存(可回收缓存),对应的是
/proc/meminfo
中的 Cached 与 SReclaimable 之和。缓存从文件读取的数据,下次访问时直接从内存中获取,而不需要再次访问磁盘。SReclaimable:Slab 分为两部分,其中的可回收部分,用 SReclaimable 记录;不可回收部分,用 SUnreclaim 记录。
太长不看版:总的来说,Buffer 和 Cache 分别缓存磁盘和文件系统的读写数据。
写:不仅优化磁盘和文件的写入,对于应用程序而言在数据真正落盘前,就可以返回去做其他工作。
读:既可以加速读取那些需要频繁访问的数据,也降低了频繁 I/O 对磁盘的压力。
实际上其包括的内容因环境而异,要理解实际的含义需要进一步分析。
写操作
使用 dd
生成读取随机设备生成大文件或写入磁盘分区:
1 | dd if=/dev/urandom of=/tmp/file bs=1M count=500 |
使用 vmstat
监控内存和 I/O:
1 | vmstat 1 |
以写文件为例,在 Cache 刚开始增长时块设备 I/O 很少。一段时间后才出现大量的块设备写;dd 命令结束后 Cache 不再增长,但块设备写还会持续一段时间,多次 I/O 写的结果加起来才是 dd 要写入的 500M 数据。
相反,如果是直接写入磁盘分区,可见写磁盘时 bo 大于 0,Buffer 和 Cache 都在增长,但 Buffer 的增长快得多。
因此写文件时用到 Cache 缓存数据,而写磁盘则会用到 Buffer 缓存数据。
读操作
清理缓存,从文件或磁盘分区中读取数据写入空设备:
1 | 清理缓存 |
使用 vmstat
监控内存和 I/O:
1 | vmstat 1 |
从文件中读取,可见读时(bi 大于 0 时)Buffer 保持不变,Cache 在不停增长。因此可印证 Cache 是对文件读的页缓存。
从磁盘分区中读取,可见读时(bi 大于 0)Buffer 和 Cache 都在增长,其中 Buffer 的增长快很多。说明读磁盘时数据缓存到了 Buffer 中。
缓存优化
缓存命中率越高,使用缓存带来收益越高,应用程序的性能越好。
基于 Linux 内核 eBPF(extended Berkeley Packet Filters)机制的 bcc 工具来跟踪内核中管理的缓存,可以查看缓存的使用和命中情况,安装:
1 | sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 4052245BD4284CDD |
使用 cachestat
查看总体缓存的读写命中情况:
1 | cachestat 1 3 |
使用 cachetop
查看每个进程的缓存命中情况:
1 | cachetop |
使用 pcstat
工具可以查看文件在内存中的缓存大小以及缓存比例,安装:
1 | 其依赖于 Golang |
查看文件缓存:
1 | ls |
命中分析
测试使用 dd
对同一个文件进行多次读取。
先生成文件、清理缓存:
1 | dd if=/dev/sda1 of=file bs=1M count=512 |
使用 dd
测试文件读取速度:
1 | dd if=file of=/dev/null bs=1M |
使用 cachetop
监控缓存命中情况:
1 | cachetop 5 |
读请求的缓存命中率只有为 49.8% ,此时在另一个终端执行 dd
读取同一个文件:
1 | dd if=file of=/dev/null bs=1M |
查看监控缓存命中情况:
1 | cachetop 5 |
可见系统缓存对第二次 dd
操作有明显的加速效果,大大提高文件读取的性能。
如何监控?
监控读写性能时需要注意:
使用缓存可以大幅提高读写性能,在用
dd
测试文件系统性能时,要注意由于缓存造成的测试结果失真。命中率只反映经过系统缓存的读写请求比例,不能反映实际的读写数据大小。比如使用直接 I/O 会绕过系统缓存,由于没有使用缓存,就谈不上缓存命中率了(
cachetop
工具不会把直接 I/O 计算在内)。因此除了缓存命中率,还要结合实际的 I/O 大小(HITS,次数)综合分析缓存的使用情况。
内存泄漏
进程的内存空间:
栈:内存由系统自动分配和管理,程序运行超出局部变量的作用域,栈内存就会被系统自动回收,所以不会产生内存泄漏。
堆:内存由应用程序动态分配管理,比如使用
malloc()
分配,使用free()
释放。如果没有正确释放堆内存,就会造成内存泄漏。只读段:包括只读的程序的代码和常量,不会产生内存泄漏。
数据段:包括在定义时就已经确定大小的全局变量和静态变量,不会产生内存泄漏。
内存映射段:包括动态链接库和共享内存,共享内存由程序动态分配和管理,没有正确回收会造成内存泄漏.
在内存紧张时系统会通过 OOM (Out of Memory)机制杀死进程,但在 OOM 前就可能造成严重的性能问题,比如:其他需要内存的进程,无法分配新的内存;内存不足触发系统的缓存回收以及 SWAP 机制,从而进一步导致 I/O 的性能问题等。
如何监控?
使用 vmstat
查看内存动态变化情况:
1 | vmstat 3 |
可见 memory 的 free 一列不断下降,而 buff 和 cache 基本不变。说明系统使用内存在不断升高,要确定是否发生内存泄漏,还需要找到让内存增长的进程,并找到其内存增长的原因。
使用 bcc 的 memleak
工具可以跟踪系统或指定进程的内存分配、释放请求,然后定期输出未释放内存和相应调用栈的汇总情况(默认 5s)。
1 | -a 显示每个内存分配请求的大小以及地址 |
bcc-tools 需要内核版本为 4.1 或者更高,对于较久的版本,建议使用 valgrind。
内存泄漏的原因有很多,比如:
malloc()
和free()
通常并不是成对出现,需要注意在每个异常处理路径和成功路径上都释放内存 。在多线程程序中,一个线程中分配的内存,可能会在另一个线程中访问和释放。
在第三方的库函数中,隐式分配的内存可能需要应用程序显式释放。
对于 Java 应用,可参考 Java 虚拟机 ProcessOn Mind 的 “内存溢出异常(OOM)”。
交换空间
文件页(File-backed Page):经系统释放后可以回收的内存,包括缓存和缓冲区。其在内存管理中通常被称为文件页,大部分文件页都可以直接回收,需要时再从磁盘重新读取。被应用程序修改过且未落盘的数据称为内存 脏页,需要先落盘然后才能进行内存释放:
应用程序通过系统调用
fsync
把脏页同步到磁盘中。交由系统内核线程
pdflush
负责脏页的刷新。
通过内存映射获取的文件映射页也是常见的文件页,也可以被释放掉、在下次访问时从文件重新读取。
匿名页(Anonymous Page):诸如应用程序动态分配的堆内存,不能被直接回收。但通过 Swap 机制 就能把它们暂时先存在磁盘、释放内存。
Swap 原理
Swap 即把磁盘空间或者本地文件当成内存使用,使服务器内存不足也可以运行大内存的应用程序。
其包括换出和换入两个过程:
换出:把进程暂时不用的内存数据存储到磁盘中,并释放数据占用的内存。
换入:在进程再次访问这些内存时,从磁盘读到内存。
应用场景:
应用程序不希望被 OOM 杀死(即使内存不足时),而是缓一段时间、等待人工介入或系统自动释放其他进程内存,再分配给它。
常见的电脑休眠和快速开机功能也是基于 Swap,省去应用程序的初始化过程,加快开机速度。
…
内存回收的时机:
直接回收:有新的大块内存分配请求、但是剩余内存不足,系统回收一部分内存(比如缓存),尽可能满足新内存请求。
定期回收:即 kswapd0。kswapd0 定义了三个内存阈值(watermark,也称为水位)用于衡量内存的使用情况,剩余内存使用 pages_free 表示:
页最小阈值(pages_min)
页低阈值(pages_\low)
页高阈值(pages_high)
1 | ↑ |
一旦剩余内存小于页低阈值就会触发内存的回收。页低阈值可通过内核选项 /proc/sys/vm/min_free_kbytes
间接设置页最小阈值,其他两个阈值根据页最小阈值计算生成的 :
1 | pages_low = pages_min*5/4 |
NUMA 架构
有时系统有剩余内存,但 Swap 仍在升高。就是处理器 NUMA (Non-Uniform Memory Access)架构 导致。
在 NUMA 架构下多个处理器被划分到不同 Node 上,每个 Node 都拥有自己的本地内存空间。同一个 Node 内部的内存空间可进一步分为不同的内存域(Zone):
直接内存访问区(DMA)
普通内存区(NORMAL)
伪内存区(MOVABLE)
…
1 |
|
使用 numactl
命令可查看处理器在 Node 的分布情况、每个 Node 的内存使用情况:
1 | numactl --hardware |
通过 /proc/zoneinfo
查看内存阈值:
1 | cat /proc/zoneinfo |
某个 Node 内存不足时,系统可以从其他 Node 寻找空闲内存,也可以从本地内存中回收内存。
通过 /proc/sys/vm/zone_reclaim_mode
来调整模式:
默认 0 即既可以从其他 Node 寻找空闲内存,也可以从本地回收内存。
1、2、4 表示只回收本地内存,2 表示可以回写脏数据回收内存,4 表示可以用 Swap 方式回收内存。
因此内存的回收既包含文件页,也包含匿名页:
对文件页的回收是直接回收缓存,或把脏页写回磁盘后再回收。
对匿名页的回收,是通过 Swap 机制把它们写入磁盘后再释放内存。
通过 /proc/sys/vm/swappiness
选项可调整使用 Swap 的积极程度:范围 [0, 100],数值越大越积极使用 Swap(更倾向于回收匿名页);数值越小越消极使用 Swap(更倾向于回收文件页)。
如何监控?
配置 Swap(以 Swap 文件为例):
1 | 创建 Swap 文件 |
使用 free
命令查看 Swap 使用情况:
1 | free |
使用 dd
命令模拟大文件读取:
1 | dd if=/dev/sda1 of=/dev/null bs=1G count=2048 |
使用 sar
命令观察各项指标变化(内存、Swap):
1 | sar -r -S 1 |
可见总内存使用率(%memused)在不断增长(23% -> 98%),且主要内存都被缓冲区(kbbuffers)占用:
- 剩余内存(kbmemfree)不断减少,缓冲区(kbbuffers)不断增大,可知剩余内存不断分配给缓冲区。
- 一段时间后剩余内存很小,而缓冲区占用大部分内存。此时 Swap 逐渐增大,缓冲区和剩余内存则只在小范围内波动。
使用 cachetop
命令观察缓存使用情况:
1 | cachetop 5 |
可见 dd
进程只有 50% 的缓存命中率,未命中的缓存页数(MISSES)为 41022(单位是页),之前运行的 dd
使得缓冲区使用升高。动态观察剩余内存、内存阈值以及匿名页和文件页的活跃情况:
1 |
|
剩余内存(pages_free)在小范围内不停地波动。当小于页低阈值(pages_low)时,又突然增大到大于页高阈值(pages_high)。结合用 sar 看到剩余内存和缓冲区的变化情况可以推导出,剩余内存和缓冲区的波动变化是由于内存回收和缓存再次分配的循环往复:
当剩余内存小于页低阈值时,系统会回收一些缓存和匿名内存,使剩余内存增大。缓存的回收导致
sar
中的缓冲区减小,而匿名内存的回收导致了 Swap 的使用增大。由于
dd
还在继续,剩余内存重新分配给缓存,导致剩余内存减少、缓冲区增大。
如果多次运行 dd
和 sar
,可能会见到在多次的循环重复中,有时 Swap 时多时少,缓冲区的波动则更大。系统回收内存时有时候会回收更多的文件页,有时候回收更多的匿名页。系统回收不同类型内存的倾向不明显,可以查看 swappiness 配置:
1 | cat /proc/sys/vm/swappiness |
查看使用 Swap 最多的进程:
1 |
|
虽然缓存属于可回收内存,但在类似大文件拷贝的场景下,系统还是会用 Swap 机制回收匿名内存,而不仅回收占用绝大部分内存的文件页。
Swap 用来解决内存资源紧张的问题,但本质上还是磁盘操作,因此降低降低 Swap 的使用,可以提高系统的整体性能,因此建议:
服务器内存足够大则直接禁止 Swap。
大部分云平台中的虚拟机都默认禁止 Swap。如果实需要用 Swap,可尝试降低 swappiness,减少内存回收时 Swap 的使用倾向。
对于响应延迟敏感的应用,如果在开启 Swap 的服务器中运行,可以用库函数
mlock()
或者mlockall()
锁定内存、阻止其内存换出。
优化总结
基本优化思路:
禁止 Swap,如果必须开启 Swap 则降低 swappiness 的值,减少内存回收时 Swap 的使用倾向。
减少内存的动态分配。比如使用内存池、大页(HugePage)等。
尽量使用缓存和缓冲区来访问数据。比如使用堆栈明确声明内存空间来存储需要缓存的数据;或者用 Redis 等外部缓存组件,优化数据的访问。
使用 cgroups 等方式限制进程内存使用情况。可以确保系统内存不会被异常进程耗尽。
通过
/proc/pid/oom_adj
,调整核心应用的 oom_score。可保证即使内存紧张,核心应用也不会被 OOM 杀死。
了解内存相关概念与指标后,可通过下表找到合适的工具、分析问题。


参考
分析案例可参考: