点我查看操作系统秘籍连载


OOM和swap分区

进程的虚拟内存空间是映射到整个物理内存空间的,所以在进程自身看来它拥有了整个物理内存,它也能使用整个物理内存,只需在使用的时候请求操作系统帮忙分配更多空间即可。

但是,操作系统上并非只运行了一个进程,如果一个进程无休止的申请物理内存空间,最终会导致物理内存耗尽或即将耗尽,使得操作系统无法创建新进程,因为创建新进程需要为它分配虚拟内存。

所以,操作系统必须得监视物理内存的使用情况,在出现物理内存耗尽或即将耗尽的时候,如果进程继续请求分配内存,将报错out-of-memory(OOM)表示内存不足,并且在出现OOM的时候,操作系统将触发OOM Killer程序从进程列表中筛选出一个内存密集型进程杀掉,从而释放大片内存。但显然,这是不怎么友好的方式,不少服务器都专门或主要运行一个服务,如果这个服务进程正好申请了大量内存,它很可能会被OOM Killer杀掉,从而导致服务停止。在Linux下出现OOM时,可以配置内核参数让Killer决定优先杀死哪个进程或避免被优先杀死,参见下文。

其实,早期内存一般都比较小,很容易就出现内存不足的问题,所以很早就提出了一个交换分区(swap partition)的概念。

swap分区是将磁盘当作内存使用,使得虚拟地址空间的范围大小可以超出物理内存的实际大小,在物理内存空间不足时,可以将物理内存中的一些不重要数据拷贝到磁盘的swap分区中,从而让出内存空间,并且在需要那些已被拷出数据时再从swap分区中拷回到内存,从而不再那么容易发生OOM错误。

但是swap分区毕竟是在磁盘上,并且要在内存和磁盘之间传递数据,所以要访问swap分区上的数据时速度会非常慢。在目前大内存都已足够廉价的情况下,已经没有多少必要使用swap分区。

虽然,现在swap分区对进程来说很少派上用场,但涉及到的一些技术和概念有必要简单介绍下。

使用swap分区第二个要解决的问题是,如何让虚拟地址空间的页映射到磁盘swap分区的页上。也就是,如何将虚拟页翻译成swap页,并且在访问时怎么知道这不是内存的物理页。

因为在物理内存的基础上引入了swap分区,所以每个虚拟页中都有一个存在位来表示该页是否存在于内存中,如果该位的值为1,表示驻留在内存中,如果为0则表示在swap分区而非内存。其中,所有驻留在内存中的页也称为驻留集(RSS, Resident Set)。RSS是统计进程内存使用情况最关键的指标,它代表了当前进程此时此刻实实在在地占用了多少物理内存(包括程序代码、包括普通数据、包括共享内存等)。

当访问一个虚拟页的时候,如果发现不在内存中,这时候会产生缺页异常(page fault),也称为页未命中(page miss),于是陷入操作系统,启动页错误处理程序,从swap分区中找到对应的页(显然,这里需要将虚拟内存页翻译成swap分区页的地址)并拷贝到内存中。从swap分区拷入页的过程称为换入页(page in)。

注意:page fault

Page Fault表示缺页异常,即表示访问的数据不是可直接访问的,需要额外的动作去找数据。

page fault有多种情况:

  • (1).数据在内存中,但没有被当前进程标记为已加载数据,比如多个程序之间使用共享内存的时候会出现这种情况,这也是一种page fault;
  • (2).进程不在内存,但是所需数据是按需加载的,比如现在的操作系统加载程序时,通常不会一次性将整个程序代码读入内存,而是以写时复制的方式按需加载所需数据以节省内存并加速程序启动,如果该进程运行时发现指令未在内存,于是从磁盘加载所需指令,这也是一种page fault;
  • (3).属于该进程的数据,但是已经被交换到磁盘swap分区,这也是一种page fault;
  • (4).最后就是越界访问page,即访问了不属于该进程允许访问的地址,即前面曾提到过的segment fault。

