1.1      Oom

1.1.1      简介

Oom的全称是out-of-memory,是内核在处理系统内存不足而又回收无果的情况下采取的一种措施,内核会经过选择杀死一些进程,以释放一些内存,满足当前内存申请的需求。所以oom是一种系统行为,对应到memcg的oom,其原理和动机跟全局oom是一样的,区别只在于对象的不同,全局oom的对象是整个系统中所有进程,而memcg oom只针对memcg中的进程(如果使能了hierarchy,还包括所有子memcg中的进程),这里的对象主要是指oom时内核选择从哪些进程中杀死一些进程,所以memcg的oom只可能杀死属于该memcg的进程。

1.1.2      实现

跟全局oom一样,memcg的oom也分成select_bad_process和oom_kill_process两个过程:

a.     select_bad_process找出该memcg下最该被kill的进程(如果memcg设置了hierarchy,也会考虑子memcg下的进程);

b.    oom_kill_process杀掉选中的进程及与其共用mm的进程(杀进程的目的是释放内存,所以当然要把mm的所有引用都干掉);

对于实现的代码细节,不同的版本代码演进较快,之前memcg中会直接调用内核的select_bad_process和oom_kill_process,在最新的3.10中,memcg实现了自己的select_bad_process,即在memcg的代码中自己来找到要杀死的进程。虽然函数调用不同,但是找到要杀死的进程的原理都是类似的,select的过程会给memcg(或及其子memcg)下的每个进程打一个分,得分最高者被选中。评分因素每个版本不尽相同,主要会考虑以下因素:

a.     进程拥有page和swap entry越多,分得越高;

b.    可以通过/proc/$pid/oom_score_adj进行一些分值干预;

c.     拥有CAP_SYS_ADMIN的root进程分值会被调低;

 

kill的过程比较简单,简单的说就是向要杀死的进程发送SIGKILL信号,但其中依然有一些细节:

a.     如果被选中的进程有一些子进程跟他不共用同一个mm,并且也是可以被杀死的,那么就挑选这些子进程中badness得分最高的一个来代替父进程被杀死,这样是为了确保我们在释放内存的同时失去更少的东西;

b.    上面已经说了,oom_kill的过程会杀死选中的进程及与其共用mm的进程,所以会遍历所有用户态进程,找到并杀死与选中进程共用同一个mm的进程;

c.     遍历进程的过程中,会过滤掉通过/proc/$pid/oom_score_adj干预的不可被oom_kill掉的进程(目前是设置为OOM_SCORE_ADJ_MIN的进程);

 

在oom的过程中,另外值得一说的是其中的同步过程。

oom过程会向选中的进程发送SIGKILL信号,但是距离进程处理信号、释放空间,还是需要经历一定时间的。如果系统负载较高,则这段时间内很可能有其他上下文也需要却得不到page,而触发新的oom。那么如果大量oom在短时间内爆发,可能会大面积杀死系统中的进程,带来一场浩劫。

所以oom过程需要同步:在给选中的进程发送SIGKILL后,会设置其TIF_MEMDIE标记。而在select被杀死进程的过程中如果发现记有TIF_MEMDIE的进程,则终止当前的oom过程,并等待上一个oom过程结束。这样做可以避免oom时大面积的kill进程。

而在进程退出时,会先将task->mm置为NULL,再mmput(mm)释放掉引用计数,从而导致内存空间被释放(如果引用计数减为0的话)。所以,只要task->mm被置为NULL(内存即将开始释放),就没人认得它是属于哪个memcg的了,针对那个memcg的新的oom过程就可以开始。

1.1.3      关闭oom

从oom的实现可知,虽然在oom的设计中已经考虑的各种情况,并采取了各种措施来让oom在杀死进程释放内存的同时对系统造成最小的伤害,并且这种努力和改进一直在继续。但系统的环境复杂多变,内核永远无法完美的识别某个进程对系统的重要性,在实际应用中,oom常常会造成未知的影响,所以我们有时会希望可以关闭oom,而memcg也正好提供了这样的功能。

