内存监控管理实现
cgroup 内存管理子系统定义了一个叫mem_cgroup的结构体来管理cgroup相关的内存使用信息,定义如下:
struct mem_cgroup {
structcgroup_subsys_state css;
/*
* the counter to account for memory usage
*/
struct res_counter res; 管理memory资源
union {
/*
* the counter to account for mem+swap usage.
*/
struct res_counter memsw; memory+swap资源
structrcu_head rcu_freeing;
structwork_struct work_freeing;
}
/*
* the counter to account for kernel memoryusage.
*/
structres_counter kmem;
/*
* Per cgroup active and inactive list, similarto the
* per zone LRU lists.
*/
structmem_cgroup_lru_info info;
/*
* Should the accounting and control behierarchical, per subtree?
*/
bool use_hierarchy; 用来标记资源控制和记录时是否是层次性的
unsignedlong kmem_account_flags; /* See KMEM_ACCOUNTED_*, below */
bool oom_lock;
atomic_t under_oom;
atomic_t refcnt;
int swappiness;
/*OOM-Killer disable */
int oom_kill_disable;/*高版本可以禁止oom*/
/* set when res.limit == memsw.limit */
bool memsw_is_minimum; //res.limit=memsw.limit即当进程组使用的内存超过memory的限制时,不能通过swap来缓解
… …
};
跟其他子系统一样,mem_cgroup也包含了一个cgroup_subsys_state成员,便于task或cgroup获取mem_cgroup。
mem_cgroup中包含了两个res_counter成员,分别用于管理memory资源和memory+swap资源,如果memsw_is_minimum为true,则res.limit=memsw.limit,即当进程组使用的内存超过memory的限制时,不能通过swap来缓解。
/*If memsw_is_minimum==1, swap-out is of-no-use. */
if(root_mem->memsw_is_minimum)
noswap= true;
use_hierarchy则用来标记资源控制和记录时是否是层次性的。
static intmem_cgroup_walk_tree(struct mem_cgroup *root, void *data,
int (*func)(struct mem_cgroup *, void *))
{
intfound, ret, nextid;
structcgroup_subsys_state *css;
structmem_cgroup *mem;
if (!root->use_hierarchy)
return (*func)(root, data);
nextid= 1;
do {
ret= 0;
mem= NULL;
rcu_read_lock();
css= css_get_next(&mem_cgroup_subsys, nextid, &root->css,
&found);
if(css && css_tryget(css))
mem= container_of(css, struct mem_cgroup, css);
rcu_read_unlock();
if(mem) {
ret =(*func)(mem, data);
css_put(&mem->css);
}
nextid= found + 1;
}while (!ret && css);
returnret;
}
另外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 */
};
此结构体将page与特定的mem_cgroup关联起来,每个page都知道它属于的page_cgroup,进而也知道所属的mem_cgroup。
linux系统中,一个物理页框对应一个page结构体,而每个进程中的task_struct中都有一个mm_struct来管理进程的内存信息。每个mm_struct知道它属于的进程,进而知道所属的mem_cgroup,而每个page都知道它属于的page_cgroup,进而也知道所属的mem_cgroup,而内存使用量的计算是按cgroup为单位的,这样以来,内存资源的管理就可以实现了。
memory子系统既然是通过resource counter实现的,那肯定会在内存分配给进程时进行charge操作的。首先来看一下这些charge操作:
1.page fault发生时,有两种情况内核需要给进程分配新的页框。一种是进程请求调页,另一种是copy on write。其中,do_linear_fault处理pte不存在且页面线性映射了文件的情况,do_anonymous_page处理pte不存在且页面没有映射文件的情况;do_nonlinear_fault处理pte存在且页面非线性映射文件的情况;do_wp_page则处理copy on write的情况。其中do_linear_fault和do_nonlinear_fault都会调用__do_fault来处理。
Memory子系统在__do_fault、do_anonymous_page、do_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函数进行charge。mem_cgroup_try_charge_swapin是处理页面换入时的charge的,当执行swapoff系统调用(关掉swap空间),内核也会执行页面换入操作,因此mem_cgroup_try_charge_swapin也被植入到了相应的函数中。
3.当内核将page加入到page cache中时,也需要进行charge操作,mem_cgroup_cache_charge函数正是处理这种情况,它被植入到系统处理page cache的add_to_page_cache_locked函数中。
4.最后mem_cgroup_prepare_migration是用于处理内存迁移中的charge操作。
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。
需要指出的是,Charge统计只针对用户态申请内存的
除了charge操作,memory子系统还需要处理相应的uncharge操作:
1.mem_cgroup_uncharge_page用于当匿名页完全unmaped的时候。但是如果该page是swap cache的话,uncharge操作延迟到mem_cgroup_uncharge_swapcache被调用时执行。
2.mem_cgroup_uncharge_cache_page用于page cache从radix-tree删除的时候。但是如果该page是swap 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操作。
Uncharge函数最终都是通过调用__do_uncharge来实现的。在__do_uncharge中,分别调用res_counter_uncharge(&mem->res,PAGE_SIZE)和res_counter_uncharge(&mem->memsw, PAGE_SIZE)来uncharge memory和memory+swap。
下面分析resource counter机制实现。
resource counter是内核为子系统提供的一种资源管理机制。这个机制的实现包括了用于记录资源的数据结构和相关函数。Resource counter定义了一个res_counter的结构体来管理特定资源,定义如下:
struct res_counter {
unsigned long long usage;Usage用于记录当前已使用的资源
unsigned long long max_usage;max_usage用于记录使用过的最大资源量
unsigned long long limit;limit用于设置资源的使用上限
unsigned long long soft_limit;soft_limit用于设定一个软上限,进程组使用的资源可以超过这个限制,超过之后内存会加快回收
unsigned long long failcnt;/* failcnt用于记录资源分配失败的次数,管理可以根据这个记录
spinlock_t lock;
struct res_counter *parent;Parent指向父节点,这个变量用于处理层次性的资源管理。
};
其中balance_pgdat->mem_cgroup_soft_limit_reclaim加快回收。
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。当资源将要被分配的时候,资源就要被记录到相应的res_counter里。这个函数作用就是记录进程组使用的资源。在这个函数中有:
static int__res_counter_charge(struct res_counter *counter, unsigned long val,
structres_counter **limit_fail_at, bool force)
{… …
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这个函数是在加锁的情况下增加使用量。实现如下:
int res_counter_charge_locked(structres_counter *counter, unsigned long val)
{
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函数,就能实现对资源的控制了。
除了memory子系统外,cpuset子系统对组使用的内存节点进行管理,具体来说:
Linux中内核分配物理页框的函数有6个:
alloc_pages,alloc_page,__get_free_pages,__get_free_page,get_zeroed_page,__get_dma_pages,这些函数最终都通过alloc_pages实现,而alloc_pages又通过__alloc_pages_nodemask实现,在__alloc_pages_nodemask中,调用get_page_from_freelist从zone list中分配一个page,在get_page_from_freelist中调用cpuset_zone_allowed_softwall判断当前节点是否属于mems_allowed。通过附加这样一个判断,保证进程从mems_allowed中的节点分配内存。
int__cpuset_node_allowed_softwall(int node, gfp_t gfp_mask)
{
/*
* Scan zonelist, looking for a zone withenough free.
* See also cpuset_zone_allowed() comment inkernel/cpuset.c.
*/
for_each_zone_zonelist_nodemask(zone,z, zonelist,
high_zoneidx,nodemask) {
if(IS_ENABLED(CONFIG_NUMA) && zlc_active &&
!zlc_zone_worth_trying(zonelist,z, allowednodes))
continue;
if((alloc_flags & ALLOC_CPUSET) &&
!cpuset_zone_allowed_softwall(zone,gfp_mask))
continue;
Linux在cpuset出现之前,也提供了mbind, set_mempolicy来限定进程可用的内存节点。Cpuset子系统对其做了扩展,扩展的方法跟扩展sched_setaffinity类似,通过设置在进程中保存cupset设置的允许分配内存节点mems_allowed达到内存控制的效果。
然后看一下cpuset子系统最重要的控制文件:
{
.name = "mems",
.read = cpuset_common_file_read,
.write_string = cpuset_write_resmask,
.max_write_len = (100U + 6 * MAX_NUMNODES),
.private = FILE_MEMLIST,
},
通过cpus文件,可以指定进程可以使用的cpu节点,通过mems文件,可以指定进程可以使用的memory节点。
这两个文件的读写都是通过cpuset_common_file_read和cpuset_write_resmask实现的,通过private属性区分。在cpuset_common_file_read中读出可用的cpu或memory节点;在cpuset_write_resmask中则根据文件类型分别调用update_cpumask和update_nodemask更新cpu或memory节点信息。
随着内核内存的扩展,内存控制器可以对系统使用的内核空间内存量进行限制。内核内存与用户空间内存不同,不能交换出去,所以有可能消耗掉大量珍贵的资源。linux3.8内核添加了内核空间内存统计功能,由内核内存扩展宏CONFIG_MEMCG_KMEM控制:
其中新增标识__GFP_KMEMCG的请页标识来表示所申请的是内核内存:
#define __GFP_KMEMCG ((__force gfp_t)___GFP_KMEMCG) /*Allocation comes from a memcg-accounted resource */
如何显示cgroup的内核空间内存:
mem_cgroup中新增kmem的res_counter结构用于对内核空间内存进行统计以及限制:
struct mem_cgroup {
union{
… …
/*
* the counter to account for kernel memoryusage.
*/
struct res_counter kmem;
res_counter的用法上文已经详细介绍,这里介绍几个kmem内存统计的函数:
内核内存对使用内存计数函数:
memcg_charge_kmem
{
structres_counter *fail_res;
structmem_cgroup *_memcg;
intret = 0;
boolmay_oom;
ret =res_counter_charge(&memcg->kmem, size, &fail_res);
if(ret)
returnret;
内核内存去计数函数:
static voidmemcg_uncharge_kmem(struct mem_cgroup *memcg, u64 size)
{
res_counter_uncharge(&memcg->res,size);
if (do_swap_account)
res_counter_uncharge(&memcg->memsw,size);
/*Not down to 0 */
if(res_counter_uncharge(&memcg->kmem, size))
return;
if(memcg_kmem_test_and_clear_dead(memcg))//mem_cgroup不存在时清除
mem_cgroup_put(memcg);
}
内核内存对可申请上限值的设置函数:
static intmemcg_update_kmem_limit(struct cgroup *cont, u64 val)
if(!memcg->kmem_account_flags && val != RESOURCE_MAX) {
if(cgroup_task_count(cont) || (memcg->use_hierarchy &&
!list_empty(&cont->children))){
ret= -EBUSY;
gotoout;
}
ret =res_counter_set_limit(&memcg->kmem, val);
内核空间内存是如何统计进cgroup空间的呢?
上文提到申请页面接口__alloc_pages_nodemask,会调用 memcg_kmem_newpage_charge->__memcg_kmem_newpage_charge->memcg_charge_kmem实现增加计数的功能:
static inline bool
memcg_kmem_newpage_charge(gfp_t gfp,struct mem_cgroup **memcg, int order)
{
if(!memcg_kmem_enabled())
returntrue;
if (!(gfp & __GFP_KMEMCG) || (gfp& __GFP_NOFAIL))
returntrue;
if (in_interrupt() || (!current->mm)|| (current->flags &PF_KTHREAD))
return true;
/* If the test is dying, just let itgo. */
if (unlikely(fatal_signal_pending(current)))
return true;
return __memcg_kmem_newpage_charge(gfp,memcg, order);
}
值得注意的是除了没有设置__GFP_KMEMCG标识无须进行组的内核空间内存监控之外,还有几种情况无须进行组的内核内存监控:
1、__GFP_NOFAIL的请页是不受内核内存监控的(不允许失败所以不受限);
2、中断中的内存申请不受内核内存监控(内核内存监控会获取锁,在中断中会引起死锁);
3、当前进程为内核线程或空闲等非用户进程情况(已不属于组控制的范畴);
4、当前进程是正被杀死的进程
__memcg_kmem_newpage_charge获取mem_cgroup结构并对监控条件做进一步检查
bool
__memcg_kmem_newpage_charge(gfp_tgfp, struct mem_cgroup **_memcg, int order)
{
structmem_cgroup *memcg;
intret;
*_memcg= NULL;
memcg =try_get_mem_cgroup_from_mm(current->mm);
//获得当前进程所属组的mem_cgroup结构
if(unlikely(!memcg))//该进程没有归入组控制则退出
returntrue;
if(!memcg_can_account_kmem(memcg)) {//非根组且没有禁止内核内存计数
css_put(&memcg->css);
returntrue;
}
ret = memcg_charge_kmem(memcg,gfp, PAGE_SIZE << order);//内核内存监控计数
if (!ret)
*_memcg = memcg;
css_put(&memcg->css);
return (ret == 0);
}
需要指出的是,linux3.8在监控统计cgroup的内核内存时,memcg_charge_kmem调用前面介绍的__mem_cgroup_try_charge将内存页面也算入总的内存统计范围。
上面介绍了内核内存计数的过程,下面介绍内核内存计数标志__GFP_KMEMCG的设置:
1、slab伙伴系统:
cache的管理结构kmem_cache中新增一个结构体成员memcg_cache_params:
struct memcg_cache_params {
boolis_root_cache;
union{
structkmem_cache *memcg_caches[0];//组对应的kmem_cache
struct{
structmem_cgroup *memcg;//组的mem_cgroup结构
structlist_head list;
structkmem_cache *root_cache;
booldead;
atomic_tnr_pages;//包含页面数
structwork_struct destroy;
};
};
};
对于每种kmem_cache,每个cgroup通过memcg_create_kmem_cache为之建立组自己的kmem_cache:
static struct kmem_cache*memcg_create_kmem_cache(struct mem_cgroup *memcg,
struct kmem_cache *cachep)
{
structkmem_cache *new_cachep;
intidx;
BUG_ON(!memcg_can_account_kmem(memcg));
idx = memcg_cache_id(memcg);//获取组对应的一个固定kmemcg_id
mutex_lock(&memcg_cache_mutex);
new_cachep =cachep->memcg_params->memcg_caches[idx];//如果已经存在
if(new_cachep)
gotoout;
new_cachep = kmem_cache_dup(memcg,cachep);创建新的组对应的kmem_cache结构。
if(new_cachep == NULL) {
new_cachep= cachep;
gotoout;
}
mem_cgroup_get(memcg);
atomic_set(&new_cachep->memcg_params->nr_pages, 0);
cachep->memcg_params->memcg_caches[idx]= new_cachep;//填充memcg_caches
/*
* the readers won't lock, make sure everybodysees the updated value,
* so they won't put stuff in the queue againfor no reason
*/
wmb();
out:
mutex_unlock(&memcg_cache_mutex);
return new_cachep;
}
kmem_cache_dup调用kmem_cache_create_memcg函数创建新的组对应的kmem_cache结构,同时设置__GFP_KMEMCG标识。
static struct kmem_cache*kmem_cache_dup(struct mem_cgroup *memcg,
struct kmem_cache *s)
{
char*name;
structkmem_cache *new;
name= memcg_cache_name(memcg, s);
if(!name)
returnNULL;
new = kmem_cache_create_memcg(memcg,name, s->object_size, s->align,
(s->flags & ~SLAB_PANIC),s->ctor, s);
if (new)
new->allocflags |=__GFP_KMEMCG;
kfree(name);
returnnew;
}
slab申请的时,kmem_cache_alloc->slab_alloc,首先将申请的kmem_cache结构指针转换为上述组的kmem_cache指针:
static __always_inline void *
slab_alloc(struct kmem_cache *cachep,gfp_t flags, unsigned long caller)
{
… …
cachep = memcg_kmem_get_cache(cachep,flags);将申请的kmem_cache结构指针转换为组的kmem_cache指针
cache_alloc_debugcheck_before(cachep,flags);
local_lock_irqsave(slab_lock,save_flags);
objp = __do_cache_alloc(cachep, flags);cache对象申请
其中cgroup所属cache的申请条件与之前所属的memcg_kmem_newpage_charge条件一致(代码复用性不好):
static __always_inline structkmem_cache *
memcg_kmem_get_cache(struct kmem_cache*cachep, gfp_t gfp)
{
if(!memcg_kmem_enabled())
returncachep;
if(gfp & __GFP_NOFAIL)
returncachep;
if(in_interrupt() || (!current->mm) || (current->flags & PF_KTHREAD))
returncachep;
if(unlikely(fatal_signal_pending(current)))
return cachep;
return __memcg_kmem_get_cache(cachep,gfp);
}
__memcg_kmem_get_cache中如果组的kmem_cache还未创建则调用上述的memcg_create_kmem_cache创建之,否则直接返回组的kmem_cache结构:
struct kmem_cache*__memcg_kmem_get_cache(struct kmem_cache *cachep,
gfp_t gfp)
{… …
if (unlikely(cachep->memcg_params->memcg_caches[idx]== NULL)) {如果组的kmem_cache还未创建则调用上述的memcg_create_kmem_cache创建之
memcg_create_cache_enqueue(memcg,cachep);
return cachep;
}
returncachep->memcg_params->memcg_caches[idx];返回组的kmem_cache结构
随后slab_alloc中采用新的kmem_cache结构用于cache对象申请管理,由于设置了__GFP_KMEMCG标识,申请的内存是memcg_charge_kmem被监控管理的。
由于只有组创建之后进程申请的cache才统计进组的内核内存,所以当cgroup已有子组或者cgroup已经有任务加入,则不能设置内核limit。
此外当kmalloc申请大于SLUB_MAX_SIZE (2 * PAGE_SIZE)的大页面,无法从伙伴系统分配时,会设置__GFP_KMEMCG标识后调用alloc_page直接分配页面,流程:
kmalloc ->__kmalloc_node-> kmalloc_large_node:
static void*kmalloc_large_node(size_t size, gfp_t flags, int node)
{
structpage *page;
void*ptr = NULL;
flags |= __GFP_COMP | __GFP_NOTRACK |__GFP_KMEMCG;
page = alloc_pages_node(node, flags,get_order(size));
if(page)
ptr= page_address(page);
kmemleak_alloc(ptr,size, 1, flags);
returnptr;
}
kmalloc ->kmalloc_large->kmalloc_order_trace->kmalloc_order:
static __always_inline void *
kmalloc_order(size_t size, gfp_tflags, unsigned int order)
{
void*ret;
flags |= (__GFP_COMP | __GFP_KMEMCG);
ret = (void *) __get_free_pages(flags,order);
kmemleak_alloc(ret,size, 1, flags);
returnret;
}
目前代码来看内核的tcp使用内存单独统计,但没有完全实现,只有基本的读功能,这里暂不做分析。
此外3.8内核cgroup的memory部分新增了memory.oom_control文件提供oom的通知机制。内存cgroup使用cgroup的event_control通知机制实现了oom时唤醒监控任务。该机制允许注册多个oom通知消息,当oom发生时投递。
通过写字符串"<event_fd> <fd ofmemory.oom_control>"到cgroup.event_control实现event_control注册eventfd的流程:
cgroup_write_event_control调用mem_cgroup_oom_register_event完成eventfd和oom_control的关联,时间原因event_control内部原理不属于本文档关注内容,不再介绍:
staticint mem_cgroup_oom_register_event(struct cgroup *cgrp,
structcftype *cft, struct eventfd_ctx *eventfd, const char *args)
{
… …
event= kmalloc(sizeof(*event), GFP_KERNEL);
if(!event)
return-ENOMEM;
spin_lock(&memcg_oom_lock);
event->eventfd= eventfd;
list_add(&event->list,&memcg->oom_notify);
添加监控任务的流程:
应用write-> eventfd_fops. eventfd_write将当前任务加入到等待队列:
static ssize_t eventfd_write(structfile *file, const char __user *buf, size_t count,
loff_t *ppos)
…
DECLARE_WAITQUEUE(wait,current);
__add_wait_queue(&ctx->wqh,&wait);
…
唤醒任务流程:
上文说到mem_cgroup_do_charge进行内存分配时如果发现内存不足调用mem_cgroup_handle_oom,优先调用mem_cgroup_oom_notify,随后如果need_to_kill为真才触发mem_cgroup_out_of_memory流程:
static boolmem_cgroup_handle_oom(struct mem_cgroup *memcg, gfp_t mask,
int order)
if(locked)
mem_cgroup_oom_notify(memcg);
spin_unlock(&memcg_oom_lock);
if(need_to_kill) {
finish_wait(&memcg_oom_waitq,&owait.wait);
mem_cgroup_out_of_memory(memcg,mask, order);
}else {
schedule();
finish_wait(&memcg_oom_waitq,&owait.wait);
}
mem_cgroup_oom_notify会调用mem_cgroup_oom_notify_cb
static intmem_cgroup_oom_notify_cb(struct mem_cgroup *memcg)
{
structmem_cgroup_eventfd_list *ev;
list_for_each_entry(ev,&memcg->oom_notify, list)
eventfd_signal(ev->eventfd,1);
return0;
}
这里的eventfd_signal 调用wake_up_locked_poll(&ctx->wqh,POLLIN);唤醒阻塞在该eventfd上面的任务队列
__u64 eventfd_signal(structeventfd_ctx *ctx, __u64 n)
{
… …
if (waitqueue_active(&ctx->wqh))
wake_up_locked_poll(&ctx->wqh,POLLIN); //唤醒阻塞在该eventfd上面的任务队列
}
wake_up_locked_poll的实现本质是try_to_wake_up的实现,下章节会对具体的使用进行介绍:
wake_up_locked_poll->__wake_up_common->memcg_oom_wake_function->autoremove_wake_function->try_to_wake_up
内存监控管理使用
memory 子系统生成cgroup 中任务使用的内存资源报告,可设定由那些组任务使用的内存限制:
linux3.8:
# mount -t cgroup none /mnt/cgroup/-o memory
cgroup.clone_children memory.kmem.limit_in_bytes memory.kmem.tcp.usage_in_bytes memory.soft_limit_in_bytes release_agent
cgroup.event_control memory.kmem.max_usage_in_bytes memory.kmem.usage_in_bytes memory.stat tasks
cgroup.procs memory.kmem.slabinfo memory.limit_in_bytes memory.swappiness
memory.failcnt memory.kmem.tcp.failcnt memory.max_usage_in_bytes memory.usage_in_bytes
memory.force_empty memory.kmem.tcp.limit_in_bytes memory.move_charge_at_immigrate memory.use_hierarchy
memory.kmem.failcnt memory.kmem.tcp.max_usage_in_bytes memory.oom_control notify_on_release
memory.stat
报告大范围内存统计,如下表所述:
统计 |
描述 |
cache |
页缓存,包括 tmpfs(shmem),单位为字节 |
Rss |
匿名和 swap 缓存,不包括 tmpfs(shmem),单位为字节 |
Mapped_file |
memory-mapped 映射的文件大小,包括 tmpfs(shmem),单位为字节 |
pgpgin |
存入内存中的页数 |
pgpgout |
从内存中读出的页数 |
swap |
swap 用量,单位为字节 |
Active_anon |
在活跃的最近最少使用(least-recently-used,LRU)列表中的匿名和 swap 缓存,包括 tmpfs(shmem),单位为字节 |
Inactive_anon |
不活跃的 LRU 列表中的匿名和 swap 缓存,包括tmpfs(shmem),单位为字节
|
Active_file |
活跃 LRU 列表中的 file-backed 内存,以字节为单位 |
Inactive_file |
不活跃 LRU 列表中的 file-backed 内存,以字节为单位 |
unevictable |
无法再生的内存,以字节为单位 |
hierarchical_memory_limit |
包含 memory cgroup 的层级的内存限制,单位为字节 |
hierarchical_memsw_limit |
包含 memory cgroup 的层级的内存加 swap 限制,单位为字节 |
memory.limit_in_bytes
设定用户内存的最大量(包括文件缓存)。如果没有指定单位,则将该数值理解为字节。但是可以使用前缀代表更大的单位 -k 或者 K 代表千字节,m 或者 M 代表 MB,g 或者 G 代表 GB。
在 memory.limit_in_bytes 中写入 -1 删除现有限制。
注意不能使用 memory.limit_in_bytes 限制 root cgroup;您只能在该层级中较低的组群中应用这些值。
limit_in_bytes限制内存的范围包括:
1.page fault发生时,有两种情况内核需要给进程分配新的页框。一种是进程请求调页,另一种是copy on write。
2.内核在handle_pte_fault中进行处理时,还有一种情况是pte存在且页又没有映射文件。这种情况说明页面之前在内存中,但是后面被换出到swap空间了。
3.当内核将page加入到page cache中时, mem_cgroup_cache_charge函数正是处理这种情况,它被植入到系统处理page cache的add_to_page_cache_locked函数中。
4.最后mem_cgroup_prepare_migration是用于处理内存迁移中的范围限制。
代码实现对应res->limit。
memory.memsw.limit_in_bytes
设定最大内存与 swap 用量之和。如果没有指定单位,则将该值解读为字节。但是可以使用前缀代表更大的单位 -k 或者 K 代表千字节,m 或者 M 代表 MB,g 或者 G 代表 GB。
不能使用 memory.memsw.limit_in_bytes 限制 root cgroup;只能在该层级中较低的组群中应用这些值。
在 memory.memsw.limit_in_bytes 中写入 -1 删除现有限制。
mount -t cgroup none /mnt/cgroup/ -o memory
查看相关的内存限制:
more memory.limit_in_bytes
9223372036854775807
more memory.memsw.limit_in_bytes
9223372036854775807
将该进程组中的内存限制开启为300MB,如下:
echo 300M > memory.limit_in_bytes
echo 300M >memory.memsw.limit_in_bytes
cat memory.limit_in_bytes
314572800
cat memory.memsw.limit_in_bytes
314572800
将当前进程的PID写入到tasks中,如下:
echo $$ > tasks
通过下面的程序,我们申请内存资源,如下:
more /tmp/test.c
#include <stdlib.h>
#define MALLOC_SIZE 1024 * 1024 * 300
int main(void)
{
char *i = NULL;
long int j;
i = malloc(MALLOC_SIZE);
if(i != NULL) {
for(j = 0; j < MALLOC_SIZE; j++)
*(i+j) = 'a';
}
sleep(5);
return 0;
}
编译后运行,如下:
gcc /tmp/test.c /tmp/test
/tmp/test
Killed
代码实现对应memsw->limit
memory.soft_limit_in_bytes
用于设定一个软上限,进程组使用的资源可以超过这个限制,超过之后内存会加快回收,代码实现对应res_counter-> soft_limit。
memory.failcnt
报告内存达到在 memory.limit_in_bytes 设定的限制值的次数。
memory.memsw.failcnt
报告内存加 swap 空间限制达到在 memory.memsw.limit_in_bytes 设定的值的次数。
memory.force_empty
当设定为 0 时,会清空这个 cgroup 中任务所使用的所有页面的内存。这个接口只可在 cgroup 中没有任务时使用。如果无法清空内存,则在可能的情况下将其移动到上级 cgroup 中。删除 cgroup前请使用 memory.force_empty 以避免将不再使用的页面缓存移动到它的上级 cgroup 中。
memory.swappiness
表示内核换出进程内存的倾向(swap到外部磁盘)。与 /proc/sys/vm/swappiness为整个系统设定的内核倾向中用法相同。默认值为60。低于这个值会降低内核换出进程内存的倾向,将其设定为 0 则完全不会为 cgroup 中的任务换出进程内存。高于这个值将提高内核换出进程内存的倾向,大于 100 时,内核将开始换出作为这个cgroup 中进程的地址空间一部分的页面。
请注意值为 0 不会阻止换出进程内存;系统缺少内存时仍可能发生换出内存,这是因为全局虚拟内存管理时不读取该 cgroup 值。要完全锁定页面,请使用 mlock() 而不时 cgroup。
在root cgroup中不能改变swappiness,它值在 /proc/sys/vm/swappiness 中设定的 swappiness。存在子控制组的cgroup不能修改swappiness。
包含指定是否应将内存使用量计入 cgroup 层级(父控制组直至root_cgroup)的使用量。如果父辈控制组达到了内存用量限制,则会向父辈控制组及其子控制组中申请回收内存。该属性默认关闭。笔者理解默认关闭可以精确控制控制组所辖任务的内存使用,而不关注所辖控制组内存的使用情况,同时申请回收时也不会涉及其他层级的任务。
代码实现对应mem_cgroup-> use_hierarchy
memory.numa_stat
显式每个numa节点的内存使用量
memory.kmem.limit_in_bytes
设置显式内核内存的硬上线,当group的kmem.limit_in_bytes被设置之后,内核空间内存才能计数。当cgroup已有子组或者cgroup已经有任务加入,则不能设置内核limit。这些情况下设置limit会返回-EBUSY。当一个组第一次设置limit时,内核会一直计数,直到组被删除。内存限制也可以通过往memory.kmem.limit_in_bytes写入-1来删除,但这种情况kmem仍会计数,不会做限制。根组的内核内存不强加限制,对根组的使用不会计数。
memory.kmem.usage_in_bytes
显式当前内核内存的使用量。
memory.kmem.failcnt
显示内核内存到达使用上限的次数
memory.kmem.max_usage_in_bytes
显示内核使用内存最多的记录
memory.kmem.tcp.limit_in_bytes
显示设置tcpbuf内存使用的硬上限
memory.kmem.tcp.usage_in_bytes
显示当前tcpbuf内存分配量
memory.kmem.tcp.failcnt
显示tcpbuf内存达到上限的次数
memory.kmem.tcp.max_usage_in_bytes
显示tcpbuf使用内存最多的记录
值得指出的是,目前内核内存没有实现软limit。将来计划limit达到软上限时触发slab回收。
OOM Control
当前oom的状态可以通过2个参数表示:
1、oom_kill_disable:0或1,1表示禁止oom-killer
2、under_oom: 0或1,1表示当前在oom状态,有任务可能阻塞。
可以通过往memory.oom_control文件写入1来禁止oom-killer:
echo 1 > memory.oom_control
如果oom-killer被禁止了,cgroup下的任务当请求不到内存时,将会挂起/睡眠在cgroup的oom-等待队列中。通过如下方式可以使任务继续运行:
1、扩大可使用内存的限制或者减少内存使用
2、杀死其它任务
3、将任务迁移至别的cgroup,并且相应内存可迁移
4、删除一些文件
memory.oom_control文件提供register_event操作用来进行oom的通知机制。内存cgroup使用event_control通知机制实现了oom通知。该机制允许注册多个oom通知消息,当oom发生时投递。
注册通知器,应用需要:
1使用eventfd(2)创建一个eventfd
2打开memory.oom_control 文件
3写字符串"<event_fd> <fd ofmemory.oom_control>"到cgroup.event_control
当oom发生时,通过eventfd通知应用。Oom通知机制对根cgroup不起作用。
实例举例:
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <libgen.h>
#include <limits.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/eventfd.h>
#define USAGE_STR "Usage:cgroup_event_listener <path-to-control-file> <args>\n"
int main(int argc, char **argv)
{
intefd = -1;
intcfd = -1;
intevent_control = -1;
charevent_control_path[PATH_MAX];
charline[LINE_MAX];
intret;
if(argc != 3) {
fputs(USAGE_STR,stderr);
return1;
}
cfd= open(argv[1], O_RDONLY);//要操作的subsystem属性文件,例如oom_contrl或memory.usage_in_bytes
if(cfd == -1) {
fprintf(stderr,"Cannot open %s: %s\n", argv[1],
strerror(errno));
gotoout;
}
ret= snprintf(event_control_path, PATH_MAX, "%s/cgroup.event_control",
dirname(argv[1]));//将参数写到这个配置文件里。
if(ret >= PATH_MAX) {
fputs("Pathto cgroup.event_control is too long\n", stderr);
gotoout;
}
event_control= open(event_control_path, O_WRONLY);//打开cgroupevent.control配置文件
if(event_control == -1) {
fprintf(stderr,"Cannot open %s: %s\n", event_control_path,
strerror(errno));
gotoout;
}
efd= eventfd(0, 0);//使用eventfd(2)创建一个事件通知的文件描述符
if(efd == -1) {
perror("eventfd()failed");
gotoout;
}
ret= snprintf(line, LINE_MAX, "%d %d %s", efd, cfd, argv[2]);//写"<event_fd><control_fd> <args>"
if(ret >= LINE_MAX) {
fputs("Argumentsstring is too long\n", stderr);
gotoout;
}
ret= write(event_control, line, strlen(line) +1); 写"<event_fd> <control_fd> <args>"到cgroup.event_control
if(ret == -1) {
perror("Cannotwrite to cgroup.event_control");
gotoout;
}
//完成写入,守候通知吧。
while(1) {
uint64_tresult;
ret= read(efd, &result, sizeof(result));//任务阻塞在eventfd上,读取eventcount数值
if(ret == -1) {
if(errno == EINTR)
continue;
perror("Cannotread from eventfd");
break;
}
assert(ret== sizeof(result));
ret= access(event_control_path, W_OK);
if((ret == -1) && (errno == ENOENT)) {
puts("Thecgroup seems to have removed.");
ret= 0;
break;
}
if(ret == -1) {
perror("cgroup.event_control"
"isnot accessible any more");
break;
}
printf("%s%s: crossed\n", argv[1], argv[2]);//显示结果
}
out:
if(efd >= 0)
close(efd);
if(event_control >= 0)
close(event_control);
if(cfd >= 0)
close(cfd);
return(ret != 0);
}
运行实例:
# ./ cgroup_event_listener/root/cgroup/test/memory.oom_control 1 &//运行上述用例
#
# echo 1 >/root/cgroup/test/memory.oom_control //关闭oom杀进程
# cat/root/cgroup/test/memory.oom_control
oom_kill_disable 1
under_oom 0
#
# echo 1M >/root/cgroup/test/memory.limit_in_bytes//设置内存使用上限
#
# echo $$ >/root/cgroup/test/tasks
# a="$(dd if=/dev/zero bs=10Mcount=10)"
/root/cgroup/test1: crossed //达到1M内存使用上限,通知cgroup_event_listener进程
# cat /root/cgroup/test/memory.usage_in_bytes
131072
# cat/root/cgroup/test/memory.limit_in_bytes
1048576
# cat/root/cgroup/test/memory.max_usage_in_bytes
1048576//查看记录使用的最大值与上限一致
可见这里并没有触发oom而是发送信号让cgroup_event_listener进程监控到,并进行处理。其中./ cgroup_event_listener /root/cgroup/test/memory.oom_control 1的第二个参数1没有用到,但如果监控的是另外的对象例如usage_in_bytes,则需要第二个参数:
# ./ cgroup_event_listener/root/cgroup/test/memory.usage_in_bytes 5M &
# echo $$ >/root/cgroup/test/tasks
# a="$(dd if=/dev/zero bs=10Mcount=10)"
/root/cgroup/test 5M: crossed
这里5M表示监控范围,当usage_in_bytes>5M时会触发event_control通知cgroup_event_listener,这也是一种用法。
此外cpuset 子系统可为cgroup 分配独立内存节点以及内存紧张监控:
cpuset.mems
指定允许这个 cgroup 中任务可访问的内存节点。这是一个用逗号分开的列表,格式为 ASCII,使用小横线("-")代表范围。
# mount -t cgroup none /mnt/cgroup/-o cpuset
# cat /mnt/cgroup/mems
cat: can't open '/mnt/cgroup/mems':No such file or directory
# cat /mnt/cgroup/cpuset.mems
0
#mkdir memset
# cd memset
#mkdir /mnt/cgroup/memstest
# cat cpuset.mems
# echo 0 > cpuset.mems
# cat cpuset.mems
0
#
查看cpuset.mems,这里显示的是0,说明默认情况下系统可以使用结点0的内存系统,大多数系统是单路控制,不是NUMA,所以只看到了0。
另外需要强调新创建的CPUSET的mems和cpus都是空的,使用前必须先初始化,否则不能使用cpuset功能。
# echo 1086 > /tmp/tmp1/tasks
为什么没有生效?看看是不是没有完成mems和cpus的初始化。
顶层CPUSET包含了系统中的所有cpuset.mems,而且是只读的,不能更改。
cpuset.memory_migrate
当一个任务从一个CPUSET1(mems值为0)迁移至另一个CPUSET2(mems值为1)的时候,此任务在节点0上分配的页面内容将迁移至节点1上分配新的页面(将数据同步到新页面),这样就避免了此任务的非本地节点的内存访问。
默认情况下禁止内存迁移(0)且页就保留在原来分配的节点中,即使在 cpuset.mems 中现已不再指定这个节点。如果启用(1),则该系统会将页迁移到由 cpuset.mems 指定的新参数中的内存节点中。
cpuset.mem_exclusive
与cpuset.cpu_exclusive类似,cpuset.mem_exclusive包含指定是否其它 cpuset 可共享为这个 cpuset 指定的内存节点的标签(0 或者 1)。默认情况下(0)内存节点不是专门分配给某个 cpuset 的。专门为某个 cpuset 保留内存节点(1)。
cpuset.mem_hardwall
即便使用了cpuset.mems对内存进行限制,但系统中的任务并不能完全孤立,比如还是可能会全局共享Page Cache,动态库等资源,因此内核在某些情况下还是可以允许打破这个限制,如果不允许内核打破这个限制,需要设定CPUSET的内存硬墙标志即mem_hardwall置1即可,启用 hardwall 时每个任务的用户分配应保持独立,CPUSET默认是软墙。
硬软墙用于Buddy系统的页面分配,优先级高于内存策略,请参考内核函数:
cpuset_zone_allowed_hardwall()和cpuset_zone_allowed_softwall()。
cpuset.memory_pressure_enabled
是否计算cpuset中内存压力。内存压力是指当前系统的空闲内存不能满足当前的内存分配请求的速率。计算出的值会输出到 cpuset.memory_pressure,报告为尝试每秒再生内存的整数值再乘 1000。
cpuset.memory_pressure
包含运行在这个 cpuset 中产生的平均内存压力的只读文件。启用cpuset.memory_pressure_enabled 时,这个伪文件中的值会自动更新,否则值为0。
cpuset.memory_spread_slab
包含指定是否应在 cpuset 的各内存节点上平均分配用于文件输入/输出操作的内核缓存的标签(0 或者 1)。默认情况是 0,即不尝试平均分配内核缓存,并将缓存放在生成这些缓存的进程所运行的同一节点中。
#ifdef CONFIG_NUMA
struct page *__page_cache_alloc(gfp_t gfp)
{
if (cpuset_do_page_mem_spread()) {
int n = cpuset_mem_spread_node();
return alloc_pages_exact_node(n, gfp, 0);
}
return alloc_pages(gfp, 0);
}
cpuset.memory_spread_page
包含指定是否应将文件系统缓冲平均分配给这个 cpuset 的内存节点的标签(0 或者 1)。默认情况为 0,不尝试为这些缓冲平均分配内存页面,且将缓冲放置在运行生成缓冲的进程的同一节点中。