关于cgroup,memcg,还有与task之间的联系我在另一篇博客cgroup和namespace有写。

memcg统计每个NUMA的信息

在多个NUMA的系统中,一个task的内存可能从多个NUMA中分配,所以一个memcg会用mem_cgroup_per_node区分每个NUMA的信息。它的核心作用是实现内存控制组的NUMA局部性统计、回收和限制,确保内存资源分配和回收能够感知NUMA架构的物理拓扑。如果要在NUMA节点间做内存调度需要关注这部分,在5.15.19这个内核是没有实现按照NUMA节点粒度来限制内存回收的。

// include/linux/memcontrol.h
struct mem_cgroup {
  	...
  	struct mem_cgroup_per_node *nodeinfo[];
#ifdef CONFIG_ARTMEM
  	// 自定义参数,用于统计这个memcg总体内存冷热情况
	unsigned long max_nr_dram_pages; /* 限制DRAM的NUMA节点可以使用的内存量 */
  	unsigned long nr_alloc;
	/* stat for sampled accesses */
	unsigned long nr_sampled; /* the total number of sampled accesses */
	unsigned long nr_dram_sampled; /* accesses to DRAM: n(i) */
	/* thresholds */
	unsigned int hot_threshold;
	unsigned int warm_threshold;
#endif
};

struct mem_cgroup_per_node {
    struct lruvec      lruvec;          // 管理该cgroup在该node上的LRU页面链表
    struct lruvec_stats_percpu __percpu	*lruvec_stats_percpu; // per-CPU LRU统计,提高并发性能
	  unsigned long		usage_in_excess; 
    // 记录当前mem_cgroup在该NUMA节点上超出软限制(soft limit)的内存量,用于触发限制机制
    struct mem_cgroup_reclaim_iter	iter; 
    // 在内存回收(如 kswapd、直接回收)时,记录当前扫描的位置,实现增量式LRU链表遍历,避免一次性扫描所有页面
    struct mem_cgroup  *memcg;          
    // 所属的memory cgroup,在代码注释中写了无法直接用container_of反向获取父结构体,因此需显式存储指针。
#ifdef CONFIG_ARTMEM
	unsigned long		max_nr_base_pages; // 限制NUMA粒度的最大内存使用
	bool			need_demotion; // 只是限制DRAM的使用量(如果PMEM都限制了不如直接OOM),所以只判断是否demotion
#endif
};

struct cftype: cgroup控制文件接口

// include/linux/cgroup-defs.h
struct cftype {
	const char *name;  // 文件名,例如 "memory.limit_in_bytes"
	enum cftype_flags flags; // 文件权限、是否是只读?

	/* 读写函数指针 */
	ssize_t (*read)(struct cgroup_subsys_state *css, struct cftype *cft, char *buf); // 用户读文件发生了什么,是对哪个css操作的
	ssize_t (*write)(struct cgroup_subsys_state *css, struct cftype *cft, const char *buf, size_t nbytes);

	/* 类型化读写(比如直接读写 u64) */
	u64 (*read_u64)(struct cgroup_subsys_state *css, struct cftype *cft); 
  // 如果不知道什么类型会放在char *buf中,但是一般读取uint64数据比较多,可以直接使用这个函数
	int (*write_u64)(struct cgroup_subsys_state *css, struct cftype *cft, u64 val);
	...
	struct list_head node; // 加入 controller 的 cftype 列表
#ifdef CONFIG_ARTMEM
	int numa_node_id; // 新增用于观察所在numa node
#endif
};

每个cgroup控制器(如 memory、cpu、blkio)都可以定义很多用户可见的控制文件,它们的行为由struct cftype 决定。之后增加接口会涉及到这部分的读写指针的使用。

增加NUMA节点的控制接口

在NUMA架构中存在多个内存节点,每个内存节点在Linux内核用pg_data_t类型(实际是struct pglist_data)来表示。要实现memcg的NUMA粒度控制还需要:

// include/linux/mmzone.h
typedef struct pglist_data {
	...
#ifdef CONFIG_ARTMEM
	struct cftype *memcg_artmem_node_file; /* max, terminate. */
#endif
} pg_data_t;