在memcg的用户态目录下,memory.oom_control文件提供了这样的接口,通过查看该文件:

Cgroup-memory子系统分析(2)

其中显示了两个字段,oom_kill_disable表示是否使能了该memcg的oom_kill,1为使能,0为关闭;而under_oom则表示当前memcg是否在oom过程中。

该文件的特殊性在于其显示的内容和写入的内容不同,对该文件,我们只可以写入0或1,表示关闭或开启该memcg的oom。当写入0关闭oom时,当某个进程遇到上面说的内存不足而又回收无果的情况下,内核会将该进程放到memcg的oom等待队列中,休眠该进程,等到有足够的内存可以使用时,再唤醒该进程。这样就可以避免出现进程被意外杀死的情况,但是这么做的话,需要确保你的进程可以接受睡眠。

对于全局的oom,并没有提供关闭的功能,这是因为全局的oom可能在任何情况下触发,比如中断上下文等,在这些情况下是不能睡眠的,否则可能造成比杀死某个进程更严重的后果。

1.2      Kmem

1.2.1      概述

Kmem controller是对mem controller的补充,原来的memcg只能对用户程序申请的用户态内存进行统计和管理,kmem只有网络tcp部分的支持,最近几个版本才加入了kmem controller的支持,彻底支持了对内核内存的统计和管理。但截止目前(3.12),kmem controller还是有很多不完善,还不建议在商用中使用。

因为kmemcontroller是在memcg的基础上加上的一个相对独立的特性,所以本文用一种新的方式,即通过分析完整加入整个kmem controller的patchset,选取最重要的几个部分,只要大家对整个kmem controller是如何一步一步实现的,就可以对整个kmem controller的实现及原理有了清晰的认识。

阅读本节需要对原来的memcg实现有一定的了解,同时对内核的slab/slub机制有一定的了解,因为kmem controller的本质就是让每个memcg管理自己的slab/slub。(本文分析的代码基于主线3.12)

1.2.2      实现

完整的patch在:https://lkml.org/lkml/2012/11/1/171

原始patchset一共29个,但本文只会选取其中最主要的几个进行分析,通过这些patch将会了解整个kmemcontroller的实现步骤和原理。

一.实现kmem accounting的基本框架

Upstream commit: 510fc4e11b772fd60f2c545c64d4c55abd07ce36

[04/29] memcg: kmem accounting basic infrastructure

主要是在mem_cgroup中增加了struct res_counter kmem,同时增加了kmem统计和限制相关的cgroup文件及其处理函数,在这部分的处理上跟原来mem cgroup中的处理是完全一样的,只是对象换成了kmem。

另外,这里需要说明的是kmem cgroup设计的统计方法:所有统计在kmem counter上的内存同时也会统计在mem counter上,所以在设置内存限制时,如果要实现对kmem的限制,那么只有将memory.kmem.limit_in_bytes设置成小于memory.limit_in_bytes才是有意义的。

二.实现kmem controller的基本框架

Upstream commit: 7ae1e1d0f8ac2927ed7e3ca6d15e42d485903459

[06/29] memcg: kmem controller infrastructure

这个patch很关键,它实现了kmemcontroller的核心框架。

即对memcg中使用的内核内存进行统计跟踪,只要进程在某个cgroup内(非root cgroup),而且内存申请中有__GFP_KMEMCG标志,该patch引入的controller框架就会对申请的内存进行统计(利用上一步加入的kmem account),该框架主要实现了三个接口:

1.   memcg_kmem_newpage_charge

如果cgroup可以满足内存申请(还没有超过上限),则会更新相应的account,并返回true,此时page还没有被分配。

这个patch只是加了这个接口,后续该接口会在具体的地方被调用,目前只有在伙伴算法的核心函数__alloc_pages_nodemask中被调用。需要注意的是,在有一些情况下,对kmem的申请是不会统计进来的,目前有:

a.     申请时没有加__GFP_KMEMCG标记;

b.    申请时加了__GFP_NOFAIL标记;(因为加了该标记即使charge失败了内存申请也要继续,所以这里的策略是干脆不统计了,该策略并不完美,后续可能会修改)

c.     在中断上下文中;

d.    当前进程是线程组的线程(!current->mm);

e.     当前线程是内核线程(current->flags & PF_KTHREAD);

所以即使把内核线程加入cgroup中,它们使用的kmem,也是不会统计进来的。

2.   memcg_kmem_commit_charge

如果申请内存失败,这里会执行计数的恢复操作,如果内核申请成功了,就会绑定该page和cgroup的关系(设置该page对应的page_cgroup的mem_cgroup成员指向进程所在memcg,同时更新page_cgroup的标记,说明该page已经在某个cgroup中了);

charge和commit_charge是在一起用的,目前都只在__alloc_pages_nodemask函数中用,即先charge,成功后再申请内存,然后commit_charge。

3.   memcg_kmem_uncharge_pages

在free的时候调用,主要做计数的恢复操作。

三.实现slab/slub跟memcg的关系

Upstream commit: ba6c496ed834a37a26fc6fc87fc9aecb0fa0014d

[15/29] slab/slub: struct memcg_params

这个patch很小,但它建立了slab/slub跟memcg的关系,上面两个patch都只是计数和控制的框架,还没有具体的对象让这个框架来操作,而它操作的对象正是memcg中对应的slab/slub,建立这个联系后,后面才会一点点把操作对象加进来。

Cgroup-memory子系统分析(2)

该patch加入了这个结构(patch本身加入的该结构体要简单很多,这个是3.12代码中最新的结构体),kmem_cache结构体中增加了指向该结构体的成员。参考附录了解slub的基本机构,加入kmem controller后,跟原来相比的变化是:

1.    原来一类object对应一个kmem_cache,现在同一类object会有多个kmem_cache,其中原来的kmem_cache现在作为root cache,其他的kmem_cache是每个memcg对应一个,都作为root cache的child cache;

这里很重要的是引入了rootcache的概念,可以简单的理解,即在加入kmem controller之前,原来内核中所有的kmem_cache,在加入kmem controller之后,这些kmem cache都变成了root cache,而之后为每个memcg创建的kmem_cache,都是相应的kmem_cache的child cache;

2.    这些新加入的kmem_cache也会加入到slab_caches链表中(同一个memcg中使用的kmem_cache也会通过memcg_cache_params->list链接在一起),所以相对原来,内核中kmem_cache的数量将大大增加,不过memcg中的kmem_cache只有在使能了kmem.limit_in_bytes,且执行了slab内存申请的操作,才会创建具体的kmem_cache。

对于这些关系,我们常常会利用数据结构间的联系做一些查找,比如

a.     通过kmem_cache找到对应的memcg:

Kmem_cache --> 找到 memcg_cache_params --> 找到memcg。

b.    通过root cache和memcg找到对应的kmem_cache:

Root cache --> 找到 memcg_cache_params,加上memcg的id -->memcg对应的kmem_cache。

        为了实现上面提到的一些功能,patchset中其他一些patch辅助实现了一些函数,比如创建memcg对应的kmem_cache,将对应的kmem_cache与memcg建立联系等,这些patch不在本文一一列出。

四.实现memcg_kmem_get_cache接口

Upstream commit: d7f25f8a2f81252d1ac134470ba1d0a287cf8fcd

[19/29] memcg: infrastructure to match an allocation to theright cache

实现了memcg_kmem_get_cache接口,即某进程在尝试获取某kmem_cache时,获取到的是该进程所在cgroup的kmem_cache。其中需要注意的是:当某个cgroup中的进程第一次获取某kmem_cache时,返回的还是root kmem_cache,同时将创建该cgroup对应的kmem_cache的任务加入work queue中异步进行,下次获取就可以拿到cgroup对应的kmem_cache。

