4.4 内存
本节我们来介绍内存相关的操作系统原理。
1. 内存相关的操作原理
我们从下面几个方面入手来讲解内存相关的操作系统原理:
- 虚拟内存和页:
- 内存架构
- 内存管理
- 进程地址空间
最后我们会说一说内存检测的相关指标。
2. 虚拟内存和换页
虚拟内存是一个抽象概念,它向每个进程和内核提供巨大的、线性的并且私有的地址空间。它简化了软件开发,把物理内存的分配交给操作系统管理。
从上面的图可以看到进程的地址空间由虚拟内存子系统隐射到主内存和物理交换设备。当内存不够用时,内核需要按需在它们之间移动内存页,称为换页。
换页分为两种类型:
- 为交换共享文件系统的页缓存而产生的文件系统换页
- 虚拟内存的匿名换页
文件系统换页 文件系统换页由读写位于内存中的映射文件页引发。映射文件产生自:
- 使用文件内存映射 mmap 的应用程序
- 使用了页缓存的文件系统
文件系统页可能因为在主存修改过(“脏的”)在换出时需要写回磁盘。如果没有修改过(干净的)因为磁盘已有副本,换页仅仅需要释放内存即可。
匿名换页 匿名换页涉及进程私有数据: 堆和栈,要求数据保存至交换设备,因为这些数据磁盘没有副本。匿名页换入会给应用程序带来同步延时,因为必然发生读磁盘I/O,换出可能不会直接应用程序性能,因为换出是内核异步执行的。
2.1 按需换页
按需换页将CPU创建和映射内存的开销延时到实际访问或需要,而不是初次分配内存时。访问一个未映射的内存页将产生一个缺页异常。
虚拟内存页存在以下几种状态:
- 未分配
- 已分配,未映射
- 已分配,已映射到主存
- 已分配,已映射到物理交换空间(磁盘)
从 2->3就是缺页,如果需要磁盘读写就是严重缺页异常,否则就是次缺页异常。从这几种状态出发可以定义另外两个内存时术语:
- 常驻集合大小 RSS: 已分配的主存(状态3)大小
- 虚拟内存大小: 所有已分配区域(2+3+4)
除此之外我们还能经常看到另一个内存术语,共享内存 SHR,它标识与其他进程共同使用的共享内存、加载的动态链接库以及程序的代码段等所占用的内存。要注意共享内存 SHR 并不一定是共享的,比方说,程序的代码段、非共享的动态链接库,也都算在 SHR 里。当然,SHR 也包括了进程间真正共享的内存。
3.内存架构
3.1 UMA 架构
内存硬件包括主存,总线,CPU 缓存和 MMU(内存管理单元)。下面展示了一个普通双处理器均匀访问模型(UMA) 系统的主存架构,又称对称多处理器架构 SMP
通过共享总线,每个 CPU 访问所有内存都有均匀的访问延时。
3.2 NUMA
作为对照下面是一个双处理器非均匀访问模型 NUMA 系统,其中采用一个 CPU 互联(CPU 原理一章有提到)。
在 NUMA 架构下,多个处理器被划分到不同 Node 上,对主存的访问时间随着相对 CPU 的位置不同而变化。与 CPU 直接相连的内存称为本地内存。
既然 NUMA 架构下的每个 Node 都有自己的本地内存空间,那么,在分析内存的使用时,我们也应该针对每个 Node 单独分析。
可以通过 numactl 命令,来查看处理器在 Node 的分布情况,以及每个 Node 的内存使用情况。
|
|
4.内存管理
内存管理软件包括虚拟内存系统,地址转换,换页。与性能相关的内容包括:
- 内存释放
- 内存分配
4.1 内存分配
内存分配器用于内存分配。下图展示了分配器的作用,以及分配器的一些常见类型:
- slab: 内核级分配器
- libc: 用户级分配器的统称,包括 libmalloc, libumem, mumalloc
- Page Allocator: Linux 用于管理页的伙伴分配器,在下面的空闲链表中会详细介绍
slab
内核 slab 分配器管理特定大小的对象缓存,使它们能被快速的回收利用,并且避免页分配开销。这对经常处理固定大小结构的内核内存分配来说特别有效。slab 大小固定,因此可以一次分配 M 个 slab 大小的缓存,也就避免了多次页分配的开销。大小固定类似于数组也便于回收利用。
Linux 基于 slab 分配器提供了另一个分配器 SLUB。SLUB 为解决多个问题设计,特别是 slab 分配器的复杂性。包括移除对象队列,以及每 CPU 缓存,吧 NUMA 优化留个页分配器(Page Allocator)
SLUB 现在是 Linux 的默认选项.
用户级分配器
有多种用户级分配器,他们的性能也有所差异:
- libc: 不建议使用,性能较差,容易导致内存碎片化
- glibc: 分配基于分配请求的长度,是结合了多种分配策略的高效分配器
- 较小的分配来自内存集合,包括伙伴关系算法合并长度相似的单位
- 较大的分配用树高效搜索空间
- 非常大的分配转到 mmap()
4.2 内存释放
内存过低时,系统会按照上面的高到低的次序释放内存:
- 空闲链表: 未使用的页列表,能立即用于分配。通常每个 NUMA 的内存有一个
- 回收: 内核释放可以轻易释放的内存
- 换页: 对于 Linux 可以配置一个交换倾向的可调参数
/proc/sys/vm/swappiness
,范围为 0-100,默认值为 40。- 值越高: 倾向于"换页"来释放内存,此处的换页指匿名换页
- 值越低: 倾向于收回页缓存,即文件系统换页
- 这就通过在保留热文件系统缓存的同时,换出冷应用程序的内存来提高系统的吞吐量
- OOM: 搜索并杀死可牺牲的进程来释放内存。Linux 采用 select_band_process() 搜索后用 omm_kill_process() 杀死进程
空闲链表
类UNIX系统使用空闲链表和页面换出守护进程来管理内存,如上图所示:
- 回收的内存添加到空闲链表表头以便将来分配
- 通过页面换出守护进程释放的内存被加到表尾。kswapd 释放的内存包括有价值的文件系统缓存,这些文件系统缓存在未被重用前,如有对任一页的请求,它能被取回并从空闲链表中移除
空闲链表通常由分配器消耗,如内核的 slab 分配器,以及用户空间的 libc malloc。上面的单个空闲链表是一种简化,具体实现依内核版本不同。
Linux 使用伙伴分配器管理页。它以2的幂的方式向不同尺寸的内存分配器提供多个空闲链表。术语伙伴指找到相邻的空闲内存页以被同时分配。
回收
回收大多是从内核的slab 分配器缓存释放内存。这些缓存包括 slab 大小的未使用内存块,以供重用。回收将这些内存交还给系统进行分配。
文件系统部分我们会说到内核使用 Slab 机制来管理目录项和索引节点的缓存,/proc/sys/vm/vfs_cache_pressure 可以定义目录项缓存和索引节点缓存的回收倾向,默认值 100,数值越大,就表示越容易回收。
换页
页面换出守护进程管理利用换页释放内存。当主存中可用的空闲链表低于阀值时,kswapd就会开始页扫描。页扫描仅会按需启动,通常平衡的系统不会经常做页扫描并且仅以短期爆发方式扫描。因此如果页扫描多于几秒通常是内存压力问题的预兆。
Linux 的页面换出守护进程称作 kswapd(),如下图所示,它扫描非活动和活动内存的 LRU 页列表以释放页面。
页的活动列表和非活动列表采用 LRU 方式工作,kswapd 先扫描非活动列表,然后按需扫描活动列表。扫描会遍历列表检查页面,找出可以释放的页面:
- 如果是干净的页直接释放
- 如果是脏页会需要先将脏页写回磁盘然后释放。 kswapd 只有在系统严重不足才会选择脏页释放,其他情况下脏页由 flush 内核线程写回磁盘。脏页的管理详见文件系统的操作系统原理部分。
kswapd 的激活基于空闲内存和两个提供滞后的阀值。如下图所示:
- 一旦空闲内存达到最低阀值,kswapd 运行于同步模式,按需求释放内存页(内核排除在外),此时只有内核才可以分配内存。
- 最低阀值通过 vm.min_free_kbytes 设置,其他阀值基于此按比例放大两倍,三倍。
在 NUMA 架构下,三个内存阈值(页最小阈值、页低阈值和页高阈值),都可以通过内存域在 proc 文件系统中的接口 /proc/zoneinfo 来查看。
某个 Node 内存不足时,系统可以从其他 Node 寻找空闲内存,也可以从本地内存中回收内存。具体选哪种模式,你可以通过 /proc/sys/vm/zone_reclaim_mode
来调整。它支持以下几个选项:
- 默认的 0 ,也就是刚刚提到的模式,表示既可以从其他 Node 寻找空闲内存,也可以从本地回收内存。
- 1、2、4 都表示只回收本地内存,2 表示可以回写脏数据回收内存,4 表示可以用 Swap 方式回收内存。
OOM
OOM(Out of Memory),其实是内核的一种保护机制。它监控进程的内存使用情况,并且使用 oom_score 为每个进程的内存使用情况进行评分:
- 一个进程消耗的内存越大,oom_score 就越大
- 一个进程运行占用的 CPU 越多,oom_score 就越小
这样,进程的 oom_score 越大,代表消耗的内存越多,也就越容易被 OOM 杀死。
可以通过 /proc 文件系统,手动设置进程的 oom_adj ,从而调整进程的 oom_score。oom_adj 的范围是 [-17, 15]
,数值越大,表示进程越容易被 OOM 杀死;数值越小,表示进程越不容易被 OOM 杀死,其中 -17 表示禁止 OOM。
|
|
5. 进程地址空间
用户空间内存,从低到高分别是五种不同的内存段。
- 只读段,包括代码和常量等
- 数据段,包括全局变量等
- 堆,包括动态分配的内存,从低地址开始向上增长,堆上的内存是匿名页
- 文件映射段,包括动态库、共享内存等,从高地址开始向下增长,
- 栈,包括局部变量和函数调用的上下文等。栈的大小是固定的,一般是 8 MB
堆和文件映射段的内存是动态分配的。比如使用 C 标准库
- malloc(): 可以在堆动态分配内存
- mmap(): 可以在文件映射段动态分配内存
对于大多数分配器,free() 不会将内存换给操作系统,相反会保留它们以备将来分配。这意味着对会不停增长,进程的常驻内存只会增加,并且是正常现象。
使用 mmap() 分配,使用 munmap() 释放的内存则会归还给操作系统。
6. 内存监测指标
与内存相关的专业术语或者指标包括:
- 内存统计: 包括各种内存的使用统计
- 缓存命中率
- 内存泄漏
- swap 的影响
6.1 内存统计
各种内存的统计信息记录在下面几个文件中:
/proc/meminfo
: 系统各内存统计/proc/zoneinfo
: 各 NUMA Node 内存以及页缓存统计信息/proc/pid
: 进程的各项统计信息/proc/buddyinfo
: 内核页面伙伴分配器统计信息
meminfo
|
|
其中:
- Buffers 是内核缓冲区用到的内存
- Cache 是内核页缓存
- Reclaimable 是 Slab 的一部分。Slab 包括两部分,其中的可回收部分,用 SReclaimable 记录;而不可回收部分,用 SUnreclaim 记录。
执行 man proc
我们可以看到这些指标的准确含义,有关 Buffers 和 Cached 的区别我们在文件系统一节再详述。
zoneinfo
|
|
其中:
- pages 处的 min、low、high,就是上面提到的三个内存阈值,而 free 是剩余内存页数,它跟后面的 nr_free_pages 相同。
- nr_zone_active_anon 和 nr_zone_inactive_anon,分别是活跃和非活跃的匿名页数。
- nr_zone_active_file 和 nr_zone_inactive_file,分别是活跃和非活跃的文件页数。
后面我们看到的诸如 top,free,sar 命令输出的内存统计信息基本上都是基于这两个文件。
6.2 缓存命中率
所谓缓存命中率,是指直接通过缓存获取数据的请求次数,占所有数据请求次数的百分比。
不过 Linux 系统中并没有直接提供查询缓存命中率的接口。基于 Linux 内核的 eBPF(extended Berkeley Packet Filters)机制的软件包bcc 提供了查询缓存命中率的工具:
- cachestat 提供了整个操作系统缓存的读写命中情况。
- cachetop 提供了每个进程的缓存命中情况。
最后,Buffers 和 Cache 都是操作系统来管理的,应用程序并不能直接控制这些缓存的内容和生命周期。所以,在应用程序开发中,一般要用专门的缓存组件,来进一步提升性能。比如,程序内部可以使用堆或者栈明确声明内存空间,来存储需要缓存的数据。再或者,使用 Redis 这类外部缓存服务,优化数据的访问效率。
6.3 内存泄漏
堆和文件映射段由应用程序自己来分配和管理,如果应用程序没有正确释放内存,就会造成内存泄漏。内存泄漏的危害这么大,因此我们需要有能检测内存泄漏的方法 – memleak。memleak 同样是位于 bcc工具包中的命令。
6.4 swap 影响
|
|
像上面这样,使用后面讲到的诸如sar, free, cachetop
命令可以帮我找到Swap 发生的根源。但另一个问题是 Swap 到底影响了哪些应用程序呢? 通过 /proc/pid/status 中的 VmSwap 字段,我们可以查到进程 Swap 换出的虚拟内存大小:
|
|
或者直接使用 smem --sort swap
可以直接将进程按照swap使用量排序显示。
最后要想从根本上降低 swap 带来的影响可以按需使用下面几种方法:
- 禁止 Swap,现在服务器的内存足够大,所以除非有必要,禁用 Swap 就可以了。随着云计算的普及,大部分云平台中的虚拟机都默认禁止 Swap。
- 如果实在需要用到 Swap,可以尝试降低 swappiness 的值,减少内存回收时 Swap 的使用倾向。
- 响应延迟敏感的应用,如果它们可能在开启 Swap 的服务器中运行,你还可以用库函数 mlock() 或者 mlockall() 锁定内存,阻止它们的内存换出。