操作系统:内存满了怎么办

内存分配的过程是怎样的?

应用程序通过 malloc 函数申请内存的时候,实际上申请的是虚拟内存,此时并不会分配物理内存。

当应用程序读写了这块虚拟内存,CPU 就会去访问这个虚拟内存, 这时会发现这个虚拟内存没有映射到物理内存, CPU 就会产生缺页中断,进程会从用户态切换到内核态,并将缺页中断交给内核的 **Page Fault Handler (缺页中断函数)**处理。

缺页中断处理函数会看是否有空闲的物理内存,如果有,就直接分配物理内存,并建立虚拟内存与物理内存之间的映射关系。

如果没有空闲的物理内存,那么内核就会开始进行回收内存的工作,回收的方式主要是两种:直接内存回收和后台内存回收。

  • 后台内存回收(kswapd):在物理内存紧张的时候,会唤醒 kswapd 内核线程来回收内存,这个回收内存的过程异步的,不会阻塞进程的执行。
  • 直接内存回收(direct reclaim):如果后台异步回收跟不上进程内存申请的速度,就会开始直接回收,这个回收内存的过程是同步的,会阻塞进程的执行。

如果直接内存回收后,空闲的物理内存仍然无法满足此次物理内存的申请,那么内核就会触发 OOM (Out of Memory)机制

哪些内存可以被回收?

要有两类内存可以被回收,而且它们的回收方式也不同。

  • 文件页(File-backed Page):内核缓存的磁盘数据(Buffer)和内核缓存的文件数据(Cache)都叫作文件页。大部分文件页,都可以直接释放内存,以后有需要时,再从磁盘重新读取就可以了。而那些被应用程序修改过,并且暂时还没写入磁盘的数据(也就是脏页),就得先写入磁盘,然后才能进行内存释放。所以,回收干净页的方式是直接释放内存,回收脏页的方式是先写回磁盘后再释放内存
  • 匿名页(Anonymous Page):这部分内存没有实际载体,不像文件缓存有硬盘文件这样一个载体,比如堆、栈数据等。这部分内存很可能还要再次被访问,所以不能直接释放内存,它们回收的方式是通过 Linux 的 Swap 机制,Swap 会把不常访问的内存先写到磁盘中,然后释放这些内存,给其他更需要的进程使用。再次访问这些内存时,重新从磁盘读入内存就可以了。

文件页和匿名页的回收都是基于 LRU 算法,也就是优先回收不常访问的内存。LRU 回收算法,实际上维护着 active 和 inactive 两个双向链表,其中:

  • active_list 活跃内存页链表,这里存放的是最近被访问过(活跃)的内存页;
  • inactive_list 不活跃内存页链表,这里存放的是很少被访问(非活跃)的内存页;

越接近链表尾部,就表示内存页越不常访问。这样,在回收内存时,系统就可以根据活跃程度,优先回收不活跃的内存。

回收内存带来的性能影响

回收内存的操作基本都会发生磁盘 I/O 的,如果回收内存的操作很频繁,意味着磁盘 I/O 次数会很多,这个过程势必会影响系统的性能。

调整文件页和匿名页的回收倾向

从文件页和匿名页的回收操作来看,文件页的回收操作对系统的影响相比匿名页的回收操作会少一点,因为文件页对于干净页回收是不会发生磁盘 I/O 的,而匿名页的 Swap 换入换出这两个操作都会发生磁盘 I/O。

建议设置成更倾向于文件页回收倾向。

尽早触发 kswapd 内核线程异步回收内存

「直接内存回收」会导致系统抖动,所以应该尽早触发「后台内存回收」来避免应用程序进行直接内存回收。。

内核定义了三个内存阈值(watermark,也称为水位),用来衡量当前剩余内存(pages_free)是否充裕或者紧张,分别是:

  • 页最小阈值(pages_min);
  • 页低阈值(pages_low);
  • 页高阈值(pages_high);

回收机制:

  • 大于pages_min,小于pages_low:这时 kswapd0 会执行内存回收,直到剩余内存大于高阈值(pages_high)为止
  • 小于pages_min:触发直接内存回收

