linux内核mem_cgroup浅析_mem cgroup 详细-程序员宅基地

技术标签: Linux内核_学习笔记  

1,https://www.cnblogs.com/lisperl/archive/2012/04/28/2474872.html

memory 子系统可以设定 cgroup 中任务使用的内存限制,并自动生成由那些任务使用的内存资源报告。memory子系统是通过linuxresource counter机制实现的。下面我们就先来看一下resource counter机制。

resource counter是内核为子系统提供的一种资源管理机制。这个机制的实现包括了用于记录资源的数据结构和相关函数。Resource counter定义了一个res_counter的结构体来管理特定资源,定义如下:

struct res_counter {

unsigned long long usage;

unsigned long long max_usage;

unsigned long long limit;

unsigned long long soft_limit;

unsigned long long failcnt; /*

spinlock_t lock;

struct res_counter *parent;

};

 

Usage用于记录当前已使用的资源,max_usage用于记录使用过的最大资源量,limit用于设置资源的使用上限,进程组不能使用超过这个限制的资源,soft_limit用于设定一个软上限,进程组使用的资源可以超过这个限制,failcnt用于记录资源分配失败的次数,管理可以根据这个记录,调整上限值。Parent指向父节点,这个变量用于处理层次性的资源管理。

除了这个关键的数据结构,resource counter还定义了一系列相关的函数。下面我们来看几个关键的函数。

void res_counter_init(struct res_counter *counter, struct res_counter *parent)

{

spin_lock_init(&counter->lock);

counter->limit = RESOURCE_MAX;

counter->soft_limit = RESOURCE_MAX;

counter->parent = parent;

}

这个函数用于初始化一个res_counter。

第二个关键的函数是int res_counter_charge(struct res_counter *counter, unsigned long val, struct res_counter **limit_fail_at)。当资源将要被分配的时候,资源就要被记录到相应的res_counter里。这个函数作用就是记录进程组使用的资源。在这个函数中有:

for (c = counter; c != NULL; c = c->parent) {

spin_lock(&c->lock);

ret = res_counter_charge_locked(c, val);

spin_unlock(&c->lock);

if (ret < 0) {

*limit_fail_at = c;

goto undo;

}

}

在这个循环里,从当前res_counter开始,从下往上逐层增加资源的使用量。我们来看一下res_counter_charge_locked这个函数,这个函数顾名思义就是在加锁的情况下增加使用量。实现如下:

{

if (counter->usage + val > counter->limit) {

counter->failcnt++;

return -ENOMEM;

}

 

counter->usage += val;

if (counter->usage > counter->max_usage)

counter->max_usage = counter->usage;

return 0;

}

首先判断是否已经超过使用上限,如果是的话就增加失败次数,返回相关代码;否则就增加使用量的值,如果这个值已经超过历史最大值,则更新最大值。

第三个关键的函数是void res_counter_uncharge(struct res_counter *counter, unsigned long val)。当资源被归还到系统的时候,要在相应的res_counter减轻相应的使用量。这个函数作用就在于在于此。实现如下:

for (c = counter; c != NULL; c = c->parent) {

spin_lock(&c->lock);

res_counter_uncharge_locked(c, val);

spin_unlock(&c->lock);

}

从当前counter开始,从下往上逐层减少使用量,其中调用了res_counter_uncharge_locked,这个函数的作用就是在加锁的情况下减少相应的counter的使用量。

有这些数据结构和函数,只需要在内核分配资源的时候,植入相应的charge函数,释放资源时,植入相应的uncharge函数,就能实现对资源的控制了。

介绍完resource counter,我们再来看memory子系统是利用resource counter实现对内存资源的管理的。

memory子系统定义了一个叫mem_cgroup的结构体来管理cgroup相关的内存使用信息,定义如下:

struct mem_cgroup {

struct cgroup_subsys_state css;

struct res_counter res;

struct res_counter memsw;

struct mem_cgroup_lru_info info;

spinlock_t reclaim_param_lock;

int prev_priority;

int last_scanned_child;

bool use_hierarchy;

atomic_t oom_lock;

atomic_t refcnt;

unsigned int swappiness;

int oom_kill_disable;

bool memsw_is_minimum;

struct mutex thresholds_lock;

struct mem_cgroup_thresholds thresholds;

struct mem_cgroup_thresholds memsw_thresholds;

struct list_head oom_notify;

unsigned long  move_charge_at_immigrate;

struct mem_cgroup_stat_cpu *stat;

};

跟其他子系统一样,mem_cgroup也包含了一个cgroup_subsys_state成员,便于taskcgroup获取mem_cgroup

mem_cgroup中包含了两个res_counter成员,分别用于管理memory资源和memory+swap资源,如果memsw_is_minimumtrue,则res.limit=memsw.limit,即当进程组使用的内存超过memory的限制时,不能通过swap来缓解。

use_hierarchy则用来标记资源控制和记录时是否是层次性的。

oom_kill_disable则表示是否使用oom-killer

oom_notify指向一个oom notifier event fd链表。

另外memory子系统还定义了一个叫page_cgroup的结构体:

struct page_cgroup {

unsigned long flags;

struct mem_cgroup *mem_cgroup;

struct page *page;

struct list_head lru; /* per cgroup LRU list */

};