这里产生了一个新问题,既然已经使用了swap分区,说明此时物理内存已经处于紧张状态,从swap中page in的页如何放进内存中?其实,这里也需要先进行page out,将该进程的一页或多页page out以便腾出内存。那么哪些运气不好的页会被page out呢?这里又出现了策略算法,称为页替换策略算法,用来决定哪些页应该被page out,比如使用FIFO算法换出那些先进入的页,使用随机算法随机选择换出的页,使用LRU算法选择最近最不常用的页等等。

其实,除了page out换出页腾出少量内存空间外,操作系统还设置了两个关于空闲页数量(显然,这是物理页)的水位线:高水位线(High Watermark,HW)和低水位线(Low Watermark,LW)。当操作系统发现空闲页的数量少于低水位线的值时,就会自动启动一个称为swap daemon(也称为page daemon)的后台线程kswapd,该线程会扫描所有进程并从中选出一些候选进程,然后将这些进程的所有页都拷贝到swap分区,直到空闲物理内存页的数量达到高水位线的值。

提示:高水位线和低水位线

在计算机领域中,常会使用高水位线和低水位线来监视进程的一些可用性资源。当这类可用性资源的数量高于或临近高水位线的值时,表明资源充足。当可用性资源的数量已经低于低水位线,说明资源紧张,应当采取一些措施恢复一些资源。

其中,换出进程所有页到swap分区的过程称为swap out,而从swap分区拷入进程所有页的过程(比如再次调度到该进程)称为swap in。将这两个概念与page out和page in区分以下:

  • page in和page out是拷入或拷出进程的一页或某些页
  • swap in和swap out是拷入或拷出进程的所有页

关于swap分区,能不用的话最好还是不用,因为当真正用到swap分区的时候,内存已经进入了紧张状态,之后的绝大多数进程基本上都会涉及到swap分区,不断地出现page fault而进行换页,这会导致进程抖动(thrashing),使得整体效率低下,而且这个状态是持续的,直到释放足够的内存空间。这时候还不如采取其它办法释放内存空间,例如杀掉某些无关进程、重启内存密集型服务、重启机器等。

Linux killer如何处理OOM情况

当Linux出现OOM问题时,有两种应对方案,由内核参数panic_on_oom控制:

  • (1).panic_on_oom=2时,内核直接panic,即直接宕机
  • (2).panic_on_oom=0时,内核进行选择,选择要杀死的进程以便释放内存

panic_on_oom的默认值为0:

1
2
$ cat /proc/sys/vm/panic_on_oom   
0

panic_on_oom为0时即表示选择杀死哪个进程时又出现”选择谁”的问题。这由内核参数oom_kill_allocating_task控制:

  • (1).oom_kill_allocating_task=1时,内核直接杀死触发OOM问题的那个进程
  • (2).oom_kill_allocating_task=0时,内核根据”评分”系统选择最坏(bad)的进程杀死

为什么要区分两种情况呢?为什么还要评分而不直接杀死占用内存最多的进程呢?比如两类进程A和B,A类程序本身就是内存大户,它占用大内存是应该的,比如数据库服务进程,假设它占用15G内存都认为是合理的,但B类程序不应该占用太大内存,如果随着程序的运行,B类进程的内存疯狂增长,比如从100M增长到2G。这时应该杀死谁呢?

oom_kill_allocating_task参数的默认值为0,所以默认采用评分制决定杀死谁。

1
2
$ cat /proc/sys/vm/oom_kill_allocating_task 
0

内核的”评分”系统有三个相关的参数。

  • oom_score:记录各进程的分数,分数越高,越优先被杀掉
  • oom_score_adj:该参数可让用户可以调整进程的oom_scoreoom_score_adj的有效范围-1000~1000
    • 0值表示oom_score不采用用户打分,只采用系统内部自身对该进程的打分
    • 负值表示降低分值,即宽恕该进程,例如设置为-1000表示绝不杀死该进程
    • 正值表示提高分值,即惩罚该进程
  • oom_adj:它是旧的接口参数,其功能类似oom_score_adj,为了兼容,目前仍然保留这个参数,当操作这个参数的时候,kernel会换算成oom_score_adj

显然,对各个进程的打分是各进程自身的参数,而不是内核的参数。它们的路径为/proc/<PID>/oom_score/proc/<PID>/oom_score_adj