准备

内核版本: 5.0

cgroup全称Control Groups,顾名思义就是把进程放到一个组里面统一加以控制,cgroup可以限制进程的各种资源,包括用来控制一组进程的内存使用量,cgroup把各种资源控制器成为子系统,内存控制即为内存子系统.

使用方法

cgroup目前现存两个版本,我们仅讨论cgroup v1的使用方法.

  • Centos安装cgroup:

    1
    sudo yum -y install libcgroup-tools
  • 在目录`/sys/fs/cgroup”下挂在tmpfs文件系统

    1
    mount -t tmpfs none /sys/fs/cgroup
  • 在目录/sys/fs/cgroup下创建目录memory

    1
    mkdir /sys/fs/cgroup/memory
  • 在目录sys/fs/cgroup/memory下挂载cgroup文件系统, 把内存资源控制器关联到控制组

    1
    mount -t cgroup -o memory none /sys/fs/cgroup/memory
  • 创建新的控制组

    1
    mkdir /sys/fs/cgroup/memory/test_memory
  • 设置控制组的内存使用限制2G:

    1
    sudo echo 2147483648 > /sys/fs/cgroup/memory/test_memory/memory.limit_in_bytes
  • 将线程组加入控制组:

    1
    sudo echo <pid> > /sys/fs/cgroup/memory/test_memory/cgroup.procs
  • 或者启动进程附带控制组:

    1
    cgexec -g memory:test_memory ./a.out

cgroup内存子系统

内存资源控制器mem_cgroup

cgroup的内存资源控制器限制每一个控制组的Page CacheRSS物理内存.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/*
* The memory controller data structure. The memory controller controls both
* page cache and RSS per cgroup. We would eventually like to provide
* statistics based on the statistics developed by Rik Van Riel for clock-pro,
* to help the administrator determine what knobs to tune.
*/
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; /* 内存计数器 */
struct page_counter swap; /* 交换区计数器 */

/* Legacy consumer-oriented counters */
struct page_counter memsw;
struct page_counter kmem; /* 内核内存限制计数器 */
struct page_counter tcpmem; /* TCP的socket缓冲区计数器 */

/* Upper bound of normal memory consumption range */
unsigned long high; /* 限制使用的内存上限 */

/* Range enforcement for interrupt charges */
struct work_struct high_work;

unsigned long soft_limit;

/* vmpressure notifications */
struct vmpressure vmpressure;

/*
* Should the accounting and control be hierarchical, per subtree?
*/
bool use_hierarchy; /* 是否启用分层计数 */

/*
* Should the OOM killer kill all belonging tasks, had it kill one?
*/
bool oom_group;

/* protected by memcg_oom_lock */
bool oom_lock;
int under_oom;

int swappiness;
/* OOM-Killer disable */
int oom_kill_disable; /* 是否打开OOM的killer, 即kill超出限制的内存容量的进程 */

/* ... */
};

结构体page_counter是页计数器,单位为Page:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct page_counter {
atomic_long_t usage; /* 已使用的Page数量 */
unsigned long min;
unsigned long low;
unsigned long max;
struct page_counter *parent;

/* effective memory.min and memory.min usage tracking */
unsigned long emin;
atomic_long_t min_usage;
atomic_long_t children_min_usage;

/* effective memory.low and memory.low usage tracking */
unsigned long elow;
atomic_long_t low_usage;
atomic_long_t children_low_usage;

/* legacy */
unsigned long watermark;
unsigned long failcnt;
};

理解cgroup内存记账

当为内存控制组中的进程分配物理内存时,会记录内存使用量, 内存记账简单的理解为记录控制组的内存使用量, 以下是记录的时间点:

  • 第一次访问匿名页时分配物理页.
  • 访问文件时分配物理页(Page Cache) .
  • 执行COW(写时复制)时,分配物理页.
  • 从交换区换入页.

我们仅分析4类中较为常见的访问文件分配物理页的内存记账处理过程:

我们以ext4文件系统为例, 当需要读取文件时,某个Page不在内存中,需要把该Page读取至内存中,即调用address_space的操作函数ext4_readpage

cgroup_page_cache

内存计数步骤:

  • mem_cgroup_try_charge()用来表示尝试记账, 把内存控制组的内存计数加上指定的数量.

  • 如果成功,调用mem_cgroup_commit_charge()以提交计数,否则调用mem_cgroup_cancel_charge()放弃计数.

OOM killer

cgroup的内存控制器是默认开启OOM killer,当进程消耗的内存超过了cgroup的限制,就会调用OOM killer,向指定的进程发送杀死信号SIGKILL. 假如触发了OOM,关于crgroup内存控制kill的信息,可以通过dmesg进行查看.

dmesg示例

新建了一个名为test_cgroup的控制组, 使用进程a.out申请超过cgroup限制大小的内存:

dmesg

Q&A

  • 使用cgroup遇到cgroup change of group failed问题:

    注意/sys/fs/cgroup/test_memory即控制组的目录权限, 是否与执行进程的文件权限保持一致.

  • 即使进程以及完全退出,cgroup的内存控制组目录仍然无法清理?

    因为cgroup会限制进程使用Page Cache,而Page Cache的清理不会随着进程的退出而完成,所以当我们使用cgroup限制的进程有文件读写操作从而使用了Page Cache, 会导致cgroup内存控制组目录无法清理,所以正确的做法是清空Page Cache: echo 3 > /proc/sys/vm/drop_caches 或者echo 3 > /cgroup/memory/test_memory/memory.drop_caches

参考

《Linux内核深度解析》