此结构体可以看作是mem_map的一个扩展,每个page_cgroup都和所有的page关联,而其中的mem_cgroup成员,则将page与特定的mem_cgroup关联起来。

我们知道在linux系统中,page结构体是用来管理物理页框的,一个物理页框对应一个page结构体,而每个进程中的task_struct中都有一个mm_struct来管理进程的内存信息。每个mm_struct知道它属于的进程,进而知道所属的mem_cgroup,而每个page都知道它属于的page_cgroup,进而也知道所属的mem_cgroup,而内存使用量的计算是按cgroup为单位的,这样以来,内存资源的管理就可以实现了。

memory子系统既然是通过resource counter实现的,那肯定会在内存分配给进程时进行charge操作的。下面我们就来看一下这些charge操作:

1.page fault发生时,有两种情况内核需要给进程分配新的页框。一种是进程请求调页(demand paging),另一种是copy on write。内核在handle_pte_fault中进行处理。其中,do_linear_fault处理pte不存在且页面线性映射了文件的情况,do_anonymous_page处理pte不存在且页面没有映射文件的情况,do_nonlinear_fault处理pte存在且页面非线性映射文件的情况,do_wp_page则处理copy on write的情况。其中do_linear_faultdo_nonlinear_fault都会调用__do_fault来处理。Memory子系统则__do_faultdo_anonymous_pagedo_wp_page植入mem_cgroup_newpage_charge来进行charge操作。

2.内核在handle_pte_fault中进行处理时,还有一种情况是pte存在且页又没有映射文件。这种情况说明页面之前在内存中,但是后面被换出到swap空间了。内核用do_swap_page函数处理这种情况,memory子系统在do_swap_page加入了mem_cgroup_try_charge_swapin函数进行chargemem_cgroup_try_charge_swapin是处理页面换入时的charge的,当执行swapoff系统调用(关掉swap空间),内核也会执行页面换入操作,因此mem_cgroup_try_charge_swapin也被植入到了相应的函数中。

3.当内核将page加入到page cache中时,也需要进行charge操作,mem_cgroup_cache_charge函数正是处理这种情况,它被植入到系统处理page cacheadd_to_page_cache_locked函数中。

4.最后mem_cgroup_prepare_migration是用于处理内存迁移中的charge操作。

除了charge操作,memory子系统还需要处理相应的uncharge操作。下面我们来看一下uncharge操作:

1.mem_cgroup_uncharge_page用于当匿名页完全unmaped的时候。但是如果该pageswap cache的话,uncharge操作延迟到mem_cgroup_uncharge_swapcache被调用时执行。

2.mem_cgroup_uncharge_cache_page用于page cacheradix-tree删除的时候。但是如果该pageswap cache的话,uncharge操作延迟到mem_cgroup_uncharge_swapcache被调用时执行。

3.mem_cgroup_uncharge_swapcache用于swap cache从radix-tree删除的时候。Charge的资源会被算到swap_cgroup,如果mem+swap controller被禁用了,就不需要这样做了。

4.mem_cgroup_uncharge_swap用于swap_entry的引用数减到0的时候。这个函数主要在mem+swap controller可用的情况下使用的。

5.mem_cgroup_end_migration用于内存迁移结束时相关的uncharge操作。

Charge函数最终都是通过调用__mem_cgroup_try_charge来实现的。在__mem_cgroup_try_charge函数中,调用res_counter_charge(&mem->res, csize, &fail_res)对memory进行charge,调用res_counter_charge(&mem->memsw, csize, &fail_res)memory+swap进行charge