注册NUMA节点的控制文件

向memory cgroup控制器(memory_cgrp_subsys)注册一组新的控制文件比如memcg_artmem_file,目的是动态添加进memory controller的文件列表中。memcg_artmem_file文件主要是做开关用,而memcg_artmem_node_file主要是做控制NUMA节点可分配内存大小限制用。

static int __init mem_cgroup_artmem_enable_init(void)
{
    WARN_ON(cgroup_add_dfl_cftypes(&memory_cgrp_subsys,
		memcg_artmem_file));
    return 0;
}
subsys_initcall(mem_cgroup_artmem_enable_init);

int mem_cgroup_per_node_artmem_init(void)
{
    int nid;

    for_each_node_state(nid, N_MEMORY) {
		struct pglist_data *pgdat = NODE_DATA(nid);

		if (!pgdat || pgdat->memcg_artmem_node_file)
	    	continue;
		if (pgdat_memcg_artmem_init(pgdat))
			// 这个函数主要目的:pgdat->memcg_artmem_node_file = kzalloc(sizeof(struct cftype) * 2, GFP_KERNEL);
	    	continue;

		snprintf(pgdat->memcg_artmem_node_file[0].name, MAX_CFTYPE_NAME,
			"max_at_node%d", nid);
		pgdat->memcg_artmem_node_file[0].flags = CFTYPE_NOT_ON_ROOT;
		pgdat->memcg_artmem_node_file[0].seq_show = memcg_per_node_max_show;
		pgdat->memcg_artmem_node_file[0].write = memcg_per_node_max_write;
		pgdat->memcg_artmem_node_file[0].numa_node_id = nid;

		WARN_ON(cgroup_add_dfl_cftypes(&memory_cgrp_subsys,
				pgdat->memcg_artmem_node_file));
	}
		return 0;
}
subsys_initcall(mem_cgroup_per_node_artmem_init);

subsys_initcall()是内核的一种特殊初始化宏机制,会把mem_cgroup_artmem_enable_init()注册为 一个“子系统初始化”阶段执行的函数,在内核启动early stage被调用。内核自带的子系统会直接放在memory_cgrp_subsys.dfl_cftypes里。

struct cftype实现的memcg_artmem_file描述了控制接口;cgroup_add_dfl_cftypes()负责把这些接口挂载到对应的controller上。所有控制文件都要用struct cftype[]数组来定义总不能每个文件写一个register函数吧。一次注册一批文件,减少调用频率,方便组织和管理。

int cgroup_add_dfl_cftypes(struct cgroup_subsys *ss, struct cftype cft[]);

NUMA节点热插拔

如果说像是PMEM这种热插拔的NUMA节点,会在系统启动之后才有,原来的初始化没法涵盖未来新增节点。而 online_pages()是处理“新内存上线”的关键路径,所以在这里重新调用。

// linux\mm\memory_hotplug.c
int __ref online_pages(unsigned long pfn, unsigned long nr_pages,
		       struct zone *zone, struct memory_group *group)
{
	. . .
	mem_hotplug_done();
#ifdef CONFIG_ARTMEM
	mem_cgroup_per_node_artmem_init();
#endif
	. . .
}

实现控制接口

memcg_artmem_file[]控制接口具体要做什么事情:

static struct cftype memcg_artmem_file[] = {
    { // 注意这里是数组,要用空条目结束
	.name = "artmem_enabled",
	.flags = CFTYPE_NOT_ON_ROOT,
	.seq_show = memcg_artmem_show,
	.write = memcg_artmem_write,
    },
    {},
};

static int memcg_artmem_show(struct seq_file *m, void *v)
{
    struct mem_cgroup *memcg = mem_cgroup_from_css(seq_css(m));
	...
	// 通过一系列逻辑判断是否启用了这个功能,然后返回输出
	seq_printf(m, "[enabled] disabled\n");
    return 0;
}

