Memory Diagnosis
Last updated
Last updated
Linux 给每个进程都提供了一个独立的虚拟地址空间,连续的。虚拟地址空间又分为内核空间(高位)和用户空间(低位)。进程在用户态时,只能访问用户空间内存;进入内核态后才能访问内核空间内存。每个进程的内核空间关联的是相同的物理内存。
并不是所有的虚拟内存都会分配物理内存,内核为每个进程都维护了一张页表,记录虚拟地址与物理地址的映射关系。页表存储在 MMU 中。虚拟地址在页表中查不到时,系统产生一个缺页异常,进入内核空间分配物理内存、更新进程页表,最后再返回用户空间,恢复进程的运行。
TLB 是 MMU 的高速缓存,若上下文切换过多,则 TLB 的刷新次数增加,进程的 MMU 是独立的,所以会导致 TLB 缓存的使用率降低。
MMU 规定了内存映射的最小单位,一般为 4KB。页仅为 4KB,页表就会很大,Linux 提供了多级页表和大页两种方式来解决此问题。Linux 用四级页表来管理内存,前四个表用于选择页,最后一个索引表示页内偏移。
虚拟内存空间的用户空间又被分为多个不同的段。以 32位系统为例:
只读段,代码和常量。不会内存泄漏。
数据段,全局变量。不会内存泄漏。
堆,动态分配的内存,malloc() 分配。调用 free() 回收,可能产生内存泄漏问题。
文件映射段,动态库,共享内存 mmap() 分配。可能产生内存泄漏问题。
栈,局部变量,函数调用的上下文。大小固定,一般 8MB。由系统自动分配与管理,内存会被系统自动回收,不会产生内存泄漏问题。
malloc 对应到系统调用上,有 brk() 和 mmap() 两种,brk 适合小块内存(小于 128K),而 mmap 适合大块内存。
brk:通过移动堆顶位置来分配内存。内存释放后不会立即归还给系统,而是被缓存起来,重复使用。可以减少缺页异常的发生,提高内存响应效率。但是会造成内存碎片。
mmap:在文件映射段找一块空闲内存分配出去。释放时直接归还给系统,所以每次 mmap 都会发生缺页异常。
这两种调用后并没有分片真正的内存,只有在首次访问的时候才分配。
系统有一系列回收内存的机制:
回收缓存,基于 LRU
回收不常访问的内存,通过交换分区(Swap)写入磁盘中,也是基于 LRU
杀死进程。系统通过 oom_score 为每个进程的内存使用情况评分,消耗内存越大、占用 CPU 使用越小,oom_score 越大,越容易被杀死。可以通过 /proc 设置 oom_adj。
可以通过 free 查看系统的整体内存情况,通过 top、ps 查看进程的内存使用情况。
free 输出的缓存一列是 Buffer 与 Cache 之和,那两者有什么区别呢?通过 man free 可以看到:
buffers: Memory used by kernel buffers (Buffers in /proc/meminfo)
cache: Memory used by the page cache and slabs (Cached and SReclaimable in /proc/meminfo)
进一步查看 /proc/meminfo 的定义,通过 man proc:
Buffers: Relatively temporary storage for raw disk blocks that shouldn't get tremendously large (20MB or so). 原始磁盘块的临时存储。
Cached: In-memory cache for files read from the disk (the page cache). Doesn't include SwapCached. 读取文件的页缓存。实际上,用 dd 和 vmstat 观测下来,写文件也会使用 Cache缓存。
SReclaimable: Part of Slab, that might be reclaimed, such as caches. Slab 中可回收的部分。
Buffer 是对磁盘数据的缓存,而 Cache 是文件数据的缓存,它们既会用在读请求中,也会用在写请求中。
磁盘是块设备,可以划分不同分区。分区之上可以再创建文件系统,挂载到某个目录。通常写文件时会经过文件系统,文件系统负责与磁盘交互。裸 IO 直接读写磁盘或分区,会跳过文件系统,存在 buffer。注意裸 IO 与直接 IO 的区别,直接 IO 跳过 buffer,直达块层,需要用户处理对齐问题。
通过缓存获取数据的请求次数占所有请求次数的百分比。可以通过 cachestat、cachetop 查看 Linux 缓存命中的情况。
进程看到的是操作系统提供的虚拟内存空间,通过页表映射到物理内存。虚拟内存空间中有内核空间和用户空间,用户空间中的堆和内存映射段由应用程序自己来分配和管理内存,若没有正确释放内存,可能造成内存泄漏。避免内存泄漏通常有如下要点:
malloc() 和 free() 通常并不是成对出现,所以需要在每个异常处理路径和成功路径上都释放内存 。
在多线程程序中,一个线程中分配的内存,可能会在另一个线程中访问和释放。
在第三方的库函数中,隐式分配的内存可能需要应用程序显式释放。
在原理一节我们已经知道,当发生内存泄漏或运行了大内存的程序,从而导致系统内存资源紧张时,系统会有内存回收和 OOM 两种处理方式。
内存回收就是释放可以回收的内存,它们通常叫做文件页(File backed Page),文件页包含缓存、缓冲区、内存映射获取的文件映射页,文件页分为两种回收方式:
直接回收,再需要时,重新从磁盘读取。
文件页被应用程序修改过,暂时还没写入磁盘(脏页),得先写入磁盘再释放。写入磁盘的方式又分为两种:
应用程序通过系统调用 fsync 把脏页同步到磁盘。
内核线程 pdflush 负责脏页的刷新。
可回收内存除了文件页,还包含匿名页(Anonymous Page),即应用程序动态分配的堆内存。由于这些内存有应用程序管理,所以系统不能直接回收,Linux 通过 Swap 机制把不常访问的脏页写入磁盘,然后释放这些内存,再次访问这些内存时,重新从磁盘读入内存。
Swap 可以是一块磁盘,也可以是一个文件,当成内存来使用,包括两个过程:
换入:从磁盘读入内存。
换出:内存写入磁盘,释放内存。
Swap 可以使系统内存变大,笔记本电脑的休眠和快速开机功能,也是基于 Swap。
可通过 sar、/proc/zoneinfo、/proc/pid/status 等方式查看系统和进程的内存使用情况。swap 会影响性能,常用如下方法降低 swap 的使用:
禁用 swap
降低 swappiness 的值
mlock(), mlockall() 锁定内存,阻止换出
触发系统回收内存有两种方式:
直接内存回收,有新的大块内存分配请求,但是内存剩余不足,这时系统会回收一部分内存。
kswap0,内核线程,定期回收。
kswap0 定义了三个阈值(watermark),pages_min, pages_low, pages_high,剩余内存用 pages_free 表示。根据剩余内存与三个阈值的关系,进行内存操作:
小于 pages_min,仅内核可以分配内存。
pages_min ~ pages_low,执行内存回收,直到大于 pages_high。
pages_low ~ pages_high,内存有一定压力,可以满足新内存请求。
大于 pages_high,没有内存压力。
可以通过/proc/sys/vm/min_free_kbytes
设置 pages_min,其它两个阈值的关系是固定的:
有时发现系统 swap 升高,但是系统还有很多空余内存,这可能就是 NUMA(Non-Uniform Memory Access)导致的。在 NUMA 架构下,多个处理器划分到不同的 Node 上,每个 Node 有自己的本地内存空间。每个 node 内部内存又分为不同的区域:直接内存访问区(DMA)、普通内存区(NORMAL)、伪内存区(MOVABLE)。可通过 numactl 查看 node 信息。
可通过 /proc/zoneinfo 查看每个 node 的三个阈值:
nr_*_anon:活跃和非活跃的匿名页数。
nr_*_file:活跃和非活跃的文件页数。
某个 Node 内存不足时,可以从其它 node 寻找空闲内存,也可以从本地内存中回收。可通过 /proc/sys/vm/zone_reclaim_mode 调整:
0 表示两种方式都可以
1,2,4 表示只回收本地内存,2 表示可回写脏数据回收内存,4 表示可用 Swap 的方式。
通过上文,我们知道内存回收:
文件页:直接回收或把脏页写回磁盘再回收
匿名页:通过 Swap 回收
那么 Linux 优先采用哪种呢,可设置 /proc/sys/vm/swappiness 来调整使用 swap 的积极程度,范围是 0 ~ 100。注意这是权重,就算设置为 0,也会使用 swap。
已用内存
剩余内存
共享内存,通过 tempfs 实现,大小等于 tmpfs 的大小。
可用内存,剩余内存+可回收缓存
缓存(Cache):
磁盘读取文件的页缓存
slab 分配器中的可回收内存
缓冲区(Buffer),对原始磁盘块的临时存储
虚拟内存,代码段+数据段+共享内存+已经申请的堆内存+已经换出的内存(Swap)。注意:已经申请,还未分配物理内存,也算在内。
常驻内存,实际使用的物理内存,不包括 Swap 和共享内存。一般换算成系统总内存的百分比。
共享内存,动态加载的链接库+程序代码段。
Swap 内存。
进程申请内存后,首次访问时系统才会通过缺页异常分配内存,有两种场景:
次缺页异常:可以直接从物理内存分配
主缺页异常:需要磁盘 I/O 介入(如 Swap)
已用空间
剩余空间
换入和换出速度
先运行几个覆盖面较大的性能工具,free, top, vmstat, pidstat 等:
free, top 查看系统整体内存情况
vmstat, pidstat 查看一段时间的趋势,判断内存问题类型
详细分析
内存调优最重要的就是,保证应用程序的热点数据放到内存中,并尽量减少换页和交换。
禁止 Swap。若必须开启,则降低 swappiness 的值。
减少动态内存分配。使用内存池、大页(HugePage)等。
尽量使用 Cache、Buffer 来访问数据。
使用 cgroups 限制进程内存使用。
调整核心进程的 /proc/pid/oom_adj,保证内存紧张时,核心进程不会被 OOM 杀死。