Uncharge函数最终都是通过调用__do_uncharge来实现的。在__do_uncharge中,分别调用res_counter_uncharge(&mem->res,PAGE_SIZE)和res_counter_uncharge(&mem->memsw, PAGE_SIZE)uncharge memorymemory+swap

跟其他子系统一样,memory子系统也实现了一个cgroup_subsys。

struct cgroup_subsys mem_cgroup_subsys = {

.name = "memory",

.subsys_id = mem_cgroup_subsys_id,

.create = mem_cgroup_create,

.pre_destroy = mem_cgroup_pre_destroy,

.destroy = mem_cgroup_destroy,

.populate = mem_cgroup_populate,

.can_attach = mem_cgroup_can_attach,

.cancel_attach = mem_cgroup_cancel_attach,

.attach = mem_cgroup_move_task,

.early_init = 0,

.use_id = 1,

};

Memory子系统中重要的文件有

memsw.limit_in_bytes

{

.name = "memsw.limit_in_bytes",

.private = MEMFILE_PRIVATE(_MEMSWAP, RES_LIMIT),

.write_string = mem_cgroup_write,

.read_u64 = mem_cgroup_read,

},

这个文件用于设定memory+swap上限值。

Limit_in_bytes

{

.name = "limit_in_bytes",

.private = MEMFILE_PRIVATE(_MEM, RES_LIMIT),

.write_string = mem_cgroup_write,

.read_u64 = mem_cgroup_read,

},

这个文件用于设定memory上限值。