static ssize_t memcg_artmem_write(struct kernfs_open_file *of,
	char *buf, size_t nbytes, loff_t off)
{
    struct mem_cgroup *memcg = mem_cgroup_from_css(of_css(of));
    int nid;

    if (sysfs_streq(buf, "enabled"))
		...
    else if (sysfs_streq(buf, "disabled"))
		...
    else
		return -EINVAL;
    return nbytes;
}

memcg_artmem_node_file[] 接口具体做的事情:

static int memcg_per_node_max_show(struct seq_file *m, void *v)
{
    struct mem_cgroup *memcg = mem_cgroup_from_css(seq_css(m));
    struct cftype *cur_file = seq_cft(m);
    int nid = cur_file->numa_node_id;
    unsigned long max = READ_ONCE(memcg->nodeinfo[nid]->max_nr_base_pages);

    if (max == ULONG_MAX)
		seq_puts(m, "max\n");
    else
		seq_printf(m, "%llu\n", (u64)max * PAGE_SIZE);

    return 0;
}

static ssize_t memcg_per_node_max_write(struct kernfs_open_file *of,
	char *buf, size_t nbytes, loff_t off)
{
    struct mem_cgroup *memcg = mem_cgroup_from_css(of_css(of));
    struct cftype *cur_file = of_cft(of);
    int nid = cur_file->numa_node_id;
    unsigned long max, nr_dram_pages = 0;
    int err, n;

    buf = strstrip(buf);
    err = page_counter_memparse(buf, "max", &max);
    if (err)
		return err;

    xchg(&memcg->nodeinfo[nid]->max_nr_base_pages, max);
    return nbytes;
}

新的memcg创建和初始化

不是在创建整个cgroup,而是专门为memory controller(memcg)创建并初始化一个mem_cgroup实例。当一个新的cgroup被挂载到memory控制器上时(例如通过 mkdir /sys/fs/cgroup/memory/my_group),内核就需要创建这个mem_cgroup来为这个cgroup维护内存资源信息。

static struct mem_cgroup *mem_cgroup_alloc(void)
{
	struct mem_cgroup *memcg;
	unsigned int size;
	int node;
	int __maybe_unused i;
	long error = -ENOMEM;

	size = sizeof(struct mem_cgroup);
	size += nr_node_ids * sizeof(struct mem_cgroup_per_node *);
  	// 由于是NUMA系统,所以需要为每个节点分配mem_cgroup_per_node *指针数组。
	memcg = kzalloc(size, GFP_KERNEL);
	if (!memcg)
		return ERR_PTR(error);

	memcg->id.id = idr_alloc(&mem_cgroup_idr, NULL,
				 1, MEM_CGROUP_ID_MAX,
				 GFP_KERNEL);
	if (memcg->id.id < 0) {
		error = memcg->id.id;
		goto fail;
	}

  	// 每CPU上各自维护一份统计信息,以减少争用和提升效率
	memcg->vmstats_percpu = alloc_percpu_gfp(struct memcg_vmstats_percpu,
						 GFP_KERNEL_ACCOUNT);
	if (!memcg->vmstats_percpu)
		goto fail;

	for_each_node(node)
		if (alloc_mem_cgroup_per_node_info(memcg, node))
			goto fail;

  	// 初始化各种子系统
	if (memcg_wb_domain_init(memcg, GFP_KERNEL))
		goto fail;

  	// 初始化异步任务、锁、链表等必要字段
	INIT_WORK(&memcg->high_work, high_work_func);
	INIT_LIST_HEAD(&memcg->oom_notify);
	mutex_init(&memcg->thresholds_lock);
	spin_lock_init(&memcg->move_lock);
	...
#ifdef CONFIG_ARTMM
	memcg->max_nr_dram_pages = ULONG_MAX;
	memcg->nr_alloc = 0;
	memcg->nr_sampled = 0;
	memcg->nr_dram_sampled = 0;
	memcg->active_threshold = thres_hot;
	memcg->warm_threshold = thres_hot;
	spin_lock_init(&memcg->access_lock);
#endif
  	// 在idr中注册
	idr_replace(&mem_cgroup_idr, memcg, memcg->id.id);
	return memcg;
fail:
	mem_cgroup_id_remove(memcg);
	__mem_cgroup_free(memcg);
	return ERR_PTR(error);
} 

static int alloc_mem_cgroup_per_node_info(struct mem_cgroup *memcg, int node)
{
	...
#ifdef CONFIG_ARTMEM
	pn->max_nr_base_pages = ULONG_MAX;
	pn->need_demotion = false;
#endif
}

补充cgroup的创建:当mkdir /sys/fs/cgroup/xxx会触发cgroup_mkdir, 会获得父group的信息cgroup_create创建新的struct cgroup; cgroup_apply_control_enable会启用cgroup中的各个子系统css_alloc;对于每个启用的子系统,会调用其函数来初始化对应的子系统状态对象比如mem_cgroup_css_alloc,在这个函数中就会看到:

memcg = mem_cgroup_alloc(parent); // 建立新的memcg

获取memcg

有以下几种获取memcg的方式,根据不同场景选用。

struct mm_struct *mm
struct mem_cgroup *memcg = get_mem_cgroup_from_mm(mm);

struct vm_area_struct *vma
struct mem_cgroup *memcg = get_mem_cgroup_from_mm(vma->vm_mm);

struct page *head = compound_head(page);
struct mem_cgroup *memcg = page_memcg(head);

收费时机

一般来说会在缺页中断时收费,收费过程:

  • try_charge_memcg尝试更新所有层级的 cgroup memory counter. page_counter_try_charge从当前 cgroup开始,向上遍历所有父cgroup尝试增加parent page_counter usage
  • 超出内存阈值:该层会返回失败,发送MEMCG_MAX更新memory.events -> 记录PSI,表示此时有内存压力 -> 尝试回收内存try_to_free_mem_cgroup_pages -> 回收后仍不足以满足当前分配请求,并且允许OOM(Out Of Memory)Kill,则会触发OOM Killer -> 如果OOM Kill也无法释放足够的内存,则最终返返回错误回到中断处理函数。

collapse_huge_page()

是内核中用于将多个4KB base pages合并成一个2MB Transparent Huge Page的核心函数。它是Khugepaged(Kernel Huge Page Daemon)后台线程的函数。

THP可以提升性能,但不能总是通过缺页时一次性分配huge page(像 do_huge_pmd_anonymous_page()那样)。很多情况下,应用程序已经有很多连续的4KB匿名页在使用了。这时候,Khugepaged会后台扫描内存,如果发现某一段满足合并条件,就会尝试调用collapse_huge_page()合并它们为一个compound page.

static int collapse_huge_page(struct mm_struct *mm,
				   unsigned long address,
				   struct page **hpage,
				   int node, int referenced, int unmapped)
{
	. . .
		mmap_read_unlock(mm);
#ifdef CONFIG_ARTMEM
	if (node_is_dram(node)) {
	    struct mem_cgroup *memcg = get_mem_cgroup_from_mm(mm);
	    unsigned long max_nr_pages, cur_nr_pages;
	    pg_data_t *pgdat;
	    
	    pgdat = NODE_DATA(node);
	    cur_nr_pages = get_nr_lru_pages_node(memcg, pgdat); // 每次都重新计算一下这个node的这个memcg有多少页面
	    max_nr_pages = memcg->nodeinfo[node]->max_nr_base_pages;

	    if (max_nr_pages == ULONG_MAX) // ULONG_MAX表示无限制(即允许尽可能多地分配内存)
		goto normal_exec;
	    else if (cur_nr_pages + HPAGE_PMD_NR <= max_nr_pages) // 如果再分配512个页后不会超过上限,则继续分配
		goto normal_exec;
	    else { // 不满足上面那两个条件
		result = SCAN_ALLOC_HUGE_PAGE_FAIL;
		goto out_nolock;
	    }
	}
normal_exec:
	new_page = khugepaged_alloc_page(hpage, gfp, node);
#else
	new_page = khugepaged_alloc_page(hpage, gfp, node);
#endif
	if (!new_page) {
		result = SCAN_ALLOC_HUGE_PAGE_FAIL;
		goto out_nolock;
	}

	if (unlikely(mem_cgroup_charge(new_page, mm, gfp))) { // 对当前task所属的memory cgroup进行收费
		result = SCAN_CGROUP_CHARGE_FAIL;
		goto out_nolock;
	}
	count_memcg_page_event(new_page, THP_COLLAPSE_ALLOC);

	mmap_read_lock(mm);
	. . .
}

unsigned long get_nr_lru_pages_node(struct mem_cgroup *memcg, pg_data_t *pgdat)
{
    struct lruvec *lruvec;
    unsigned long nr_pages = 0;
    enum lru_list lru;

    lruvec = mem_cgroup_lruvec(memcg, pgdat);

    for_each_lru(lru)
		nr_pages += lruvec_lru_size(lruvec, lru, MAX_NR_ZONES);
   
    return nr_pages;
}

collapse_huge_page() 大致做了以下步骤:

  • 检查: 确保VMA支持THP;地址必须2MB对齐,且整页区域映射完整;页表项都是有效的、未被锁定的PTE.
  • 分配:调用alloc_pages()分配一个order-9的compound page;调用alloc_pages()分配一个order-9的compound page.
  • 复制:将原有512个4KB页复制到new huge page中。
  • 更新页表:解除原来的512个PTE映射(unmap);设置一个新的PMD映射,指向新的huge page.
  • 建立反向映射(rmap):page_add_new_anon_rmap()添加反向映射;各项统计更新。

collapse_file()

对于memcg计数来说和上面同样的逻辑,不过是匿名页和文件页(page cache)的差别。

alloc_pages_vma()

alloc_pages_vma()为用户空间分配物理页的接口函数,这是执行alloc_pages()__alloc_pages()之前带上VMA和mempolicy的wrapper。主要是限制每个NUMA对DRAM内存的过多分配,如果DRAM分配不了,就去PMEM的node. 当然还有每次分配的memcg计数。

struct page *alloc_pages_vma(gfp_t gfp, int order, struct vm_area_struct *vma,
		unsigned long addr, int node, bool hugepage)
{
	struct mempolicy *pol;
	struct page *page;
	int preferred_nid;
	nodemask_t *nmask;

	pol = get_vma_policy(vma, addr); // 定义了当前内存区域使用的NUMA分配策略(如:interleave、preferred、bind 等)

	// 交错分配策略
	if (pol->mode == MPOL_INTERLEAVE) {
		unsigned nid;

		nid = interleave_nid(pol, vma, addr, PAGE_SHIFT + order);
		mpol_cond_put(pol);
		page = alloc_page_interleave(gfp, order, nid);
		goto out;
	}

#ifdef CONFIG_ARTMEM
	struct task_struct *p = current;
	struct mem_cgroup *memcg = mem_cgroup_from_task(p);
	unsigned long max_nr_pages;
	int nid = pol->mode == MPOL_PREFERRED ? first_node(pol->nodes) : node;
	int orig_nid = nid;
	unsigned int nr_pages = 1U << order;
	pg_data_t *pgdat = NODE_DATA(nid);

	max_nr_pages = READ_ONCE(memcg->nodeinfo[nid]->max_nr_base_pages);
	if (max_nr_pages == ULONG_MAX) //如果没有限制这个节点上允许使用的最大页数,跳过
		goto use_default_pol;

	if (max_nr_pages <= (get_nr_lru_pages_node(memcg, pgdat) + nr_pages)) {
		// DRAM放不下,需要放PM了
		nid = 2;
		max_nr_pages = READ_ONCE(memcg->nodeinfo[nid]->max_nr_base_pages);
		pgdat = NODE_DATA(nid);
	}
	    
	mpol_cond_put(pol);
	page = __alloc_pages_node(nid, gfp | __GFP_THISNODE, order);
	// 在当前确定的nid上强制(__GFP_THISNODE)分配页
	goto out;
use_default_pol:
#endif
	. . .
out:
	return page;
}
EXPORT_SYMBOL(alloc_pages_vma);

文章作者: 易百分
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 易百分 !
  目录