页低阈值(pages_low)可以通过内核选项 /proc/sys/vm/min_free_kbytes (该参数代表系统所保留空闲内存的最低限)来间接设置。min_free_kbytes 虽然设置的是页最小阈值(pages_min),但是页高阈值(pages_high)和页低阈值(pages_low)都是根据页最小阈值(pages_min)计算生成的,可以增大 min_free_kbytes 这个配置选项来及早地触发后台回收

增大了 min_free_kbytes 配置后,这会使得系统预留过多的空闲内存,从而在一定程度上降低了应用程序可使用的内存量,这在一定程度上浪费了内存。所以在调整 min_free_kbytes 之前,应用程序如果关注延迟那就适当地增大 min_free_kbytes,如果关注内存的使用量那就适当地调小 min_free_kbytes。

NUMA 架构下的内存回收策略

SMP 指的是一种多个 CPU 处理器共享资源的电脑硬件架构,也就是说每个 CPU 地位平等,它们共享相同的物理资源,包括总线、内存、IO、操作系统等。每个 CPU 访问内存所用时间都是相同的,因此,这种系统也被称为一致存储访问结构(UMA,Uniform Memory Access)。随着 CPU 处理器核数的增多,多个 CPU 都通过一个总线访问内存,这样总线的带宽压力会越来越大,同时每个 CPU 可用带宽会减少,这也就是 SMP 架构的问题。

为了解决 SMP 架构的问题,就研制出了 NUMA 结构,即非一致存储访问结构(Non-uniform memory access,NUMA)。NUMA 架构将每个 CPU 进行了分组,每一组 CPU 用 Node 来表示,一个 Node 可能包含多个 CPU 。每个 Node 有自己独立的资源,包括内存、IO 等,每个 Node 之间可以通过互联模块总线(QPI)进行通信,所以,也就意味着每个 Node 上的 CPU 都可以访问到整个系统中的所有内存。但是,访问远端 Node 的内存比访问本地内存要耗时很多。

在 NUMA 架构下,当某个 Node 内存不足时,系统可以从其他 Node 寻找空闲内存,也可以从本地内存中回收内存。具体选哪种模式,可以通过 /proc/sys/vm/zone_reclaim_mode 来控制。它支持以下几个选项:

  • 0 (默认值):在回收本地内存之前,在其他 Node 寻找空闲内存;(建议使用)
  • 1:只回收本地内存;
  • 2:只回收本地内存,在本地回收内存时,可以将文件页中的脏页写回硬盘,以回收内存。
  • 4:只回收本地内存,在本地回收内存时,可以用 swap 方式回收内存。

如何保护一个进程不被 OOM 杀掉?

在 Linux 内核里有一个 oom_badness() 函数,它会把系统中可以被杀掉的进程扫描一遍,并对每个进程打分,得分最高的进程就会被首先杀掉。

进程得分的结果受下面这两个方面影响:

  • 第一,进程已经使用的物理内存页面数。
  • 第二,每个进程的 OOM 校准值 oom_score_adj。它是可以通过 /proc/[pid]/oom_score_adj 来配置的。我们可以在设置 -1000 到 1000 之间的任意一个数值,调整进程被 OOM Kill 的几率。

oom_badness() 里的最终计算方法是这样的:

# points 代表打分的结果
# process_pages 代表进程已经使用的物理内存页面数
# oom_score_adj 代表 OOM 校准值
# totalpages 代表系统总的可用页面数
points = process_pages + oom_score_adj*totalpages/1000

每个进程的 oom_score_adj 默认值都为 0,所以最终得分跟进程自身消耗的内存有关,消耗的内存越大越容易被杀掉。我们可以通过调整 oom_score_adj 的数值,来改成进程的得分结果:

  • 如果你不想某个进程被首先杀掉,那你可以调整该进程的 oom_score_adj,从而改变这个进程的得分结果,降低该进程被 OOM 杀死的概率。
  • 如果你想某个进程无论如何都不能被杀掉,那你可以将 oom_score_adj 配置为 -1000。

参考资料

《小林 coding》

《深入理解计算机系统 第3版》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值