系统低内存后的处理

Low Memory Killer(低内存管理)

  对于PC来说,内存是至关重要。如果某个程序发生了内存泄漏,那么一般情况下系统就会将其进程Kill掉。Linux中使用一种名称为OOM(Out Of Memory,内存不足)的机制来完成这个任务,该机制会在系统内存不足的情况下,选择一个进程并将其Kill掉。Android则使用了一个新的机制——Low Memory Killer来完成同样的任务。下面首先来看看Low Memory Killer机制的原理以及它是如何选择将被Kill的进程的。

  1.Low Memory Killer的原理和机制

  Low Memory Killer在用户空间中指定了一组内存临界值,当其中的某个值与进程描述中的oom_adj值在同一范围时,该进程将被Kill掉。通常,在“/sys/module/lowmemorykiller / parameters/adj”中指定oom_adj的最小值,在“/sys/module/lowmemorykiller/parameters/minfree”中储存空闲页面的数量,所有的值都用一个逗号将其隔开且以升序排列。比如:把“0,8”写入到/sys/module/lowmemorykiller/parameters/adj中,把“1024,4096”写入到/sys/module/lowmemory- killer/parameters/minfree中,就表示当一个进程的空闲存储空间下降到4096个页面时,oom_adj值为8或者更大的进程会被Kill掉。同理,当一个进程的空闲存储空间下降到1024个页面时,oom_adj值为0或者更大的进程会被Kill掉。我们发现在lowmemorykiller.c中就指定了这样的值,如下所示:

static  int  lowmem_adj[ 6 ]  =  {
    
0 ,
    
1 ,
    
6 ,
    
12 ,
};
static 
int  lowmem_adj_size  =   4 ;
static size_t lowmem_minfree[
6 ]  =  {
    
3 * 512 ,  //  6MB
    
2 * 1024 ,  //  8MB
    
4 * 1024 ,  //  16MB
    
16 * 1024 ,  //  64MB
};
static 
int  lowmem_minfree_size  =   4 ;

  这就说明,当一个进程的空闲空间下降到3´512个页面时,oom_adj值为0或者更大的进程会被Kill掉;当一个进程的空闲空间下降到2´1024个页面时,oom_adj值为10或者更大的进程会被Kill掉,依此类推。其实更简明的理解就是满足以下条件的进程将被优先Kill掉:

  task_struct->signal_struct->oom_adj越大的越优先被Kill。

  占用物理内存最多的那个进程会被优先Kill。

  进程描述符中的signal_struct->oom_adj表示当内存短缺时进程被选择并Kill的优先级,取值范围是-17~15。如果是-17,则表示不会被选中,值越大越可能被选中。当某个进程被选中后,内核会发送SIGKILL信号将其Kill掉。

  实际上,Low Memory Killer驱动程序会认为被用于缓存的存储空间都要被释放,但是,如果很大一部分缓存存储空间处于被锁定的状态,那么这将是一个非常严重的错误,并且当正常的oom killer被触发之前,进程是不会被Kill掉的。

  2.Low Memory Killer的具体实现

  在了解了Low Memory Killer的原理之后,我们再来看如何实现这个驱动。Low Memory Killer驱动的实现位于drivers/misc/lowmemorykiller.c。

  该驱动的实现非常简单,其初始化与退出操作也是我们到目前为止见过的最简单的,代码如下:

static  int  __init lowmem_init(void)
{
    register_shrinker(
& lowmem_shrinker);
    return 
0 ;
}
static void __exit lowmem_exit(void)
{
    unregister_shrinker(
& lowmem_shrinker);
}
module_init(lowmem_init);
module_exit(lowmem_exit);

  在初始化函数lowmem_init中通过register_shrinker注册了一个shrinker为lowmem_shrinker;退出时又调用了函数lowmem_exit,通过unregister_shrinker来卸载被注册的lowmem_shrinker。其中lowmem_shrinker的定义如下:

static struct shrinker lowmem_shrinker  =  {
    .shrink 
=  lowmem_shrink,
    .seeks 
=  DEFAULT_SEEKS  *   16
};

  lowmem_shrink是这个驱动的核心实现,当内存不足时就会调用lowmem_shrink方法来Kill掉某些进程。下面来分析其具体实现,实现代码如下:

