文章目录
-
重要:本系列文章内容摘自
<Linux内核深度解析>
基于ARM64架构的Linux4.x内核一书,作者余华兵。系列文章主要用于记录Linux内核的大部分机制及参数的总结说明
1 内存资源控制器
控制组(cgroup)的内存资源控制器用来控制一组进程的内存使用量,启用内存资源控制器的控制组简称内存控制组(memcg)。控制组把各种资源控制器称为子系统,内存资源控制器也称为内存子系统。
1.1 使用方法
编译内核时需要开启以下配置宏:
(1)控制组的配置宏CONFIG_CGROUPS。
(2)内存资源控制器的配置宏CONFIG_MEMCG。
可选的配置宏如下:
(1)内存资源控制器交换扩展(也称为交换控制器)的配置宏CONFIG_MEMCG_SWAP,控制进程使用的交换区的大小,依赖配置宏CONFIG_MEMCG和页交换的配置宏CONFIG_SWAP。
(2)配置宏CONFIG_MEMCG_SWAP_ENABLED控制是否默认开启交换控制器,默认开启,依赖配置宏CONFIG_MEMCG_SWAP。
可以在引导内核时通过内核参数"swapaccount="指定是否开启交换控制器,参数值为1表示开启,参数值为0表示关闭。
控制组已经从版本1(cgroup v1)演进到版本2(cgroup v2),主要的改进如下:
(1)版本1可以创建多个层级树,版本2只有一个统一的层级树。
(2)在版本2中,进程只能加入作为叶子节点的控制组(即没有子控制组),根控制组是个例外(进程默认属于根控制组)。
控制组版本1可以创建多个控制组层级树,但是每种资源控制器只能关联一个控制组层级树,内存资源控制器只能关联一个控制组层级树。
控制组版本1和版本2的内存资源控制器是互斥的:如果使用了控制组版本1的内存资源控制器,就不能使用控制组版本2的内存资源控制器;同样,如果使用了控制组版本2的内存资源控制器,就不能使用控制组版本1的内存资源控制器。
1.控制组版本1的内存资源控制器
控制组版本1的内存资源控制器提供的主要接口文件如下:
(1)memory.use_hierarchy:启用分层记账,默认禁止。内存控制组启用分层记账以后,子树中的所有内存控制组的内存使用都会被记账到这个内存控制组。
(2)memory.limit_in_bytes:设置或查看内存使用的限制(硬限制),默认值是“max”。
(3)memory.soft_limit_in_bytes:设置或查看内存使用的软限制,默认值是“max”。软限制和硬限制的区别是:内存使用量可以超过软限制,但是不能超过硬限制,页回收算法会优先从内存使用量超过软限制的内存控制组回收内存。
(4)memory.memsw.limit_in_bytes:设置或查看内存+交换区的使用限制,默认值是“max”。
(5)memory.swappiness:设置或查看交换积极程度。
(6)memory.oom_control:控制是否禁止内存耗尽杀手,1表示禁止,0表示启用,默认启用内存耗尽杀手。
(7)memory.stat:查看内存使用的各种统计值。
(8)memory.usage_in_bytes:查看当前内存使用量。
(9)memory.memsw.usage_in_bytes:查看当前内存+交换区的使用量。
(10)memory.max_usage_in_bytes:查看记录的最大内存使用量。
(11)memory.memsw.max_usage_in_bytes:查看记录的最大内存+交换区使用量。
(12)memory.failcnt:查看内存使用量命中限制的次数。
(13)memory.memsw.failcnt:查看内存+交换区的使用量命中限制的次数。
(14)memory.kmem.limit_in_bytes:设置或查看内核内存的使用限制。
(15)memory.kmem.usage_in_bytes:查看当前内核内存使用量。
(16)memory.kmem.failcnt:查看内核内存使用量命中限制的次数。
(17)memory.kmem.max_usage_in_bytes:查看记录的最大内核内存使用量。
(18)memory.kmem.tcp.limit_in_bytes:设置或查看TCP缓冲区的内存使用限制。
(19)memory.kmem.tcp.usage_in_bytes:查看当前TCP缓冲区的内存使用量。
(20)memory.kmem.tcp.failcnt:查看TCP缓冲区内存使用量命中限制的次数。
(21)memory.kmem.tcp.max_usage_in_bytes:查看记录的最大TCP缓冲区内存使用量。
根控制组对资源使用量没有限制,并且不允许在根控制组配置资源使用限制,进程默认属于根控制组。创建子进程的时候,子进程继承父进程加入的控制组。
控制组版本1的内存资源控制器的配置方法如下:
(1)在目录“/sys/fs/cgroup”下挂载tmpfs文件系统。
mount -t tmpfs none /sys/fs/cgroup
(2)在目录“/sys/fs/cgroup”下创建目录“memory”。
mkdir /sys/fs/cgroup/memory
(3)在目录“/sys/fs/cgroup/memory”下挂载cgroup文件系统,把内存资源控制器关联到控制组层级树。
mount -t cgroup -o memory none /sys/fs/cgroup/memory
(4)创建新的控制组。
mkdir /sys/fs/cgroup/memory/memcg0
(5)设置内存使用的限制。例如把控制组的内存使用限制设置为4MB:
echo 4M > /sys/fs/cgroup/memory/memcg0/memory.limit_in_bytes
(6)把线程加入控制组。
echo <pid> > /sys/fs/cgroup/memory/memcg0/tasks
(7)也可以把线程组加入控制组,指定线程组中任意一个线程的标识符,就会把线程组的所有线程加入任务组。
echo <pid> > /sys/fs/cgroup/memory/memcg0/cgroup.procs
2.控制组版本2的内存资源控制器
控制组版本2的内存资源控制器提供的主要接口文件如下:
(1)memory.low:内存使用低界限,默认值是 0。用来保护一个控制组可以分配到指定数量的内存,这种保护只能尽力而为,没有绝对的保证。如果一个控制组和所有祖先的内存使用量在低界限以下,并且可以从其他不受保护的控制组回收内存,那么这个控制组的内存不会被回收。
(2)memory.high:内存使用高界限,内存使用节流(throttle)限制,默认值是“max”。这是控制内存使用的主要机制。如果一个控制组的内存使用量超过高界限,那么这个控制组里面的所有进程将会被节流,从这个控制组回收内存。
(3)memory.max:内存使用硬限制,默认值是“max”。如果一个控制组的内存使用量达到硬限制,将会在这个控制组中调用内存耗尽杀手选择进程杀死。
(4)memory.current:查看控制组和所有子孙的当前内存使用量。
(5)memory.stat:查看内存使用的各种统计值。
(6)memory.swap.max:交换区使用硬限制,默认值是“max”。如果一个控制组的交换区使用量达到硬限制,那么不会换出这个控制组的匿名页。
(7)memory.swap.current:查看控制组和所有子孙的当前交换区使用量。
根控制组对资源使用量没有限制,并且不允许在根控制组配置资源使用限制,进程默认属于根控制组。创建子进程的时候,子进程继承父进程加入的控制组。
控制组版本1和版本2的内存资源控制器的区别如下:
(1)控制组版本1的内存资源控制器默认禁止分层记账方式,可以配置;控制组版本2的内存资源控制器总是使用分层记账方式,不可配置。
(2)对交换区的记账方式不同:控制组版本1使用内存+交换区记账方式,即记录内存使用量和交换区使用量的总和;控制组版本2对交换区单独记账。
(3)控制组版本1的内存资源控制器默认启用内存耗尽杀手,可以配置;控制组版本2的内存资源控制器总是启用内存耗尽杀手,不可配置。
控制组版本2的内存资源控制器的配置方法如下:
(1)在目录“/sys/fs/cgroup”下挂载tmpfs文件系统。
mount -t tmpfs cgroup_root /sys/fs/cgroup
(2)在目录“/sys/fs/cgroup”下挂载cgroup2文件系统。
mount -t cgroup2 none /sys/fs/cgroup
(3)在根控制组开启内存资源控制器。
cd /sys/fs/cgroup
echo "+memory" > cgroup.subtree_control
(4)创建新的控制组。
mkdir cgroup0
(5)设置控制组的内存使用高界限。
echo 4M > cgroup0/memory.high
(6)把线程组加入控制组。
echo <pid> > cgroup0/cgroup.procs
1.2 技术原理
(1)内存控制组。
内存资源控制器的数据结构是结构体mem_cgroup,如下所示,主要成员如下:
- 成员css:结构体cgroup_subsys_state是所有资源控制器的基类,结构体mem_cgroup是它的一个派生类。
- 成员memory:内存计数器,记录内存的限制和当前使用量。
- 成员swap:(控制组版本2的)交换区计数器,记录交换区的限制和当前使用量。
- 成员memsw:(控制组版本1的)内存+交换区计数器,记录内存+交换区的限制和当前使用量。
- 成员kmem:(控制组版本1的)内核内存计数器,记录内核内存的限制和当前使用量。
- 成员tcpmem:(控制组版本1的)TCP套接字缓冲区计数器,记录TCP套接字缓冲区的限制和当前使用量。
- 成员low:(控制组版本2的)内存使用低界限。
- 成员high:(控制组版本2的)内存使用高界限。
- 成员soft_limit:(控制组版本1的)内存使用的软限制。
- 成员use_hierarchy:控制是否启用分层记账。
- 成员swappiness:控制交换的积极程度。
- 成员oom_kill_disable:控制是否禁止内存耗尽杀手。
- 成员nodeinfo:每个内存节点对应一个mem_cgroup_per_node实例,存放内存控制组在每个内存节点上的信息。
结构体page_counter是页计数器,计数的单位是页,其代码如下:
include/linux/page_counter.h
struct page_counter {
atomic_long_t count;
unsigned long limit;
struct page_counter *parent;
/* 历史遗留成员 */
unsigned long watermark;
unsigned long failcnt;
}
1)成员count是计数值。
2)成员limit是硬限制。
3)成员parent:如果父内存控制组启用分层记账,那么成员parent指向父内存控制组的页计数器;如果父内存控制组禁止分层记账,那么成员parent是空指针。
4)成员watermark记录计数值的历史最大值。
5)成员failcnt是命中限制的次数。
结构体mem_cgroup_per_node存放内存控制组在每个内存节点上的信息,如下所示,主要成员如下:
1)成员lruvec是LRU向量,其中的LRU链表是内存控制组的私有LRU链表。当进程加入内存控制组以后,给进程分配的页不再加入内存节点的LRU链表,而是加入内存控制组的私有LRU链表。
2)成员usage_in_excess是内存使用量超过软限制的数值。如果内存使用量超过软限制,成员usage_in_excess等于(mem_cgroup.memory.count – mem_cgroup.soft_limit),否则成员usage_in_excess等于0。
3)成员on_tree指示内存控制组是否在软限制树中。当内存使用量超过软限制的时候,借助成员tree_node把mem_cgroup_per_node实例加入内存节点的软限制树,软限制树是红黑树,根据内存使用量超过软限制的数值从小到大排序,树的根是soft_limit_tree.rb_tree_per_node[n]->rb_root,其中n是内存节点编号。
4)成员memcg指向mem_cgroup_per_node实例所属的内存控制组。
进程怎么知道它属于哪个内存控制组?如下所示,给定一个进程,得到进程所属的内存控制组的方法如下:
1)根据进程描述符的成员cgroups得到结构体css_set,结构体css_set是控制组子系统状态的集合。
2)根据css_set.subsys[memory_cgrp_id]得到内存控制组的第一个成员css的地址。结构体css_set的成员subsys指向每种资源控制器的结构体cgroup_subsys_state,其中索引为memory_cgrp_id(枚举常量)的数组元素指向内存控制组的第一个成员css。
3)如果css_set.subsys[memory_cgrp_id]是空指针,说明进程没有加入内存控制组,默认属于根内存控制组,全局变量root_mem_cgroup指向根内存控制组。
4)如果css_set.subsys[memory_cgrp_id]不是空指针,把地址减去结构体mem_cgroup中成员css的偏移,就是内存控制组的地址。
内存描述符怎么知道它属于哪个内存控制组?内存描述符的成员owner指向进程描述符。如果进程属于线程组,那么成员owner指向线程组组长的进程描述符:
include/linux/mm_types.h
struct mm_struct {
…
#ifdef CONFIG_MEMCG
struct task_struct __rcu *owner;
#endif
…
};
首先根据成员owner得到进程描述符,然后可以得到进程所属的内存控制组。
怎么知道物理页属于哪个内存控制组?如下所示,如果将进程加入内存控制组,给进程分配的物理页的页描述符的成员mem_cgroup指向内存控制组:
(2)交换槽位到内存控制组的映射。
把一个页换出到交换区的时候,需要把内存控制组的交换区使用量加 1,把内存使用量减 1,并且保存交换槽位到页所属的内存控制组的映射关系。
访问页的时候,把页从交换区换入交换缓存,需要把内存控制组的内存使用量加 1。释放交换槽位的时候,需要把内存控制组的交换区使用量减 1,必须使用页在换出时所属的内存控制组,不能使用进程所属的内存控制组。
为什么换入页时要使用换出时页所属的内存控制组?
假设进程1和进程2共享一个交换支持的页,把进程1加入内存控制组cg1,进程2属于内存控制组cg2。假设物理页是由进程1申请分配的,所以页属于内存控制组cg1。
把页换出到交换区的时候,把内存控制组cg1的交换区使用量加 1,把内存使用量减1。
假设在换出页以后进程2先访问页,把页从交换区换入。如果使用进程2所属的内存控制组,那么页属于内存控制组cg2,把内存控制组cg2的内存使用量加 1。释放交换槽位的时候,把内存控制组cg2的交换区使用量减1,这就出现问题了:“换出页时把内存控制组cg1的交换区使用量加 1,换入页以后,释放交换槽位的时候,把内存控制组cg2的交换区使用量减1。”
所以在换入页时要使用换出时页所属的内存控制组,换出时需要保存交换槽位到页所属的内存控制组的映射关系。
如下所示,内核定义了数组swap_cgroup_ctrl,每个数组项指向一个交换区的swap_cgroup_ctrl实例,交换区的每个槽位对应一个swap_cgroup实例:
swap_cgroup_ctrl实例的主要成员如下:
1)成员map指向页描述符指针数组,每个数组项指向交换槽位的swap_cgroup实例所在的物理页的页描述符。
2)成员length是页描述符指针数组的大小,也就是一个交换区需要多少个物理页来存放所有槽位的swap_cgroup实例,等于(交换区的总页数 / SC_PER_PAGE),宏SC_PER_PAGE是一个物理页可以容纳的swap_cgroup实例数量。
swap_cgroup实例的成员id是换出页所属的内存控制组的标识符,其代码如下:
mm/swap_cgroup.c
struct swap_cgroup {
unsigned short id;
};
获取交换槽位对应的内存控制组的方法如下:
1)根据交换区索引得到swap_cgroup_ctrl实例。
2)mappage = ctrl->map[offset / SC_PER_PAGE],得到交换槽位的swap_cgroup实例所在的物理页的页描述符,其中offset是交换区的偏移。
3)sc = page_address(mappage),得到交换槽位的swap_cgroup实例所在的物理页的内核虚拟地址。
4)交换槽位的swap_cgroup实例的地址等于(sc + offset % SC_PER_PAGE)。
5)从交换槽位的swap_cgroup实例的成员id得到内存控制组的标识符。
6)在全局的标识符到内存控制组的映射mem_cgroup_idr中根据标识符得到mem_cgroup实例。
2.分层记账
在一个内存控制组启用分层记账以后,子树中的所有内存控制组的内存使用都会被记账到这个内存控制组。
在一个内存控制组启用或禁止分层记账的时候,要求它没有子树。如果一个内存控制组启用了分层记账,创建子内存控制组的时候,子内存控制组自动启用分层记账,并且不允许管理员禁止分层记账。
版本1的内存控制组默认禁止分层记账,可以配置;版本2的内存控制组总是启用分层记账,不可配置。
如下所示,假设有3个内存控制组:a、b和c。b是a的孩子,c是b的孩子,a禁止分层记账,b和c启用分层记账,那么c的内存使用会被记账到所有启用分层记账的祖先,即会被记账到b,但是不会被记账到a。以内存控制组中的内存计数器为例,页计数器之间的关系如下所示:
(1)b的内存计数器的成员parent是空指针。
(2)c的内存计数器的成员parent指向b的内存计数器。
如果内存控制组c申请分配了一个物理页,那么c和b的内存计数器都会加 1,a的内存计数器不会加 1。
3.内存记账
内存记账(charge)是指分配物理页的时候记录内存控制组的内存使用量,主要在以下4种情况下执行:
(1)第一次访问匿名页时分配物理页。
(2)访问文件时分配物理页。
(3)执行写时复制,分配物理页。
(4)从交换区换入页。
如下所示,第一次访问匿名页时生成页错误异常,函数do_anonymous_page负责处理匿名页的页错误异常,主要步骤如下:
(1)分配物理页。
(2)调用函数mem_cgroup_try_charge以记账。
(3)锁住页表。
(4)如果直接页表项是空表项,那么调用函数mem_cgroup_commit_charge以提交记账,然后设置页表项。
(5)如果直接页表项不是空表项,说明其他处理器已经分配并映射到物理页,那么当前处理器放弃处理,调用函数mem_cgroup_cancel_charge以放弃记账。
(6)释放页表锁。
如下所示,访问文件的某一页时,如果文件页不在内存中,那么把文件页读到内存中。函数add_to_page_cache_lru负责把文件页添加到文件的页缓存中,主要步骤如下:
(1)调用函数mem_cgroup_try_charge以记账。
(2)把页添加到文件的页缓存中。
(3)如果添加成功,调用函数mem_cgroup_commit_charge以提交记账。
(4)如果添加失败,调用函数mem_cgroup_cancel_charge以放弃记账。
如下所示,写只读页时生成页错误异常。函数do_wp_page负责执行写时复制,主要步骤如下:
(1)分配物理页。
(2)调用函数mem_cgroup_try_charge以记账。
(3)锁住页表。
(4)如果直接页表项和锁住页表之前相同,那么调用函数mem_cgroup_commit_charge以提交记账,然后设置页表项。
(5)如果直接页表项和锁住页表之前不同,说明其他处理器已经执行写时复制,那么当前处理器放弃处理,调用函数mem_cgroup_cancel_charge以放弃记账。
(6)释放页表锁。
如下,访问已换出到交换区的页时生成页错误异常,函数do_swap_page负责从交换区换入页,主要步骤如下:
(1)在交换缓存中查找页。
(2)如果页不在交换缓存中,那么从交换区换入页。
(3)调用函数mem_cgroup_try_charge以记账。
(4)锁住页表。
(5)如果直接页表项和锁住页表之前相同,那么设置页表项,然后调用函数mem_cgroup_commit_charge以提交记账。
(6)如果直接页表项和锁住页表之前不同,说明其他处理器已经换入页,那么当前处理器放弃处理,调用函数mem_cgroup_cancel_charge以放弃记账。
(7)释放页表锁。
可以看出,内存记账分为两步:
(1)调用函数mem_cgroup_try_charge以记账,把内存控制组的内存计数加上准备记账的页数。
(2)如果成功,调用函数mem_cgroup_commit_charge以提交记账;如果失败,调用函数mem_cgroup_cancel_charge以放弃记账。
为了减少处理器之间的竞争,提高内存记账的速度,为每个处理器定义一个记账缓存,其代码如下:
mm/memcontrol.c
#define CHARGE_BATCH 32U
struct memcg_stock_pcp {
struct mem_cgroup *cached;
unsigned int nr_pages;
struct work_struct work;
unsigned long flags;
#define FLUSHING_CACHED_CHARGE 0
};
static DEFINE_PER_CPU(struct memcg_stock_pcp, memcg_stock);
每处理器记账缓存一次从内存控制组批量申请32页,把内存控制组的内存使用量加上32页。结构体memcg_stock_pcp的成员cached记录内存控制组,成员nr_pages是预留页数。
在内存控制组记账的时候,先查看当前处理器的记账缓存。如果记账缓存保存的内存控制组正是准备记账的内存控制组,并且预留页数大于或等于准备记账的页数,那么把预留页数减去准备记账的页数。
(1)函数mem_cgroup_try_charge。
函数mem_cgroup_try_charge尝试记账一个页,检查内存控制组的内存使用量是否超过限制。如果没有超过限制,就把页记账到内存控制组。该函数有5个参数:
- struct page *page:准备记账的页。
- struct mm_struct *mm:指向申请物理页的进程的内存描述符。
- gfp_t gfp_mask:申请分配物理页的掩码。
- struct mem_cgroup **memcgp:输出参数,返回记账的内存控制组。
- bool compound:指示复合页按一页记账还是按单页数量记账。
返回值:如果内存控制组的内存使用量没有超过限制,那么该函数返回0,参数memcgp返回记账的内存控制组,否则返回错误码。
4.撤销内存记账
如下所示,调用函数put_page释放页的时候,把页的引用计数减1,只有页的引用计数变成0,才会真正释放页,调用函数mem_cgroup_uncharge以撤销内存记账:
5.交换区记账
交换区记账是指把页换出到交换区的时候记录内存控制组的交换区使用量。
如下所示,函数shrink_page_list把匿名页换出到交换区,主要步骤如下:
(1)调用函数add_to_swap,把页添加到交换缓存中,处理如下。
- 从交换区分配槽位。
- 调用函数mem_cgroup_try_charge_swap以执行交换区记账。
- 把页添加到交换缓存中。
(2)从页表中删除映射。
(3)把页写到交换区。
(4)调用函数__remove_mapping,处理如下:
调用函数mem_cgroup_swapout,针对内存+交换区记账方式撤销内存记账。
把页从交换缓存中删除。
(5)释放物理页。
1)函数mem_cgroup_try_charge_swap。
函数mem_cgroup_try_charge_swap负责执行交换区记账,有两个参数。
- struct page *page:换出的页。
- swp_entry_t entry:交换项,包含交换区的索引和偏移。
返回值:如果内存控制组的交换区使用量没有超过限制,返回0。
2)函数mem_cgroup_swapout。
函数mem_cgroup_swapout负责针对内存+交换区记账方式撤销内存记账。如果没有启用内存+交换区记账,只使用内存记账,可以在释放页的时候撤销记账。如果启用了内存+交换区记账,把一页换出到交换区,内存使用量减1,交换区使用量加 1,内存+交换区的使用量不变;如果等到释放页的时候撤销记账,则会把内存使用量和内存+交换区使用量都减1,这种做法显然是错误的,所以需要在释放页之前做特殊处理。
6.撤销交换区记账
如下所示,释放交换区槽位的时候撤销交换区记账。函数mem_cgroup_uncharge_swap负责撤销交换区记账,其代码如下:
7.版本1的内存使用限制
版本1的内存控制组支持两个限制:
(1)软限制(soft limit):内存使用量可以超过软限制,页回收算法会优先从内存使用量超过软限制的内存控制组回收内存。
(2)硬限制(hard limit):内存使用量不可以超过硬限制。如果超过硬限制,处理办法是:如果启用内存耗尽杀手,那么使用内存耗尽杀手从内存控制组选择进程杀死;如果禁止内存耗尽杀手,那么进程睡眠,直到内存使用量小于硬限制。
如下所示,提交内存记账的时候,检查内存使用量是否超过软限制。为了避免频繁检查,对检查软限制进行限速:每当记账和撤销记账1024页时检查一次软限制。函数mem_cgroup_update_tree检查内存控制组和所有启用分层记账的祖先,如果内存控制组的内存使用量超过软限制,那么把内存控制组添加到当前记账的物理页所属的内存节点的软限制树,软限制树按内存使用量和软限制的差值从小到大排序:
如下所示,页分配器在内存严重不足的时候直接回收页。如果是全局回收(即不是从指定的内存控制组回收内存),那么优先从内存使用量超过软限制的内存控制组回收内存:
8.版本2的内存使用限制
版本2的内存控制组支持3个限制:
- 低界限(low):用来保护一个控制组可以分配到指定数量的内存,这种保护只能尽力而为,没有绝对的保证。如果一个控制组和所有祖先的内存使用量在低界限以下,并且可以从其他不受保护的控制组回收内存,那么这个控制组的内存不会被回收。
- 高界限(high):这是控制内存使用的主要机制。如果一个控制组的内存使用量超过高界限,那么这个控制组里面的所有进程将会被节流,从这个控制组回收内存。
- 硬限制(limit):内存使用量不可以超过硬限制。如果超过硬限制,使用内存耗尽杀手从内存控制组选择进程杀死。
(1)低界限。
如下所示,直接回收页分为两轮:
1)第一轮把sc->memcg_low_reclaim设置成0,不允许从内存使用量小于低界限的内存控制组回收页。
函数shrink_node遍历内存控制组层级树,如果一个内存控制组和所有祖先的内存使用量小于低界限,并且不允许从内存使用量小于低界限的内存控制组回收页,那么跳过这个内存控制组。
2)如果第一轮跳过了内存使用量小于低界限的内存控制组,那么把sc->memcg_low_reclaim设置成1,允许从内存使用量小于低界限的内存控制组回收页,调用函数shrink_zones执行第二轮回收。
(2)高界限。
如下所示,执行内存记账的时候,检查内存控制组和所有祖先的内存使用量是否超过高界限。如果其中一个内存控制组超过高界限,那么延后执行从内存控制组回收页,具体实现如下:
1)如果在中断(包括硬中断和软中断)上下文中,那么向工作队列中添加 1个工作项。
2)如果在进程上下文中,那么进程描述符的成员memcg_nr_pages_over_high记录超过高界限的页数,然后给进程的thread_info.flags设置标志_TIF_NOTIFY_RESUME,等进程准备从内核模式返回用户模式的时候处理。
如下所示,进程准备从内核模式返回用户模式的时候,发现进程的thread_info.flags设置了标志_TIF_NOTIFY_RESUME,就会调用函数mem_cgroup_handle_over_high,从当前进程所属的内存控制组回收超过高界限的页:
检查当前进程所属的内存控制组和所有祖先,如果内存控制组的内存使用量超过高界限,就调用函数try_to_free_mem_cgroup_pages,从内存控制组回收超过高界限的页。
在中断上下文中添加的工作项的处理函数是high_work_func,该函数也把主要工作委托给函数reclaim_high。
9.硬限制
当内存控制组的内存使用量超过硬限制的时候,首先尝试从内存控制组直接回收页。如果回收页失败,采取下面的处理措施:
(1)如果申请分配页的时候指定了分配标志__GFP_NOFAIL,不允许分配失败,那么强行突破硬限制。
(2)版本1的内存控制组支持启用或禁止内存耗尽杀手。如果启用内存耗尽杀手,那么使用内存耗尽杀手从内存控制组选择进程杀死;如果禁止内存耗尽杀手,那么进程睡眠,直到内存使用量小于硬限制。
(3)版本2的内存控制组总是启用内存耗尽杀手,使用内存耗尽杀手从内存控制组选择进程杀死。
注意内存控制组内存耗尽和系统内存耗尽不同:内存控制组内存耗尽是因为内存控制组的内存使用量超过硬限制,系统内存耗尽是因为内存严重不足。
在页错误异常处理程序中,进程申请分配物理页。如果进程所属的内存控制组的内存使用量超过硬限制,处理流程如下所示: