Inside Memory Control Group

Published:

Memory Control Group Overview

memcg根据内存使用的情况不同将统计的内存分为四类:

  1. 交换空间(swap)
  2. 用户内存,包括匿名页(anonymous page)、页缓存(page cache)等与用户关系密切的内存;
  3. 套接字内存,在网络栈的实现中用于存放套接字信息的内存。
  4. 内核内存,内核中使用的用于各种用途的内存,memcg中内核内存不包括网络栈所使用的套接字内存;

对这四类内存的统计通过 memcg 的三个编译选项来配置,分别是 CONFIG_MEMCG、CONFIG_MEMCG_SWAP和 CONFIG_MEMCG_KMEM。CONFIG_MEMCG 是 memcg 的总开关,如果开启,memcg 将被启用,统计用户内存和套接字内存; CONFIG_MEMCG_SWAP 控制交换空间(swap)的统计;CONFIG_MEMCG_KMEM 控制内核内存的统计。

Memcg Data Structures

memcg 使用五个计数器来对这四类内存进行统计,这些计数器信息储存在struct mem_cgroup中, 在Linux v5.10中,struct mem_cgroup有较大变化,添加了v2内容

struct mem_cgroup {
	struct cgroup_subsys_state css;

	/* Private memcg ID. Used to ID objects that outlive the cgroup */
	struct mem_cgroup_id id;

	/* Accounted resources */
	struct page_counter memory;		/* Both v1 & v2 */

	union {
		struct page_counter swap;	/* v2 only */
		struct page_counter memsw;	/* v1 only */
	};

	/* Legacy consumer-oriented counters */
	struct page_counter kmem;		/* v1 only */
	struct page_counter tcpmem;		/* v1 only */
    ......
    struct obj_cgroup __rcu *objcg;
}

struct page_counter {
	atomic_long_t usage;
	unsigned long min;
	unsigned long low;
	unsigned long high;
	unsigned long max;
    ......
}

因为每一个内存,包含memory, swap, kmem and tcpmem都是struct page_count结构,所以所有的charge都要是通过page_counter_try_charge或者page_counter_charge来对struct page_count->usage进行加减,可以通过对这些函数的分析,同时参数是从mem_cgroup来的,过滤出所有的memcg charge/uncharge函数。

同时page_counter_charge不会检查max,所以可以直接绕过memcg限制。

struct page同样包含对mem_cgroup的引用:

struct page {
	unsigned int page_type;
#ifdef CONFIG_MEMCG
	union {
		struct mem_cgroup *mem_cgroup;
		struct obj_cgroup **obj_cgroups;
	};
#endif

#define PG_kmemcg	0x00000200

page_type会被kmem uncharge检查,判断是不是kmem,同时判断是否已被uncharge;mem_cgroup会被user mem uncharge检查。

Charge流程

四种内存charge函数如下:

Mem TypeConfig SwitchCharge FlagCharge Function
用户内存CONFIG_MEMCGNo flagmem_cgroup_charge
swapCONFIG_MEMCG_SWAPNo flagmem_cgroup_charge
套接字内存CONFIG_MEMCGNo flagmem_cgroup_charge_skmem
内核内存CONFIG_MEMCG_KMEMGFP_ACCOUNT__memcg_kmem_charge

mem_cgroup_charge调用如下:

–>try_charge

–>–>page_counter_try_charge(&memcg->memsw,...)

–>–>page_counter_try_charge(&memcg->memory,...)

__memcg_kmem_charge调用如下:

–>try_charge

–>–>page_counter_try_charge(&memcg->memsw,...)

–>–>page_counter_try_charge(&memcg->memory,...)

–>page_counter_try_charge(&memcg->kmem, ...)

从上面我们可以看出,每次memcg->kmem增加,同样会增加memcg->memory,貌似memory是总量,包含kmem,这个要从Docker memcg的使用再确认下。

mem_cgroup_charge_skmem需要再好好理解下: 1)不在default里面,调用page_counter_try_charge(&memcg->tcpmem,…),charge到memcg->tcpmem,这说明memory有可能不包括tcpmem; 如果在default里面,调用try_charge,charge到memcg->memsw和memcg->memory; 2)就算超了也会charge,保证不失败

Get Process’s mem_cgroup

task_struct 中多个member和memcg有关

#ifdef CONFIG_MEMCG
	struct mem_cgroup		*memcg_in_oom;
	gfp_t				memcg_oom_gfp_mask;
	int				memcg_oom_order;

	/* Number of pages to reclaim on returning to userland: */
	unsigned int			memcg_nr_pages_over_high;

	/* Used by memcontrol for targeted memcg charge: */
	struct mem_cgroup		*active_memcg;
#endif
#ifdef CONFIG_CGROUPS
	/* Control Group info protected by css_set_lock: */
	struct css_set __rcu		*cgroups;
	/* cg_list protected by css_set_lock and tsk->alloc_lock: */
	struct list_head		cg_list;
#endif

在charge之前,要找到正确的memcg进行charge,Linux提供了多个函数定位memcg

mem_cgroup_from_task

get_mem_cgroup_from_mm

get_mem_cgroup_from_page

get_active_memcg

get_mem_cgroup_from_current

mem_cgroup_from_obj

get_mem_cgroup_from_current 返回会首先返回task_struct->active_memcg;

其中用的最广的是get_mem_cgroup_from_mm,但mm_struct中没有memcg的data structure,还需要从task_struct中来,调用mem_cgroup_from_task ,在其中调用mem_cgroup_from_css,从css_set中定位struct mem_cgroup,但是这里面的mem_cgroup和task_struct->active_memcg是不是指向同一个还需要研究。

memcg提供一个root_mem_cgroup,mem_cgroup_charge–>try_charge在对root_mem_cgroup进行accouting时,会自动跳过charging。

User Memory Accouting

用户内存的统计在内存分配后通过 memcg 的统计接口进行,涉及匿名页、页缓存(page cache)等。 一般场景下用户内存是比例最高的一部分,但是因为内核中进行用户内存分配的地方相对较少,本文的分析工具将用户内存统计的分析结果作为内核内存统计分析的补充(后期要区分开,user的如果缺失,问题更大)。

用户内存memcg主要通过mem_cgroup_chargemem_cgroup_uncharge进行charge和uncharge。

其中mem_cgroup_charge –> try_charge–>page_counter_try_chargepage_counter_try_charge会对相应的page_counter->usage进行增加,并且检查limit,返回success or fail。

mem_cgroup_uncharge –>uncharge_batch –>page_counter_uncharge –>page_counter_cancelpage_counter_cancel里对page_counter->usage进行减少,达到uncharge的目的。

值得注意的是在用户内存memcg中并没有对__GFP_ACCOUNT flag进行检查,所以是不依赖于flag的。

mem_cgroup_charge Caller

__do_huge_pmd_anonymous_page

do_swap_page

do_anonymous_page

do_cow_fault

do_shared_fault

这些函数均被__do_page_fault –> handle_mm_fault –> __handle_mm_fault –> handle_pte_fault调用

do_anonymous_page 分析

static vm_fault_t do_anonymous_page(struct vm_fault *vmf)
{
	page = alloc_zeroed_user_highpage_movable(vma, vmf->address);
	if (!page)
		goto oom;

	if (mem_cgroup_charge(page, vma->vm_mm, GFP_KERNEL))
		goto oom_free_page;
}

其中调用了alloc_zeroed_user_highpage_movable进行内存分配,调用链是alloc_zeroed_user_highpage_movable –> __alloc_zeroed_user_highpage –> alloc_page_vma –> alloc_pages_vma –> __alloc_pages_nodemask

struct page * __alloc_pages_nodemask(...)
{
	if (memcg_kmem_enabled() && (gfp_mask & __GFP_ACCOUNT) && page &&
	    unlikely(__memcg_kmem_charge_page(page, gfp_mask, order) != 0)) {
		__free_pages(page, order);
		page = NULL;
	}
}

整个调用链上没有设置__GFP_ACCOUNT,因此kmem没有被charge,所以anonymous page进行了1次charge,一次在do_anonymous_page使用mem_cgroup_charge将page charge到用户内存上;没有kmem charge。

对于此类问题的分析方法可以从handle_pte_fault开始进行分析,遍历所有path,直到叶子节点。如果一个path上有两次page_count_try_charge,便是over charge问题。

do_cow_fault 分析

static vm_fault_t do_cow_fault(struct vm_fault *vmf)
{
	vmf->cow_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma, vmf->address);
	if (!vmf->cow_page)
		return VM_FAULT_OOM;

	if (mem_cgroup_charge(vmf->cow_page, vma->vm_mm, GFP_KERNEL)) {
		put_page(vmf->cow_page);
		return VM_FAULT_OOM;
	}
	cgroup_throttle_swaprate(vmf->cow_page, GFP_KERNEL);

	ret = __do_fault(vmf);
}

alloc_page_vma没有charge,mem_cgroup_charge会charge到用户内存。

我们这里面重点分析__do_fault,其中存在一个间接调用ret = vma->vm_ops->fault(vmf);。这个间接调用的target可能是filemap_fault

const struct vm_operations_struct generic_file_vm_ops = {
	.fault		= filemap_fault,
	.map_pages	= filemap_map_pages,
	.page_mkwrite	= filemap_page_mkwrite,
};

filemap_fault中,

vm_fault_t filemap_fault(struct vm_fault *vmf)
{
	/*
	 * Do we have something in the page cache already?
	 */
	page = find_get_page(mapping, offset);
	if (likely(page) && !(vmf->flags & FAULT_FLAG_TRIED)) {
		/*
		 * We found the page, so try async readahead before
		 * waiting for the lock.
		 */
		fpin = do_async_mmap_readahead(vmf, page);
	} else if (!page) {
		/* No page in the page cache at all */
		count_vm_event(PGMAJFAULT);
		count_memcg_event_mm(vmf->vma->vm_mm, PGMAJFAULT);
		ret = VM_FAULT_MAJOR;
		fpin = do_sync_mmap_readahead(vmf);
retry_find:
		page = pagecache_get_page(mapping, offset,
					  FGP_CREAT|FGP_FOR_MMAP,
					  vmf->gfp_mask);
}

如果已经在page cache里了,并不会进行任何charge操作。

如果不在page cache里,调用pagecache_get_page–>pagecache_get_page–>__page_cache_alloc–>__alloc_pages_node –>__alloc_pages–>__alloc_pages_nodemask

__alloc_pages_nodemask前面我们已经分析过,会charge内核内存,但是整个调用连上没有set __GFP_ACCOUNT,因此并不会被charge到kmem。真正的charge在pagecache_get_page –> add_to_page_cache_lru –> __add_to_page_cache_locked –> mem_cgroup_charge(page, current->mm, gfp) 所以我们可以看到这里面是和文档是一致的,当一个page被插入到page cache中时才进行charge,并且charge到user memory。

所以总结一下:前面因为mem_cgroup_charge会charge到用户内存上,所有path都要经过

1)mem_cgroup_charge –> filemap_fault在page cache里:1次user memory charge

2)mem_cgroup_charge –> filemap_fault不在page cache里:2次user memory charge

这里面存在overcharge

do_shared_fault 分析

其中只调用了__do_fault,根据前面分析,总结如下:

1)如果已经在page cache里了,并不会进行任何charge操作,这样会引入missing charge的问题,两个memcg可以collude。

2)不在page cache里了,会调用pagecache_get_page –> add_to_page_cache_lru,进行1次user memory charge。

用户memcg charge和major/minor page fault关系

major/minor page fault counts are in task_struct as maj_flt and min_flt.

The count happens in mm_account_fault, called by handle_mm_fault. The VM_FAULT_MAJOR bit must be set in order to be counted as major faults.

major fault是需要硬盘disk支持的才能处理的fault,一般是由文件支持或者swap page的处理。在mm/memory.c中,do_swap_page中设置了VM_FAULT_MAJOR,而do_anonymous_page并没有设置,所以do_anonymous_page不会增加major faults。

major和minor page faults只是区分有没有disk支持,memcg charge是charge physical frame的使用,二者没有一一对应关系。

Potential Problems

  • Missing charge
    • 很多user memory allocation的地方,比如ELF loading的page,没有accouting,比如fork和exec syscall,为ELF binary分配一定空间,如何分析出来
    • 现在大多是基于page fault进行charge,non page fault分配的如何系统找出
    • 如何系统地找出来所有user memory,从一个user space memory layout来看:
      • Binary –> not account
      • heap –> anonymous page
      • shared library –> not account
      • stack –> anonymous page
    • 如何系统地找出来所有user memory,从syscall interface来看:
    • 如何系统地找出来所有user memory,从memory usage来看:
      • swap
      • IPC: signal, shmem, msgqueue,pipe
  • Mis-charge
    • get_mem_cgroup_from_current 和 xxx_from_mm貌似返回值可能不是同一个,这个会造成account突破limit
    • payer是不是都对
  • Over charge:分析mem_cgroup_charge和uncharge是不是成对出现
  • Remote charge (针对共享page的charge)
    • 共享page是如何charge的,如果被charge的memcg crash了,会被重新charge到其他memcg上么
    • 这个可以细致分析do_swap_page,可以使用qemu动态分析,如果被charge的memcg与current->memcg不同,即打出来
    • shmem以及其他IPC机制也要分析

Kernel Memory Accouting

内核内存的统计根据分配机制的不同存在两种方式。 整page的内存在伙伴系统中分配,如果内存分配进行时带有 __GFP_ACCOUNT flag,那么该内存会被 memcg 所统计; 小于page的内存在slab中分配,除了带有 __GFP_ACCOUNT 的内存分配外,如果一个 kmem_cache 在创建时有指定被memcg所统计,那么所有用这个 kmem_cache 进行的内存分配都会被memcg 统计。

Kernel memory accounting主要有4个函数:

__memcg_kmem_charge

__memcg_kmem_uncharge

__memcg_kmem_charge_page

__memcg_kmem_uncharge_page

在这些函数内部没有检查__GFP_ACCOUNT。

__memcg_kmem_charge_page内部也是调用了__memcg_kmem_charge。 __memcg_kmem_charge_page被__alloc_pages_nodemask调用,在调用之前会检查__GFP_ACCOUNT。

__memcg_kmem_charge会被obj_cgroup_charge调用,随后obj_cgroup_charge会被 pcpu_memcg_pre_alloc_hookmemcg_slab_pre_alloc_hook。两处调用之前都有__GFP_ACCOUNT检查,有flag才会charge。

Potential Problems

  • Missing charge:貌似加了__GFP_NOFAIL便可以绕过limit,code
  • v5.10版本里,貌似obj_charge只有两处;v5.3.6没有对任何obj进行charge?
  • memcg_kmem_bypass current->mm为空即bypass charge

Object memory charging

Linux v5.9引入obj_cgroup

/*
 * Bucket for arbitrarily byte-sized objects charged to a memory
 * cgroup. The bucket can be reparented in one piece when the cgroup
 * is destroyed, without having to round up the individual references
 * of all live memory objects in the wild.
 */
struct obj_cgroup {
	struct percpu_ref refcnt;
	struct mem_cgroup *memcg;
	atomic_t nr_charged_bytes;
	union {
		struct list_head list;
		struct rcu_head rcu;
	};
};

struct memcg_stock_pcp {
	struct mem_cgroup *cached; /* this never be root cgroup */
	unsigned int nr_pages;

#ifdef CONFIG_MEMCG_KMEM
	struct obj_cgroup *cached_objcg;
	unsigned int nr_bytes;
#endif

	struct work_struct work;
	unsigned long flags;
#define FLUSHING_CACHED_CHARGE	0
};
  • obj_cgroup没有page_counter,所以所有的charge/uncharge是在memcg上
  • 注释是说cgroup distory后可以reparent

Memcg计数是page_counter,最小粒度是每个页page。obj_cgroup是用来计数小于page的memory分配,所以设计比较简单,维护一个单独的计数器[memcg_stock_pcp->nr_bytes](https://elixir.bootlin.com/linux/v5.10/source/mm/memcontrol.c#L2212),保存已经charge的不满一页的内存。每一次object charge,先检查memcg_stock_pcp->nr_bytes,如果能满足(nr_bytes大于要分配size)就减去,直接返回,不能满足就调用kmem charge,多分配。

int obj_cgroup_charge(struct obj_cgroup *objcg, gfp_t gfp, size_t size)
{
	if (consume_obj_stock(objcg, size))
		return 0;

	memcg = obj_cgroup_memcg(objcg);

	nr_pages = size >> PAGE_SHIFT;
	nr_bytes = size & (PAGE_SIZE - 1);

	if (nr_bytes)
		nr_pages += 1;

	ret = __memcg_kmem_charge(memcg, gfp, nr_pages);
}

void obj_cgroup_uncharge(struct obj_cgroup *objcg, size_t size)
{
	refill_obj_stock(objcg, size);
}

uncharge与charge对应,如果memcg_stock_pcp->nr_bytes大于page size,就调用kmem uncharge,uncharge一页。

Memory Uncharging

User memory uncharging

mem_cgroup_uncharge

–>uncharge_page

–>–>检查,为0退出;otherwise uncharge并clear page->mem_cgroup

–>–>检查,为0退出;otherwise uncharge并clear page->page_type

–>uncharge_batch

–>–>page_counter_uncharge(&ug->memcg->memory,…)

–>–>page_counter_uncharge(&ug->memcg->memsw,…)

–>–>page_counter_uncharge(&ug->memcg->kmem,…)

总结来说:mem_cgroup_uncharge既能uncharge user,也能uncharge kmem,会检查并清空两种flag: page->mem_cgroup和page->page_type.

分析可以检查:page_counter_uncharge之前是否check,并且clear两种flag。

Kmem uncharging

__memcg_kmem_uncharge

–>page_counter_uncharge(&memcg->kmem, nr_pages);

–>page_counter_uncharge(&memcg->memory, nr_pages);

–>page_counter_uncharge(&memcg->memsw, nr_pages);

没有检查flag。

__memcg_kmem_uncharge_page

void __memcg_kmem_uncharge_page(struct page *page, int order)
{
	__memcg_kmem_uncharge(memcg, nr_pages);
	page->mem_cgroup = NULL;
	
	if (PageKmemcg(page))
		__ClearPageKmemcg(page);
}

可以看到

  • 进行uncharge
  • 没有检查,但是clear page->mem_cgroup
  • uncharge之后,检查并clear page->page_type

这里面我们可以看到在uncharge之前没有任何flag检查,

总结:

  • __memcg_kmem_uncharge_page之前要使用flag检查
    • 两个检查都缺失会导致duplicate uncharge
    • 一个检查缺失结果待定,要看两个flag的set是否成对
  • __memcg_kmem_uncharge_page uncharges kmem, memory, memsw
  • mem_cgroup_uncharge 也是uncharges kmem, memory, memsw;是依靠page->page_type来判断时候是kmem
  • 2个mem_cgroup_uncharge
    • 没问题,第一个会clear flag,第二个不会uncharge
  • mem_cgroup_uncharge在__memcg_kmem_uncharge_page之前
    • mem_cgroup_uncharge会把kmem, memory, memsw都uncharge了,并且clear 2个flag,
    • 但是__memcg_kmem_uncharge_page会duplicate uncharge
  • mem_cgroup_uncharge在__memcg_kmem_uncharge_page之后
    • 没有问题,__memcg_kmem_uncharge_page会clear flag,mem_cgroup_uncharge不会uncharge了
  • 两个__memcg_kmem_uncharge_page
    • 会存在duplicate uncharge问题

One uncharge case

free_the_page 一支是:

–>free_unref_page

–>–>free_unref_page_prepare –>free_pcp_prepare –>free_pages_prepare –>__memcg_kmem_uncharge_page –>__memcg_kmem_uncharge

void __memcg_kmem_uncharge(struct mem_cgroup *memcg, unsigned int nr_pages)
{
	if (!cgroup_subsys_on_dfl(memory_cgrp_subsys))
		page_counter_uncharge(&memcg->kmem, nr_pages);

	page_counter_uncharge(&memcg->memory, nr_pages);
	if (do_memsw_account())
		page_counter_uncharge(&memcg->memsw, nr_pages);
}

另一支是:

–>__free_pages_ok –>–>free_pages_prepare –>……

总结来说:2支都会调用__memcg_kmem_uncharge_page,同时在 调用之前会使用PageKmemcg检查struct page中的page_typePG_kmemcg bit是不是被set,因此没有问题。

另一次疑似问题:

void free_compound_page(struct page *page)
{
	mem_cgroup_uncharge(page);
	__free_pages_ok(page, compound_order(page), FPI_NONE);
}

__free_pages_ok会对flag进行检查,所以不会调用 __memcg_kmem_uncharge,之前只有mem_cgroup_uncharge对memcg->memory, memsw,kmem进行uncharge,所以没问题。