另外,在创建cgroup对应的kmem_cache后,需要维护该kmem_cache的数据关系,比如对应的memcg_cache_params,以及跟root kmem_cache间的指向关系等,这个操作放到kmem_cache_create_memcg中,通过memcg_register_cache函数实现。

这一步跟第三步中提到的一些,如创建memcg对应的kmem_cache,这些patch进一步为memcg中的进程使用自己的slab/slub做了准备。

五.实现slab/slub中内存分配的memcg适配

Upstream commit: d79923fad95b0cdf7770e024677180c734cb7148

[22/29] sl[au]b: allocate objects from memcg cache

这里正式将slab/slub中的内存管理跟memcg对接了起来。

主要有两个地方:

1. kmalloc正常走__kmalloc时在__cache_alloc时,将传入的kmem_cache重新通过memcg_kmem_get_cache获取到memcg中的kmem_cache。

2. 如果kmalloc的内存过大,走kmalloc_large到伙伴系统分配内存时,在内存分配标记中增加__GFP_KMEMCG,这样可以将分配的内存统计到相应的memcg中。

 

至此,在slab/slub的内存分配中使用了上面各个步骤添加的一系列接口,在内存分配时,如果使能了kmemcontroller,且申请内存的进程在非rootcgroup内,则在通过传入的size获取到对应的kmem_cache后,调用会调用接口memcg_kmem_get_cache通过该root cache获取到对应的memcg使用的kmem_cache(如果是第一次申请该kmem_cache中的object,则返回的还是该root cache,然后将创建该memcg对应的该kmem_cache的操作放到work queue中,下次就可以直接获取到该memcg对应的该kmem_cache);如果分配的内存过大,不走slab/slub,则会在内存分配的flag中增加__GFP_KMEMCG,直接通过伙伴系统分配内存,并将内存计数统计到memcg对应的kmem account中。(在走slab/slub时,在创建memcg对应的kmem_cache时,分配的内存也是从伙伴系统中得到,这些内存也已经统计到memcg对应的kmem account中了)所以我们就可以对memcg中使用的内核内存进行计数和控制,实现了kmem controller。

1.2.3      现状

Kmem controller的特性是在2012年底加入upstream的,但之后该特性的开发者转去做其他的项目,一直到现在(2013年底),kmem controller的相关代码并没有什么改变和发展,实际上目前的代码并不完善,还不能真正应用到实际项目中,因为目前对memcg中进程使用的kmem,还只是简单的统计,如果超过了限制的上限,对于分配kmem的请求会直接返回失败。对于memcg使用的kmem的内存回收,还依赖全局的内存回收机制(memcg的内存回收机制还只能回收用户态内存)。所以要实现完整的kmemcontroller,我们还需要memcg实现per memcg的kmem reclaim机制。

目前,已经有人在做这个事了,正在review中,在不久的将来,kmemcontroller应该会逐步完善。

2      附录

2.1      Slub结构图

Cgroup-memory子系统分析(2)

1.   内核中所有的slab通过slab_caches链接起来;

2.   每个kmem_cache结构代表一类object对象,比如inode等;

3.   Kmem_cache_cpu存放当前cpu使用的cache对象(避免不同cpu同时取partial链表上对象时导致的竞争问题和加锁导致的开销);

4.   Kmem_cache_node存放针对不同内存节点的对象;

5.   Kmem_cache_node下的partial链表链接了可用的page,page中包含了最终使用的object;

(更多slab/slub信息请参考其他相关资料)


相关文章:

  • 2022-12-23
  • 2022-01-21
  • 2022-12-23
  • 2022-02-07
  • 2021-08-12
  • 2022-01-10
  • 2021-07-29
  • 2022-02-07
猜你喜欢
  • 2021-10-15
  • 2021-06-11
  • 2021-07-21
  • 2021-10-13
  • 2021-10-26
  • 2022-12-23
  • 2021-06-01
相关资源
相似解决方案