转自 http://hi.baidu.com/_kouu/item/56ffc80934780110addc70d1node
memory cgrouplinux
mem_cgroup是cgroup体系中提供的用于memory隔离的功能。
admin能够建立若干个mem_cgroup,造成一个树型结构。能够将进程加入到这些mem_cgroup中。(相似这样的管理功能都是由cgroup框架自带的。)算法
为了实现memory隔离,每一个mem_cgroup主要有两个维度的限制:
一、res - 物理内存
二、memsw - memory + swap,物理内存 + swap
其中,memsw确定是大于等于memory的。
另外注意,memory控制是针对于组的,而不是单个进程的。(固然,你也能够一个进程一个组。)框架
每一个维度又有三个指标:
一、usage - 组内进程已经使用的内存
二、soft_limit - 非强制内存上限。usage超过这个上限后,组内进程使用的内存可能会被加快步伐进行回收
三、hard_limit - 强制内存上限。usage不能超过这个上限。若是试图超过,则会触发同步的内存回收过程,或者OOM(挑选并杀掉一个进程,以释放空间。见《linux页面回收浅析》)
其中,soft_limit和hard_limit是由admin在mem_cgroup的参数中进行配置的(soft_limit确定是要小于hard_limit才能发挥其做用)。而usage则是由内核实时统计该组所使用的内存值。线程
mem_cgroup有hierarchy的概念。若是设置某个组的hierarchy为真,则其子组的计数会累加到它身上;而在它须要回收page时,也会尝试对子组进行回收;OOM时也会考虑杀掉子组中的进程;
反过来,若是hierarchy为假,则子组跟父组就是形同陌路的两个组了,仅仅在cgroup的层次结构上有父子关系,实则没有任何联系。计数、回收、OOM都是各顾各的。(另外一个影响在于mem_cgroup的删除,下文会提到。)
一个mem_cgroup建立的时候老是继承其父组的hierarchy。指针
usage调试
讨论mem_cgroup,第一个问题就是:内存的usage如何统计,也就是如何对res/memsw的usage计数进行charge/uncharge。对象
首先,在mem_cgroup的内存统计逻辑中,有一个基本思想:一个page最多只会被charge一次,而且通常就charge在第一次使用这个page的那个进程所在的mem_cgroup上。
若是有多个mem_cgroup的进程引用同一个page,也只会有一个mem_cgroup为它埋单。
其次,uncharge每每是跟page的释放相对应的。这就意味着mem_cgroup为它再也不使用的page埋单是正常现象。
一个进程引用了某个page,使其所在的mem_cgroup被charge;随后该进程再也不引用这个page,不过这个page可能由于某种缘由不能被释放,因此对应的mem_cgroup就不能获得uncharge。继承
page进程
那么对于usage的统计来讲,当进程使用到新的page时,怎么知道这个page有没有charge过,是否应该charge相应的mem_cgroup呢?
而当进程释放page时,又须要知道这个page是由哪一个mem_cgroup charge的,以便给它uncharge。
内核的作法是,给page安排一个指向mem_cgroup的指针,非NULL的指针表示这个page已经charge过了,而page释放时也能够经过该指针得知应该uncharge那个mem_cgroup。
不过实际上这个指向mem_cgroup的指针并不存在于page结构,而是在对应的page_cgroup结构中。
为了支持mem_cgroup,内核维护了一组跟page结构一一对应的page_cgroup,其主要成员为:
mem_cgroup - 指向一个mem_cgroup
lru - 链入mem_cgroup的lru(见后面对reclaim的讨论)
由此可知,设一个mem_cgroup-A的res计数为N,那么必有N个这样的page,其对应的page_cgroup->mem_cgroup指向mem_cgroup-A(或其子组)。
(理论上是这样,而实际会有所出入。见后面关于per-CPU的stock的讨论。)
swap
而后,关于swap呢?page的内容可能被swap-out到交换区,从而释放page。
能够想象,这将致使对应mem_cgroup的res计数获得uncharge,memsw计数不变。而当这个swap entry被释放时,memsw计数才能uncharge。
因此,swap entry也应该有一个相似于page_cgroup->mem_cgroup的指针,可以找到为它埋单的那个mem_cgroup。
相似的,swap entry会有一个与之对应的swap_cgroup结构,其主要成员为:
id - 对应mem_cgroup在cgroup体系中的id,经过它可以获得对应的mem_cgroup
由此可知,设一个mem_cgroup-B在cgroup体系中的id为id-B,其memsw计数为M。
那么必有I个这样的page,其对应的page_cgroup->mem_cgroup指向mem_cgroup-B(或其子组);和J个这样的swap entry,其对应的swap_cgroup->id为id-B(或其子组)。且M == I + J。
(理论上是这样,而实际会有所出入。见后面关于per-CPU的stock的讨论。)
相对应的状况是swap-in,这时会分配新的page,而后从新charge相应的mem_cgroup的res计数。这个要被charge的mem_cgroup怎么取得呢?其实并非page_cgroup->mem_cgroup,而是swap_cgroup->id对应的mem_cgroup。由于swap-in时的这个page是从新分配出来的,已经不是当年swap-out时的那个page了(新的page里面会装上跟原来同样的内容,可是没人保证两个page是同一个物理页面),因此此时的page_cgroup->mem_cgroup是无心义的。固然,swap-in完成以后,新的page对应的page_cgroup->mem_cgroup会被赋值,指向swap_cgroup->id对应的mem_cgroup,而swap_cgroup则被回收掉。
mm owner
另外,通常咱们会说某某进程使用了某些page。可是实际上,进程和page并非直接联系的,而是:进程 => mm => page。也就是说,对物理内存的计数是跟mm相关的。
而mem_cgroup倒是跟进程相关的(cgroup体系是按进程来分组的)。在一个mm上发生内存使用/释放时,须要找到对应的进程,再找到对应的mem_cgroup,而后charge/uncharge。
但问题是,mm到进程多是一对多的关系,多个进程引用同一个mm(好比vfork产生的子进程、clone产生的线程、等)。如何定义mm应该对应哪一个进程呢?
这里就用到了mm->owner的概念,每一个mm有其对应的owner进程。fork时父进程将本身的mm copy一份给子进程,因而子进程拥有了自已的mm,它就是这个新mm的owner。
而若是是vfork、clone致使子进程共享父进程的mm时,mm的owner依然是父进程。而相似这样的子进程则不是任何mm的owner(未来多是,好比evecve之后)。
因而,经过mm->owner就打通了page => mm => 进程 => mem_cgroup的路径。同时也意味着,对于那些不是任何mm的owner的进程,它们存在于哪一个mem_cgroup实际上是可有可无的。
charge/uncharge
mem_cgroup统计的对象主要是用户空间使用的内存,分匿名映射(anon page)和文件映射(page cache)两种类型的page。而这两种page又存在swap的状况。
至于其余的内存,则是由内核空间使用的,不在统计之列。
下面就分别来看看这些page是如何计数的。
page cache
page cache的计数原则是:谁把page请进了page cache,对应的mem_cgroup就为此而charge。主要有这么几种状况:
一、read/write系统调用;
二、mmap文件以后,在对应区域进行内存读写;
三、伴随1和2两种状况产生的预读;
反之,当page被释放(通常就在它离开page cache之时),对应的mem_cgroup得以uncharge。主要有这么几种状况:
一、page回收算法将page cache中的page回收;
二、使用direct-io致使对应区域的page cache被释放;
三、相似/proc/sys/vm/drop_caches、fadvice(DONTDEED)这样的方式主动清理page cache;
四、相似文件truncate这样的事件形成对应区域的page cache被释放;
五、等等;
注意,使用direct-io方式进行read/write是不跟page cache打交道的,因此mem_cgroup也不会所以而charge。(固然,read/write须要一块buffer,这个是要charge好的。)
NOTICE:若是某个mem_cgroup内的进程访问了某些文件,从而填充了它们的page cache。那么这个mem_cgroup就成了冤大头,一直要等到page被从page cache里释放掉,才能uncharge。就算这个进程早已再也不使用这些数据了。而与此同时,其余mem_cgoup的进程则能够无偿使用这些page。因此,使用相同数据的进程应该尽量划分到同一个mem_cgroup中。
page cache的swap状况。这主要涉及tmpfs和shm的逻辑,它们表面上看跟文件映射没什么两样,每一个文件(或shmid)都有着本身的page cache,而且均可以按照文件的那一套逻辑来操做。
但它们倒是彻底基于内存的,并无外设做为存储介质。当须要回收page的时候,只能swap。
swap-out,在page被释放时uncharge对应mem_cgroup的res计数,memsw计数不变:
a、page在离开page cache后并不会立刻释放,而是先被移动到swap cache、而后swap到交换区、最后才能释放;
b、交换区是有大小限制的,若是分配swap entry不成功,则page不能被回收,依然放在page cache中;
c、直到page释放,才uncharge;
swap-in,在page从新回到page cache时charge:
a、page先被读入(或预读)swap cache,此时并无charge操做;
b、随后,须要swap-in的page会从swap cache移动到page cache,此时对应mem_cgroup的charge;
c、而其余被预读进swap cache的page,并不会引发charge,也不会被移动到page cache,直到它真正须要swap-in时;
NOTICE:swap cache与page cache的不一样。
二者均可能会有预读,可是swap cache里面的page只有当真正要使用的时候才会charge,而page cache只要读进cache就charge。
由于文件预读是为操做它的进程服务的,而swap预读则未必,交换区里的数据多是离散的,属于不一样的进程。
anon page
anon的计数原则是:谁分配了page,谁就为此而charge。主要有这么几种状况:
一、写一个未创建映射的属于匿名vma的虚拟内存时,page被分配,并创建映射;
二、写一个待COW的page时,新page被分配,并从新创建映射。这些待COW的page可能产生于以下场景:
a、读一个未创建映射的属于匿名vma的虚拟内存时,page不会被分配,并且将相应地址临时只读的映射到一个全0的特殊page,等待COW;
b、fork后,父子进程会共享原来的anon page,而且映射被更改成只读,等待COW;(在COW以前若是对page的引用已经减为1,则不须要分配新page,也就不须要再charge。)
c、private文件映射的page是以只读方式映射到page cache中的page,等待COW;(比较有趣的状况,新的page是anon的,而对应的vma仍是映射到文件的。)
反之,当page被释放(通常在对它的映射彻底撤销时),对应的mem_cgroup得以uncharge。主要有这么几种状况:
一、进程munmap掉一段虚拟内存,则对应的已经映射的page会被减引用,可能致使引用减为0而释放;(好比主动munmap、exit退出程序、等。)
NOTICE:若是父子进程不在同一个mem_cgroup,则对于fork后那些还没有COW的anon page来讲,极可能是charge在父进程所对应的mem_cgroup上的。父进程就算撤销了映射,计数依然会算在它头上(直到page被释放)。而若是是由于父进程的写操做引起了COW,则新分配的page和老的page都要算在父进程头上。
不过子进程默认是跟父进程在同一个mem_cgroup的,除非刻意去移动它。
anon page可能被page回收算法swap掉,也会致使对应mem_cgroup的res计数uncharge。
swap-out,在page的最后一个映射被撤销时uncharge;
a、swap-out时,anon page会先放放置在swap cache上,而后对每个映射它的进程进行unmap(前提是分配swap entry成功,不然不会swap-out);
b、在最后一个映射被撤销时进行uncharge;
c、映射撤销后,这个page可能还会呆在swap cache上,等待写回交换区(不过写不写回已经不影响mem_cgroup的计数了);
swap-in,在page的第一个映射创建时charge;
a、对swap page的缺页异常,以及由此触发的预读,将致使新page被分配,并放到swap cache,再从交换区读入数据;
b、新page被放到swap cache并不会致使对应mem_cgroup的charge;
c、等这个新page第一次被映射的时候,对应mem_cgroup才会charge;
NOTICE:对于共享的anon page,charge在第一次映射它的mem_cgroup上。若是swap-out,再被其余mem_cgroup的进程swap-in,则仍是计在原来的mem_cgroup上。
由于swap-out后,原mem_cgroup的memsw计数是没有改变的,因此也不能由于swap-in而改变。
anon page被多个进程共享主要是fork()时父子进程共享这一种状况。
总的来讲:
page cache里的page,charge/uncharge是以page加入/脱离page cache为准的;
anon page,charge/uncharge是以page的分配/释放为准的;
swap的page,charge/uncharge是以page被使用/未使用为准的;
reclaim
page回收的过程详见《linux页面回收浅析》。
page要被回收,首先是要加入到lru。区别于内核中早已经存在的全局lru,每一个mem_cgroup都独自维护了一组lru。
mem_cgroup下的lru跟全局lru的构成是相似的,对于每一个NUMA node下的每个zone,会有一套lru。而lru又包含active_file、inactive_file、active_anon、inactive_anon、等若干个list。
page被加入到lru的时候,老是会找到本身所归属的NUMA node和zone,而后根据自身属性,加入其中一个lrulist。
上面提到的两种page都会被加入到全局的lru,若是它归属于某个mem_cgroup的话,也会被加入该mem_cgroup的lru。
一个page怎么加入两个lru呢?其实加入全局lru的是page,而加入mem_cgroup的lru的则是其对应的page_cgroup(前面已经介绍了page_cgroup有lru这么个成员)。
lru
总的来讲,anon page和page cache都是在分配的时候分加入lru、释放前脱离lru。
anon page:
一、alloc => add_lru => del_lru => free
二、alloc => add_lru => add_to_swap_cache => del_from_swap_cache => del_lru => free
page cache:
一、alloc => add_lru => add_to_page_cache => del_from_page_cache => del_lru => free
二、alloc => add_lru => add_to_page_cache => add_to_swap_cache => del_from_page_cache => del_from_swap_cache => del_lru => free
而可以被swap的page,包括anon page和属于tmpfs/shm的page cache,老是加入anon对应的lrulist。其余的page cache中的page老是加入file对应的lrulist。
reclaim
reclaim有三条路径:
一、普通的reclaim流程(包括kswapd和内存紧缺时的主动回收)。
这个是视整个系统的内存使用状况而定的,有无mem_cgroup都同样。
注意,在普通的reclaim流程中一样可能回收掉属于某个mem_cgroup的page,从而致使对该mem_cgroup的uncharge。
二、普通的reclaim流程中额外会尝试对soft limit超额最多的几个mem_cgroup进行回收。
这里就是soft limit主要产生做用的地方。
三、在试图对mem_cgroup作charge的时候,若是hard_limit超额,会同步地对其进行页面回收,以便charge成功;
这三个回收过程走的基本上是同一个逻辑:扫描lru,将active链表中的一些老page移动到inactive链表、对inactive链表中的一些老page进行回收。
略有不一样之处在于:
一、普通的回收流程关心的是全局的lru,然后两种则是关心特定mem_cgroup的lru;
二、按照lru的组织结构,在尝试回收一个mem_cgroup时,要先选定mem_cgroup => NUMA node => zone,才能获得一个lru:
A、mem_cgroup。若是设置了hierarchy,回收逻辑会在mem_cgroup本身及其子孙mem_cgroup间轮循一个进行回收。不然就只能回收本身;
B、NUMA node。hard limit超限时会轮循一个NUMA node;而soft limit超限时则是使用普通的reclaim流程所针对的NUMA node(好比分别有一个kswapd线程来对每个NUMA node进行回收);
C、zone。hard limit超限时会对全部zone尝试进行回收;而soft limit超限时则是随普通的reclaim流程对须要reclaim的zone进行回收;
三、hard limit超限时可能存在no-swap逻辑,若是是memsw超限的话,swap-out是无心义的;
四、hard limit超限时一次回收过程可能没法释放足够的page,则继续进行回收(会轮循到不一样的子mem_cgroup和NUMA node),最终回收无果还会进入oom逻辑;而soft limit超限时则没有回收数目的要求;
五、等等;
oom
就像内核在系统内存不足且回收无果的状况下会进入oom流程同样,在尝试charge超过hard limit状况下,若是同步的回收过程没法回收足够的page,也会进入oom流程。
固然,针对特定mem_cgroup的oom,只会挑选属于该mem_cgroup的进程来kill。
跟全局的oom同样,mem_cgroup的oom也分红select_bad_process和oom_kill_process两个过程:
一、select_bad_process找出该mem_cgroup下最该被kill的进程(若是mem_cgroup设置了hierarchy,也会考虑子mem_cgroup下的进程);
二、oom_kill_process杀掉选中的进程及与其共用mm的进程(杀进程的目的是释放内存,因此固然要把mm的全部引用都干掉);
其中仍是有很多细节的:
一、select_bad_process认为谁最该死?
select_bad_process会给mem_cgroup(或及其子mem_cgroup)下的每一个进程打一个分,得分最高者被选中。评分因素每一个版本不尽相同,主要会考虑如下因素:
a、进程拥有page和swap entry越多,分得越高;
b、能够经过/proc/$pid/oom_score_adj进行一些分值干预;
c、拥有CAP_SYS_ADMIN的root进程分值会被调低;
不过我以为既然是在mem_cgroup中,进程所在的mem_cgroup超出其soft_limit的比例也能够做为一个评分因素。YY一下:
d、若是进程所属的mem_cgroup的soft_limit超限,分值会按超限额增长必定比例的分值;
二、oom时机
oom是在同步的reclaim流程没法回收足够的page时触发的。可是reclaim流程没法继续回收,其实并不表明绝对的不可回收。
好比active的page、装有可执行代码的page、等都是尽可能不要去回收的。
由于在一个上下文进行reclaim的时候,其余的上下文还各自在干其余的事情,无时不涉及内存的使用。
那么,若是你把能回收的page都回收了,随着其余上下文的运行又会把不少page恢复回来。其结果极可能最终仍是没能回收到空间,却徒增了换入换出的开销。
因此,虽然说oom是在内存回收无果时触发的,却也并不是彻底不能再回收。至于其中的“度”,也只能靠调试和经验来把握了。
三、oom过程同步
oom过程会向选中的进程发送SIGKILL进程。可是距离进程处理信号、释放空间,仍是须要经历必定时间的。
若是系统负载较高,则这段时间内极可能有其余上下文也须要却得不到page,而触发新的oom。那么若是大量oom在短期内爆发,可能会大面积杀死系统中的进程,带来一场浩劫。
因此oom过程须要同步:在给选中的进程发送SIGKILL后,会设置其TIF_MEMDIE标记。而在select_bad_process的过程当中若是发现记有TIF_MEMDIE的进程,则终止当前的oom过程,并等待上一个oom过程结束。
这样作能够避免oom时大面积的kill进程,可是目前并无保证每次oom只会kill一个进程(假设kill的这个进程已经可以释放足够的空间)。
由于在一个mem_cgroup下触发oom时,应该选择该mem_cgroup下的进程。而一个进程是否属于这个mem_cgroup,看的是mm->owner是否属于这个mem_cgroup。
而在进程退出时,会先将task->mm置为NULL,再mmput(mm)释放掉引用计数,从而致使内存空间被释放(若是引用计数减为0的话)。
因此,只要task->mm被置为NULL(内存即将开始释放),就没人认得它是属于哪一个mem_cgroup的了,针对那个mem_cgroup的新的oom过程就能够开始。
others
config change
关于配置更改,mem_cgroup还有不少麻烦的事情须要处理,主要是涉及到mem_cgroup参数的调整以及进程的迁移:
一、hierarchy参数的调整
a、只有当父组的hierarchy为假时才能设置;
这就规定是继承关系的断代是不容许的。貌似实在很差定义断代了的继承关系该如何来处理。
b、只有当mem_cgroup没有子组只才能设置;
这个规定省去了不少麻烦。不然能够想象,hierarchy调整以后,整棵mem_cgroup子树上的计数都须要同步地进行调整。
二、进程在mem_cgroup之间移动
按理说,移动进程也是很麻烦的事情。对于进程所占有的page将在原来的mem_cgroup上uncharge,并在新的mem_cgroup上charge。不过这个逻辑默认是禁止的,也就是说,进程在mem_cgroup间移动,不会触发charge/uncharge。
也能够设置mem_cgroup的move_charge_at_immigrate参数来支持进程移动时的charge/uncharge行为。move_charge_at_immigrate是一个bitmap,bit-0表明anon和swap的行为、bit-1表明file的行为。
那么如何进行计数迁移呢?关键的问题是,移动的这个进程应该被认为带走了哪些page?注意,page的计数是跟mem_cgroup关联的,而跟进程没有直接关系。因此要判断一个进程应该带走哪些page,只能反过来,从进程的页表出发,看看它引用了哪些page(那么固然,若是没有mmu,也就不能支持)。另外,固然,须要计数迁移的page,其对应的page_cgroup->mem_cgroup必定是指向源mem_cgroup的。而迁移所须要作的事情就是charge目标mem_cgroup、uncharge源mem_cgroup、再修改page_cgroup->mem_cgroup指向目标mem_cgroup。具体哪些page应该发生计数迁移,大体的规则以下:
a、页表有引用:若是是映射数目为1的anon page,或是page cache,则计数迁移;
b、页表指向swap:若是是swap的引用数目为1,则计数迁移;
c、页表项为空:查看vma映射的文件位置上是否有page cache,有则计数迁移;
总的来讲,判断条件比较暴力,page cache只要被该进程引用,则迁移;而anon和swap则在被且仅被该进程映射的状况下,才迁移。
三、mem_cgroup的删除
mem_cgroup可以被删除,有两个前提:
a、mem_cgroup下没有进程;
b、mem_cgroup没有子组;
删除时,属于该mem_cgroup的计数将被增长到其父组上、lru里面的page也会移动到父组的lru。(无论有没有设置hierarchy。)
既然mem_cgroup已经没有了进程,为何还有计数呢?由于计数是基于mem_cgroup的,进程的退出并不意味着必定会uncharge全部的计数(它有不少当冤大头的机会)。
若是父组设置了hierarchy,则实际上并不会增长其计数(由于子组的计数已经在它头上charge过了)。
不然,父组charge,可能致使hard limit超限。这时可能触发同步的reclaim,可是并不会触发oom。而若是父组charge失败,则对子组的rmdir操做将返回-EBUSY。
若是但愿干净地删掉一个子组,而避免将计数charge到父组上,则能够经过echo 0 > memory.force_empty将该组的计数清空。force_empty的前提也是mem_cgroup下没有进程也没有子组。force_empty将试图回收mem_cgroup下全部的page,若是有些page未能回收,则仍是会将其charge到父组上。
stock cache
并不是对于每一个page的charge/uncharge都直接跟mem_cgroup的计数打交道,这样的话多个CPU可能带来很多的竞争。 解决办法是加一个per-CPU的cache,即每一个CPU在须要charge的时候,先charge一个较大的数目(如32),则以后的charge操做就可能直接在本地完成。 这个cache就是memcg_stock_pcp,其主要成员有:一个指向mem_cgroup的指针和一个nr_pages计数。 也就是说,它只cache一个mem_cgroup的计数,若是下一次须要charge的mem_cgroup跟cache中的不一样,则会将cache替换掉,而cache的计数也会随之uncharge。只cache一个mem_cgroup也已经足够了,由于同一个进程几乎老是跟一个mm打交道的,从而也只会影响到一个mem_cgroup的计数。 由于有这个cache的存在,有时候尝试charge超过hard limit限制可能并非真正的超限,因此在进行同步的reclaim以前,会先将cache清空。