准备

内核版本: 5.0

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

使用方法

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

  • Centos安装cgroup:

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

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

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

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

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

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

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

    cgexec -g memory:test_memory ./a.out

cgroup内存子系统

内存资源控制器mem_cgroup

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

/*
 * 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:

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内核深度解析》