2,http://blog.csdn.net/ctthuangcheng/article/details/8916075

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(挑选并杀掉一个进程,以释放空间。见《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。主要有这么几种情况:
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回收的过程详见《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:
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清空。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/yiyeguzhou100/article/details/78963577

智能推荐

攻防世界_难度8_happy_puzzle_攻防世界困难模式攻略图文-程序员宅基地

文章浏览阅读645次。这个肯定是末尾的IDAT了,因为IDAT必须要满了才会开始一下个IDAT,这个明显就是末尾的IDAT了。,对应下面的create_head()代码。,对应下面的create_tail()代码。不要考虑爆破,我已经试了一下,太多情况了。题目来源:UNCTF。_攻防世界困难模式攻略图文

达梦数据库的导出(备份)、导入_达梦数据库导入导出-程序员宅基地

文章浏览阅读2.9k次,点赞3次,收藏10次。偶尔会用到,记录、分享。1. 数据库导出1.1 切换到dmdba用户su - dmdba1.2 进入达梦数据库安装路径的bin目录,执行导库操作  导出语句:./dexp cwy_init/[email protected]:5236 file=cwy_init.dmp log=cwy_init_exp.log 注释:   cwy_init/init_123..._达梦数据库导入导出

js引入kindeditor富文本编辑器的使用_kindeditor.js-程序员宅基地

文章浏览阅读1.9k次。1. 在官网上下载KindEditor文件,可以删掉不需要要到的jsp,asp,asp.net和php文件夹。接着把文件夹放到项目文件目录下。2. 修改html文件,在页面引入js文件:<script type="text/javascript" src="./kindeditor/kindeditor-all.js"></script><script type="text/javascript" src="./kindeditor/lang/zh-CN.js"_kindeditor.js

STM32学习过程记录11——基于STM32G431CBU6硬件SPI+DMA的高效WS2812B控制方法-程序员宅基地

文章浏览阅读2.3k次,点赞6次,收藏14次。SPI的详情简介不必赘述。假设我们通过SPI发送0xAA,我们的数据线就会变为10101010,通过修改不同的内容,即可修改SPI中0和1的持续时间。比如0xF0即为前半周期为高电平,后半周期为低电平的状态。在SPI的通信模式中,CPHA配置会影响该实验,下图展示了不同采样位置的SPI时序图[1]。CPOL = 0,CPHA = 1:CLK空闲状态 = 低电平,数据在下降沿采样,并在上升沿移出CPOL = 0,CPHA = 0:CLK空闲状态 = 低电平,数据在上升沿采样,并在下降沿移出。_stm32g431cbu6

计算机网络-数据链路层_接收方收到链路层数据后,使用crc检验后,余数为0,说明链路层的传输时可靠传输-程序员宅基地

文章浏览阅读1.2k次,点赞2次,收藏8次。数据链路层习题自测问题1.数据链路(即逻辑链路)与链路(即物理链路)有何区别?“电路接通了”与”数据链路接通了”的区别何在?2.数据链路层中的链路控制包括哪些功能?试讨论数据链路层做成可靠的链路层有哪些优点和缺点。3.网络适配器的作用是什么?网络适配器工作在哪一层?4.数据链路层的三个基本问题(帧定界、透明传输和差错检测)为什么都必须加以解决?5.如果在数据链路层不进行帧定界,会发生什么问题?6.PPP协议的主要特点是什么?为什么PPP不使用帧的编号?PPP适用于什么情况?为什么PPP协议不_接收方收到链路层数据后,使用crc检验后,余数为0,说明链路层的传输时可靠传输

软件测试工程师移民加拿大_无证移民,未受过软件工程师的教育(第1部分)-程序员宅基地

文章浏览阅读587次。软件测试工程师移民加拿大 无证移民,未受过软件工程师的教育(第1部分) (Undocumented Immigrant With No Education to Software Engineer(Part 1))Before I start, I want you to please bear with me on the way I write, I have very little gen...

随便推点

Thinkpad X250 secure boot failed 启动失败问题解决_安装完系统提示secureboot failure-程序员宅基地

文章浏览阅读304次。Thinkpad X250笔记本电脑,装的是FreeBSD,进入BIOS修改虚拟化配置(其后可能是误设置了安全开机),保存退出后系统无法启动,显示:secure boot failed ,把自己惊出一身冷汗,因为这台笔记本刚好还没开始做备份.....根据错误提示,到bios里面去找相关配置,在Security里面找到了Secure Boot选项,发现果然被设置为Enabled,将其修改为Disabled ,再开机,终于正常启动了。_安装完系统提示secureboot failure

C++如何做字符串分割(5种方法)_c++ 字符串分割-程序员宅基地

文章浏览阅读10w+次,点赞93次,收藏352次。1、用strtok函数进行字符串分割原型: char *strtok(char *str, const char *delim);功能:分解字符串为一组字符串。参数说明:str为要分解的字符串,delim为分隔符字符串。返回值:从str开头开始的一个个被分割的串。当没有被分割的串时则返回NULL。其它:strtok函数线程不安全,可以使用strtok_r替代。示例://借助strtok实现split#include <string.h>#include <stdio.h&_c++ 字符串分割

2013第四届蓝桥杯 C/C++本科A组 真题答案解析_2013年第四届c a组蓝桥杯省赛真题解答-程序员宅基地

文章浏览阅读2.3k次。1 .高斯日记 大数学家高斯有个好习惯:无论如何都要记日记。他的日记有个与众不同的地方,他从不注明年月日,而是用一个整数代替,比如:4210后来人们知道,那个整数就是日期,它表示那一天是高斯出生后的第几天。这或许也是个好习惯,它时时刻刻提醒着主人:日子又过去一天,还有多少时光可以用于浪费呢?高斯出生于:1777年4月30日。在高斯发现的一个重要定理的日记_2013年第四届c a组蓝桥杯省赛真题解答

基于供需算法优化的核极限学习机(KELM)分类算法-程序员宅基地

文章浏览阅读851次,点赞17次,收藏22次。摘要:本文利用供需算法对核极限学习机(KELM)进行优化,并用于分类。

metasploitable2渗透测试_metasploitable2怎么进入-程序员宅基地

文章浏览阅读1.1k次。一、系统弱密码登录1、在kali上执行命令行telnet 192.168.26.1292、Login和password都输入msfadmin3、登录成功,进入系统4、测试如下:二、MySQL弱密码登录:1、在kali上执行mysql –h 192.168.26.129 –u root2、登录成功,进入MySQL系统3、测试效果:三、PostgreSQL弱密码登录1、在Kali上执行psql -h 192.168.26.129 –U post..._metasploitable2怎么进入

Python学习之路:从入门到精通的指南_python人工智能开发从入门到精通pdf-程序员宅基地

文章浏览阅读257次。本文将为初学者提供Python学习的详细指南,从Python的历史、基础语法和数据类型到面向对象编程、模块和库的使用。通过本文,您将能够掌握Python编程的核心概念,为今后的编程学习和实践打下坚实基础。_python人工智能开发从入门到精通pdf

推荐文章

热门文章

相关标签