博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
linux内核mem_cgroup浅析
阅读量:6800 次
发布时间:2019-06-26

本文共 12129 字,大约阅读时间需要 40 分钟。

memory cgroup


mem_cgroup是cgroup体系中提供的用于memory隔离的功能。

admin可以创建若干个mem_cgroup,形成一个树型结构。可以将进程加入到这些mem_cgroup中。(类似这样的管理功能都是由cgroup框架自带的。)


为了实现memory隔离,每个mem_cgroup主要有两个维度的限制:

1、
res
 - 物理内存

2、
memsw
 - memory + swap,物理内存 + swap

其中,memsw肯定是大于等于memory的。

另外注意,memory控制是针对于组的,而不是单个进程的。(当然,你也可以一个进程一个组。)


每个维度又有三个指标:

1、
usage
 - 组内进程已经使用的内存

2、
soft_limit
 - 非强制内存上限。usage超过这个上限后,组内进程使用的内存可能会被加快步伐进行回收

3、
hard_limit
 - 强制内存上限。usage不能超过这个上限。如果试图超过,则会触发同步的内存回收过程,或者OOM(挑选并杀掉一个进程,以释放空间。见《
》)

其中,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
。主要有这么几种情况:

1、read/write系统调用;

2、mmap文件之后,在对应区域进行内存读写;

3、伴随1和2两种情况产生的预读;

反之,
当page被释放
(一般就在它离开page cache之时),
对应的mem_cgroup得以uncharge
。主要有这么几种情况:

1、page回收算法将page cache中的page回收;

2、使用direct-io导致对应区域的page cache被释放;

3、类似/proc/sys/vm/drop_caches、fadvice(DONTDEED)这样的方式主动清理page cache;

4、类似文件truncate这样的事件造成对应区域的page cache被释放;

5、等等;

注意,使用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
。主要有这么几种情况:

1、写一个未建立映射的属于匿名vma的虚拟内存时,page被分配,并建立映射;

2、写一个待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
。主要有这么几种情况:

1、进程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回收的过程详见《
》。


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:

1、alloc => add_lru => del_lru => free

2、alloc => add_lru => add_to_swap_cache => del_from_swap_cache => del_lru => free

page cache:

1、alloc => add_lru => add_to_page_cache => del_from_page_cache => del_lru => free

2、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有三条路径:

1、普通的reclaim流程(包括kswapd和内存紧缺时的主动回收)。

这个是视整个系统的内存使用情况而定的,有无mem_cgroup都一样。

注意,在普通的reclaim流程中同样可能回收掉属于某个mem_cgroup的page,从而导致对该mem_cgroup的uncharge。

2、普通的reclaim流程中额外会尝试对soft limit超额最多的几个mem_cgroup进行回收。

这里就是soft limit主要产生作用的地方。

3、在试图对mem_cgroup做charge的时候,如果hard_limit超额,会同步地对其进行页面回收,以便charge成功;


这三个回收过程走的基本上是同一个逻辑:扫描lru,将active链表中的一些老page移动到inactive链表、对inactive链表中的一些老page进行回收。

略有不同之处在于:

1、普通的回收流程关心的是全局的lru,而后两种则是关心特定mem_cgroup的lru;

2、按照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进行回收;

3、hard limit超限时可能存在no-swap逻辑,如果是memsw超限的话,swap-out是无意义的;

4、hard limit超限时一次回收过程可能无法释放足够的page,则继续进行回收(会轮循到不同的子mem_cgroup和NUMA node),最终回收无果还会进入oom逻辑;而soft limit超限时则没有回收数目的要求;

5、等等;


oom


就像内核在系统内存不足且回收无果的情况下会进入oom流程一样,在尝试charge超过hard limit情况下,如果同步的回收过程无法回收足够的page,也会进入oom流程。

当然,针对特定mem_cgroup的oom,只会挑选属于该mem_cgroup的进程来kill。


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

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

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


其中还是有不少细节的:

1、
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超限,分值会按超限额增加一定比例的分值;

    

2、
oom时机

oom是在同步的reclaim流程无法回收足够的page时触发的。但是reclaim流程无法继续回收,其实并不代表绝对的不可回收。

比如active的page、装有可执行代码的page、等都是尽量不要去回收的。

因为在一个上下文进行reclaim的时候,其他的上下文还各自在干其他的事情,无时不涉及内存的使用。

那么,如果你把能回收的page都回收了,随着其他上下文的运行又会把很多page恢复回来。其结果很可能最终还是没能回收到空间,却徒增了换入换出的开销。

所以,虽说oom是在内存回收无果时触发的,却也并非完全不能再回收。至于其中的“度”,也只能靠调试和经验来把握了。


3、
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参数的调整以及进程的迁移:


1、
hierarchy参数的调整

    a、只有当父组的hierarchy为假时才能设置;

    这就规定是继承关系的断代是不允许的。貌似实在不好定义断代了的继承关系该如何来处理。

    b、只有当mem_cgroup没有子组只才能设置;

    这个规定省去了很多麻烦。否则可以想象,hierarchy调整之后,整棵mem_cgroup子树上的计数都需要同步地进行调整。


2、
进程在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则在被且仅被该进程映射的情况下,才迁移。


3、
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清空。

转载于:https://www.cnblogs.com/hehehaha/archive/2013/05/12/6332784.html

你可能感兴趣的文章
LINQ简记(2):重要概念
查看>>
jQuery 1.6 源码学习(二)——core.js[2]之extend&ready方法
查看>>
[WPF疑难] 继承自定义窗口
查看>>
WebRTC网关服务器单端口方案实现
查看>>
018 easygui的使用
查看>>
iphone 开发h5 踩过的坑
查看>>
微信支付demo集
查看>>
python读取json的工具jsonreader | the5fire的技术博客
查看>>
Sharepoint学习笔记—习题系列--70-576习题解析 -(Q99-Q101)
查看>>
转oracle 学习 - 表空间
查看>>
百度地图显示多个标注点
查看>>
robots.txt的介绍和写作
查看>>
11个实用jQuery日历插件
查看>>
MySQL slave状态之Seconds_Behind_Master
查看>>
国内外开源与 SaaS ,团队协作平台、项目管理工具整理
查看>>
oracle字符集查看修改
查看>>
[Leetcode] Container With Most Water
查看>>
查看版本信息的命令
查看>>
Linux搭建SVN服务器
查看>>
UML 之 数据流图(DFD)
查看>>