static  int  lowmem_shrink( int  nr_to_scan, gfp_t gfp_mask)
{
    struct task_struct 
* p;
    struct task_struct 
* selected  =   NULL ;
    
int   rem  = 0;
     int  tasksize;
    
int  i;
    
int  min_adj  =  OOM_ADJUST_MAX  +   1 ;
    
int  selected_tasksize  =   0 ;
    
int  array_size  =  ARRAY_SIZE(lowmem_adj);
    
int  other_free  =  global_page_state(NR_FREE_PAGES);
    
int  other_file  =  global_page_state(NR_FILE_PAGES);
    
if (lowmem_adj_size  <  array_size)
        array_size 
=  lowmem_adj_size;
    
if (lowmem_minfree_size  <  array_size)
        array_size 
=  lowmem_minfree_size;
    
for (i  =   0 ; i  <  array_size; i ++ ) {
        
if  (other_free  <  lowmem_minfree[i]  &&
            other_file 
<  lowmem_minfree[i]) {
            min_adj 
=  lowmem_adj[i];
            break;
        }
    }
    
if (nr_to_scan  >   0 )
        lowmem_print(
3 ,  " lowmem_shrink %d, %x, ofree %d %d, ma %d\n " , nr_to_scan, 
                 gfp_mask, other_free, other_file, min_adj);
    
rem  = global_page_state(NR_ACTIVE_ANON) +
        global_page_state(NR_ACTIVE_FILE)  +
        global_page_state(NR_INACTIVE_ANON) 
+
        global_page_state(NR_INACTIVE_FILE);
    
if  (nr_to_scan  <=   0  || min_adj  ==  OOM_ADJUST_MAX  +   1 ) {
        lowmem_print(
5 ,  " lowmem_shrink %d, %x, return %d\n " , nr_to_scan, gfp_mask, 
                 
rem );
        return  rem ;
    }

    read_lock(
& tasklist_lock);
    for_each_process(p) {
        
if  (p -> oomkilladj  <  min_adj || !p -> mm)
            continue;
        tasksize 
=  get_mm_rss(p -> mm);
        
if  (tasksize  <=   0 )
            continue;
        
if  (selected) {
            
if  (p -> oomkilladj  <  selected -> oomkilladj)
                continue;
            
if  (p -> oomkilladj  ==  selected -> oomkilladj  &&
                tasksize 
<=  selected_tasksize)
                continue;
        }
        selected 
=  p;
        selected_tasksize 
=  tasksize;
        lowmem_print(
2 ,  " select %d (%s), adj %d, size %d, to kill\n " ,
                     p
-> pid, p -> comm, p -> oomkilladj, tasksize);
    }
    
if (selected ! =   NULL ) {
        lowmem_print(
1 ,  " send sigkill to %d (%s), adj %d, size %d\n " ,
                     selected
-> pid, selected -> comm,
                     selected
-> oomkilladj, selected_tasksize);
        force_sig(SIGKILL, selected);
        
rem  -= selected_tasksize;
    }
    lowmem_print(
4 ,  " lowmem_shrink %d, %x, return %d\n " , nr_to_scan, gfp_mask,  rem );
    read_unlock( & tasklist_lock);
    return 
rem ;
}

  可以看出,其中多处用到了global_page_state函数。有很多人找不到这个函数,其实它被定义在了linux/vmstat.h中,其参数使用zone_stat_item枚举,被定义在linux/mmzone.h中,具体代码如下:

enum zone_stat_item {
    NR_FREE_PAGES,
    NR_LRU_BASE,
    NR_INACTIVE_ANON 
=  NR_LRU_BASE,
    NR_ACTIVE_ANON,
    NR_INACTIVE_FILE,
    NR_ACTIVE_FILE,
#ifdef CONFIG_UNEVICTABLE_LRU
    NR_UNEVICTABLE,
    NR_MLOCK,
#
else
    NR_UNEVICTABLE 
=  NR_ACTIVE_FILE,  /*  避免编译错误 */
    NR_MLOCK 
=  NR_ACTIVE_FILE,
#endif
    NR_ANON_PAGES,        
/*  匿名映射页面 */
    NR_FILE_MAPPED,        
/* 映射页面 */
    NR_FILE_PAGES,
    NR_FILE_DIRTY,
    NR_WRITEBACK,
    NR_SLAB_RECLAIMABLE,
    NR_SLAB_UNRECLAIMABLE,
    NR_PAGETABLE,
    NR_UNSTABLE_NFS,
    NR_BOUNCE,
    NR_VMSCAN_WRITE,
    NR_WRITEBACK_TEMP,    
/*  使用临时缓冲区 */
#ifdef CONFIG_NUMA
    NUMA_HIT,            
/*  在预定节点上分配 */
    NUMA_MISS,            
/*  在非预定节点上分配 */
    NUMA_FOREIGN,
    NUMA_INTERLEAVE_HIT,
    NUMA_LOCAL,            
/*  从本地页面分配 */
    NUMA_OTHER,            
/*  从其他节点分配  */
#endif
    NR_VM_ZONE_STAT_ITEMS };

  再回过头来看owmem_shrink函数,首先确定我们所定义的lowmem_adj和lowmem_minfree数组的大小(元素个数)是否一致,如果不一致则以最小的为基准。因为我们需要通过比较lowmem_minfree中的空闲储存空间的值,以确定最小min_adj值(当满足其条件时,通过其数组索引来寻找lowmem_adj中对应元素的值);之后检测min_adj的值是否是初始值“OOM_ADJUST_MAX + 1”,如果是,则表示没有满足条件的min_adj值,否则进入下一步;然后使用循环对每一个进程块进行判断,通过min_adj来寻找满足条件的具体进程(主要包括对oomkilladj和task_struct进行判断);最后,对找到的进程进行NULL判断,通过“force_sig(SIGKILL, selected)”发送一条SIGKILL信号到内核,Kill掉被选中的“selected”进程。

  关于Low Memory Killer的分析就到这里,在了解了其机制和原理之后,我们发现它的实现非常简单,与标准的Linux OOM机制类似,只是实现方式稍有不同。标准Linux的OOM Killer机制在mm/oom_kill.c中实现,且会被__alloc_pages_may_oom调用(在分配内存时,即mm/page_alloc.c中)。oom_kill.c最主要的一个函数是out_of_memory,它选择一个bad进程Kill,Kill的方法同样是通过发送SIGKILL信号。在out_of_memory中通过调用select_bad_process来选择一个进程Kill,选择的依据在badness函数中实现,基于多个标准来给每个进程评分,评分最高的被选中并Kill。一般而言,占用内存越多,oom_adj就越大,也就越有可能被选中。

 

冷血杀手OOM_Killer

Crane posted @ 2013年3月24日 02:22 in Linux with tags linux , 1224 阅读

 

前几天在折腾systemtap,其实我没想折腾来着,我就是想装个玩玩而已。需要有debug_info的内核,没问题啊,archlinux下这个easy,我从abs拉一个linux的PKGBUILD来改一下配置,按说立马就能make一个出来,我的linux实机上也确实没问题,一下子就出来一个,再装上systemtap,试两个命令,一切工作正常。但是我还有个windows下的虚拟机,有时在win下活动的时候偶尔也用用,就是在这货上装的时候出了问题,当开始编译的时候,我就关上屏幕睡觉去了,一觉起来,却发现没打好包,屏幕上提示什么链接vmlinuz的那个脚本报错,怪了,这脚本能有bug才怪。但是在公司的电脑上虚拟机却没问题,比了下是虚拟机设置的内存大小不一样,出问题的这个内存太小,于是我就去系统日志看了下,发现有句日志报了oom_killer启动把ld给杀掉了,我屮,就说链接的脚本报错,ld还没链完,就被干掉了,当然没结果了。知道问题就好办了,多开点swap让它干活去呗,也就啥事都没有了。
 
这个oom_killer就是Out Of Memory Killer,当系统的内存和交换区用尽的时候,系统为了保证可用性,会挑选出进程kill掉,为了证明是系统有意地行为,不是系统不稳定,不是系统bug,oom_killer会在干掉进程后,在系统日志里留下记录,大有此货是我杀,你能奈我何的风范。
 
一般系统日志中的输出类似这样(我当时的没留下来,从网上搜了一个)
python invoked oom-killer: gfp_mask=0x1200d2, order=0, oomkilladj=4
Pid: 13996, comm: python Not tainted 2.6.27-gentoo-r8cluster-e1000 #9

Call Trace:
 [] oom_kill_process+0x57/0x1dc
 [] getnstimeofday+0x53/0xb3
 [] badness+0x16a/0x1a9
 [] out_of_memory+0x1f2/0x25c
 [] __alloc_pages_internal+0x30f/0x3b2
 [] read_swap_cache_async+0x48/0xc0
 [] swapin_readahead+0x57/0x98
 [] handle_mm_fault+0x408/0x706
 [] do_page_fault+0x42c/0x7e7
 [] error_exit+0x0/0x51

