关于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);