Mem-Info:
Node 0 DMA per-cpu:
CPU    0: hi:    0, btch:   1 usd:   0
CPU    1: hi:    0, btch:   1 usd:   0
CPU    2: hi:    0, btch:   1 usd:   0
CPU    3: hi:    0, btch:   1 usd:   0
Node 0 DMA32 per-cpu:
CPU    0: hi:  186, btch:  31 usd: 103
CPU    1: hi:  186, btch:  31 usd:  48
CPU    2: hi:  186, btch:  31 usd: 136
CPU    3: hi:  186, btch:  31 usd: 183
Active:480346 inactive:483 dirty:0 writeback:10 unstable:0
 free:3408 slab:5146 mapped:1408 pagetables:2687 bounce:0
Node 0 DMA free:8024kB min:20kB low:24kB high:28kB active:1156kB inactive:0kB present:8364kB pages_scanned:3246 all_unreclaimable? yes
lowmem_reserve[]: 0 2003 2003 2003
Node 0 DMA32 free:5608kB min:5716kB low:7144kB high:8572kB active:1920228kB inactive:1932kB present:2051308kB pages_scanned:2941301 all_unreclaimable? yes
lowmem_reserve[]: 0 0 0 0
Node 0 DMA: 8*4kB 3*8kB 4*16kB 3*32kB 4*64kB 3*128kB 2*256kB 3*512kB 3*1024kB 1*2048kB 0*4096kB = 8024kB
Node 0 DMA32: 42*4kB 6*8kB 1*16kB 0*32kB 2*64kB 1*128kB 0*256kB 0*512kB 1*1024kB 0*2048kB 1*4096kB = 5608kB
325424 total pagecache pages
323900 pages in swap cache
Swap cache stats: add 20776604, delete 20452704, find 7856195/10744535
Free swap  = 151691424kB
Total swap = 156290896kB
524032 pages RAM
9003 pages reserved
331431 pages shared
186210 pages non-shared
Out of memory: kill process 12965 (bash) score 2236480 or a child
Killed process 13996 (python)
 
如果出现了这样的日志,就证明oom_killer已经干活完毕,不知哪个倒霉鬼进程已经挂在它手下了。要是生产环境中碰到这样的事情,生产进程被干掉了,就危险了,因此研究oom_killer挑选目标的方法也许就能帮助特别重要的进程逃过一劫。
 
当内存耗尽的时候,会触发oom_killer出来干活,oom_killer会遍历所有进程,并给每个进程打分,这里得分可不是什么好事,oom_killer扫完所有进程后会把得分最高的那个进程干掉。
 
oom_killer的评分会考虑很多因素,主要有这么几个方面(linux 3.7.10-1 mm/oom_kill.c),最低分0,最高1000.
  1. 进程的内存大小,包括RSS,页表,swap使用。1KB计一分,root进程减去3%的分
  2. proc/PID/oom_score_adj 参数的分数加上。这个值默认是0,范围[-1000,1000]
以前貌似评价标准有好多,现在只有简单的判断内存使用和sysctl参数了。比如这里的资料,评分标准就比较多:http://linux-mm.org/OOM_Killer
 
proc中有几个参数与oom killer有关系
  1. /proc/sys/vm/panic_on_oom 如果设置为1,那么oom killer启动的时候就会触发kernel panic。默认是0,咋一看设置为非0没啥用,但是kernel文档说的是集群中可以用来做failover,也可以设置为panic_on_oom=2+kdump,可以得到一个内核dump事后分析。
  2. /proc/sys/vm/oom_kill_allocating_task 设置为非0,则触发oom的进程会收到信号,不再对其他进程评分。默认是0。
  3. /proc/sys/vm/oom_dump_tasks 设置为非0,oom killer启动时就会输出进程列表,打印vm相关信息,rss,页表,swap使用,oom_score_adj,进程名字,pid,可以查看oom_killer选择的依据。默认是1。
  4. /proc//oom_score_adj 评分的时候可以用来调整分数,负值可以减少评分,正值可以增加评分,取值-1000到1000,-1000可以完全禁止oom killer
  5. /proc//oom_adj和/proc//oom_score 兼容老的内核,oom_adj,取值从-16到15,-17可以禁止oom killer kill这个进程。oom_score显示oom killer评出来的分数。

 

你可能感兴趣的:(Kernel)