一次cfs组调度不公平引起的负载不均衡分析及cfs组调度深入探索(一)

文章目录

    • 1 cfs 组调度概念
    • 2 cfs cgroup 引起的调度不公平相关描述
    • 3 复现测试用例及现象展示
      • 3.1 patch-1 中涉及的虚拟运行时间计算
      • 3.2 patch-1 问题复现

1 cfs 组调度概念

cfs group 缘起于 docker 的资源任务限制。

现在的计算机基本都支持多用户登陆。如果一台计算机被两个用户A和B使用。假设用户A运行9个进程,用户B只运行1个进程。按照之前文章对CFS调度器的讲解,我们认为用户A获得90% CPU时间,用户B只获得10% CPU时间。随着用户A不停的增加运行进程,用户B可使用的CPU时间越来越少。这显然是不公平的。因此,我们引入组调度(Group Scheduling )的概念。我们以用户组作为调度的单位,这样用户A和用户B各获得50% CPU时间。用户A中的每个进程分别获得5.5%(50%/9)CPU时间。而用户B的进程获取50% CPU时间。这也符合我们的预期。-- 蜗窝科技 http://www.wowotech.net/process_management/449.html

如果需要完全理解本系列 cfs group 的分析内容,需要对 cfs group 具有一些了解和知道如何使用 cgroup。所以这里假设读者已经了解并掌握了一定 cfs 和 cgroup 相关理论知识。

2 cfs cgroup 引起的调度不公平相关描述

首先该问题只针对 5.19 以下内核才会触发,5.19 内核后修复了该问题,不过对该问题的分析,可以深入感受到 cfs group 的核心运作机制。
问题描述:

当服务器使用 cgroup 时,如果在一个环境里某个 cgroup 节点中的任务在运行一段时间后退出,并且后续没有新任务在该 cgroup 节点中运行,那么可能会导致该层级 cgroup 节点向 parent cgroup 节点贡献的部分负载无法正确移除,引发在后续计算任务组 se(调度实体) 权重时,parent cgroup 节点任务组的 load_avg(平均负载) 偏大引起该任务组每个 cpu 对应的 se 的权重比实际权重更小, 那么该任务组下的所有 task (包括 child cgroup)实际上能够分得的 cpu 时间资源更少,而相对于该任务组同级的伙伴任务组的 cpu 时间资源相对的会增加。该 bug 会导致在 cpu 满负载时该 parent cgroup 节点以及伙伴 cgroup 节点之间的 cfs 公平性出现不公平现象,具体不公平的严重情况要根据实际 cgroup 向 parent cgroup 贡献负载的多少决定。

3 复现测试用例及现象展示

2 中描述的问题在实际测试中有两种复现方式以及 2 种展示方式,以及对应 2 个内核社区 patch,patch 链接如下:
patch-1: sched/fair: Fix unfairness caused by missing load decay
patch-2: sched/fair: Make sure to update tg contrib for blocked load

3.1 patch-1 中涉及的虚拟运行时间计算

cfs 任务通过权重来管理自己能获取到的实际运行时间,有如下公式:
分配给进程的时间 = 总的 cpu 时间 * 进程的权重/就绪队列(runqueue)所有进程权重之和
权重与 cpu 分配之间的关系有:

  1. 每降低一个 nice 值,将多获取 10% cpu 的时间。
  2. 1024 是 nice = 0 (NICE_0_LOAD)的权重,此时虚拟运行时间等于实际运行时间。

根据上述规则,权重计算方式有:weight = 1024 / 1.25^nice,保证每降低一个 nice 值,任务多获取 10% cpu 时间。
内核避免浮点运算因此权重与时间片的计算引入中间值以此快速进行计算,权重与中间值分别保存在sched_prio_to_weightsched_prio_to_wmult数组中。
如下结构体用于保存权重及权重对应的中间值(inv_weight):

struct load_weight {
	unsigned long		weight;	// 保存对应权重。
	u32			inv_weight;	// 保存对应权重的中间值。
}; 

使用该权重的结构体有:

  1. struct sched_entity 调度实体有自己的权重
  2. struct rq 在 linux3.8 版本以前负载权重记录在这里,并以此进行负载均衡,加入 pelt 后负载权重被细分到调度实体上。
  3. struct cfs_rq 启用组调度后,cfs_rq 就绪队列也有自己的权重,用于参与任务组中任务的时间片计算。

通过权重可以为每个调度实体分配一个虚拟运行时间,用于 cfs 调度器选取任务运行,其原理是通过权重计算可以使得不同权重的任务使用不同的运行时间计算得到相同的虚拟时间,权重越大的任务,虚拟运行时间流逝更慢,而权重低的任务虚拟运行时间增加得更快,因此 cfs 调度器只需要保证运行期间每个任务的虚拟运行时间尽可能相等即可, 那么调度器就可以一直0选择虚拟运行时间最小的任务来运行,任务通过当前的虚拟运行时间大小插入 cfs_rq 的红黑树中。虚拟运行时间最小的任务将位于左叶子末上,cfs 每次从这里取走需要运行的任务,并在下一个触发时机(ticks,preempt等)更新任务已经使用的虚拟运行时间,并根据当前的 vruntime 重新插入红黑树,虚拟运行时间计算在 __calc_delta 函数中:

static u64 __calc_delta(u64 delta_exec, unsigned long weight, struct load_weight *lw)
{
	u64 fact = scale_load_down(weight);
	int shift = WMULT_SHIFT;

	__update_inv_weight(lw);

	if (unlikely(fact >> 32)) {
		while (fact >> 32) {
			fact >>= 1;
			shift--;
		}
	}

	/* hint to use a 32x32->64 mul */
	fact = (u64)(u32)fact * lw->inv_weight;

	while (fact >> 32) {
		fact >>= 1;
		shift--;
	}

	return mul_u64_u32_shr(delta_exec, fact, shift);
}

calc_delta_fair函数调用__calc_delta得到 vruntime 并将其累加到对应任务的 vruntime 中:

static inline u64 calc_delta_fair(u64 delta, struct sched_entity *se)
{
	if (unlikely(se->load.weight != NICE_0_LOAD)) // 权重等于 nice=0 时不用计算,虚拟时间等于实际运行时间。
		delta = __calc_delta(delta, NICE_0_LOAD, &se->load);

	return delta;
}

static void update_curr(struct cfs_rq *cfs_rq)
{
    ...
    ...
	curr->vruntime += calc_delta_fair(delta_exec, curr);	// 将当前任务的当前运行的时间片计算为 vruntime 并累加到 curr->vruntime 中。 
}

sched_slice 也会调用 __calc_delta 来计算一个任务在一个调度周期内能获取到多少实际运行时间,该函数会自底向上遍历任务组计算出任务在顶层能够获取到的时间片,如下:

static u64 sched_slice(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
	u64 slice = __sched_period(cfs_rq->nr_running + !se->on_rq);

	for_each_sched_entity(se) {
		struct load_weight *load;
		struct load_weight lw;

		cfs_rq = cfs_rq_of(se);
		load = &cfs_rq->load;

		if (unlikely(!se->on_rq)) {
			lw = cfs_rq->load;	------------------------------------------------------1update_load_add(&lw, se->load.weight);
			load = &lw;
		}
		slice = __calc_delta(slice, se->load.weight, load);  -------------------------2}
	return slice;
}

(2)通过当前调度实体 se 的权重以及对应的 cfs_rq 权重(当前 cfs_rq 下所有调度实体权重之和)计算出当前层次能够获取到的时间,并按照se->parent往上遍历获取到该任务组在 root task_goup 能够获取到的时间片。
如何保证组任务获取的时间片不超过分配的时间片?
通过定时器来确保抢占,sched_slice函数将会在check_preempt_tick, hrtick_start_fair函数中调用,如下:

check_preempt_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr)
{
	unsigned long ideal_runtime, delta_exec;
	struct sched_entity *se;
	s64 delta;

	ideal_runtime = sched_slice(cfs_rq, curr); // 计算当前任务能执行的最大时间片。
	delta_exec = curr->sum_exec_runtime - curr->prev_sum_exec_runtime;
	if (delta_exec > ideal_runtime) {	// 如果当前任务的执行时间大于了能够运行的最大时间片则会重新调度当前任务。
		resched_curr(rq_of(cfs_rq));
		/*
		 * The current task ran long enough, ensure it doesn't get
		 * re-elected due to buddy favours.
		 */
		clear_buddies(cfs_rq, curr);
		return;
	}
	...
}

// 该函数和 check_preempt_tick 配合,在 cfs 任务出队入队时会更新 cfs 的调度周期以便能够
// 在执行时间到期时触发定时器以便检测是否需要重新调度任务,以满足 cfs 任务最小时间片粒度以及调度延时。
static void hrtick_start_fair(struct rq *rq, struct task_struct *p)
{
	struct sched_entity *se = &p->se;
	struct cfs_rq *cfs_rq = cfs_rq_of(se);

	SCHED_WARN_ON(task_rq(p) != rq);

	if (rq->cfs.h_nr_running > 1) {
		u64 slice = sched_slice(cfs_rq, se);
		u64 ran = se->sum_exec_runtime - se->prev_sum_exec_runtime;
		s64 delta = slice - ran;

		if (delta < 0) {	// 当检测定时器触发后,检测到时间片用完则重新调度任务。
			if (rq->curr == p)
				resched_curr(rq);
			return;
		}
		hrtick_start(rq, delta);
	}
}

不考虑组调度情况下每个进程的时间片计算如下:

                           se->load.weight
time = sched_period * ------------------------- // cfs_rq->load.weight 的权重是cfs就绪队列所有任务的权重之和。
                         cfs_rq->load.weight 

加入组调度后计算如下:gse = group se(任务组对应 cpu 的调度实体),gcfs_rq = group cfs_rq(任务组对应 cpu 的cfs_rq 就绪队列)

                           se->load.weight            gse->load.weight 
time = sched_period * ------------------------- * ------------------------ // cfs_rq 如果是 root,则是 rq cfs_rq,如果是属于任务组则是任务组下所有 cfs_rq 权重之和。
                         gcfs_rq->load.weight        cfs_rq->load.weight

上述例子是首先计算出调度实体在对应任务组中 cpu cfs_rq 中的权重占比,接着计算该任务组的 se 在整个 rq cfs 就绪队列权重占比最后乘以调度周期,即可计算任务组中某个任务能分配的实际运行时间,内核中对这种情况进行了更通用拓展既可以计算多层次的情况,实现代码是上边 sched_slice 函数。
其中涉及三部分权重:

  1. 进程的权重
  2. 任务组对应 cpu 的调度实体(se)的权重
  3. cfs_rq 就绪队列的权重以及整个任务组下所有 cfs_rq 权重之和

进程的权重在任务创建时就存在,可通过设置调度参数修改。
任务组对应 cpu 的调度实体权重由任务组shares,总负载计算得出(下面介绍)。
cfs_rq 就绪队列权重由 cfs_rq 队列下所有调度实体的权重累加得来。

在上面的描述中,任务组对应 cpu 的调度实体权重计算方式没有说明,这里详细解释:
首先,内核定义任务组的调度实体权重有如下公式:(ge = group se[cpu],tg = group,grq = group cfs_rq[cpu]

						tg->shares * grq->load.weight (该cfs_rq 的load.weight)
	ge->load.weight = -------------------------------1)
				  \Sum grq->load.weight	(task_group 上所有 cfs_rq 的 load.weight)

在一个多核系统上,一个任务组将会包含多个 cfs_rq,每个 cpu 分配一个 cfs_rq。所以计算 \Sum grq->load.weight 总和开销太大(cpu 数量越多,访问其他 cpu 的 group cfs_rq 造成数据访问的竞争激烈)。因此使用平均负载来近似处理,平均负载值变化缓慢,因此近似后的值更容易计算且稳定,近似处理条件如下,将权重和平均负载近似处理(平均负载 load_avg 后面详细说明)。

grq->load.weight -> grq->avg.load_avg // 对权重计算转换为对负载计算,任务组下所有cfs_rq的负载总和被记录在 tg->load_avg 中。

因此上述(1)公式变为:

                  tg->shares * grq->avg.load_avg
ge->load.weight = ------------------------------                (2)
						  tg->load_avg
 
Where: tg->load_avg ~= \Sum grq->avg.load_avg

公式(2)问题在于,因为平均负载值变化很慢 (它的设计正是如此) ,这会导致在边界条件的时候的瞬变。 具体而言,当空闲 group 开始运行一个进程的时候,我们的 cpu 的 grq->avg.load_avg需要花费时间来慢慢变化,产生不良的延迟。在这种特殊情况下(单核CPU也是这种情况),公式(1)计算如下:

                   tg->shares * grq->load.weight
ge->load.weight = ------------------------------- = tg->shares (3)
			            grq->load.weight

现在就是将近似公式(2)在单核情景时修改成公式(3)的情况:

ge->load.weight =
             tg->shares * grq->load.weight
   ---------------------------------------------------         (4)
   tg->load_avg - grq->avg.load_avg + grq->load.weight

但是因为 grq->load.weight 可以降到0,导致除数是0。因此我们需要使用grq->avg.load_avg 作为其下限,然后给出:

                  tg->shares * grq->load.weight
ge->load.weight = -----------------------------		           (5)
            		tg_load_avg'
 Where: tg_load_avg' = tg->load_avg - grq->avg.load_avg +
                       max(grq->load.weight, grq->avg.load_avg)

最终代码中即可得到:

                                tg->shares * load
ge->load.weight = -------------------------------------------------
                   tg->load_avg - grq->tg_load_avg_contrib + load
load = max(grq->load.weight, grq->avg.load_avg)

tg->load_avg 是所有 group cfs_rq 负载贡献和。
grq->tg_load_avg_contrib 是该 cfs_rq 已经向task_group 贡献的负载,这个变量是个累计值,超过阈值部分被累计到 tg->load_avg 中。

综上可知:任务组中任务能获取的时间有一个重要因素是任务组的负载总和。

3.2 patch-1 问题复现

首先根据上面 patch-1 中的描述,可以知道:

该 patch 修复了一个问题:cfs_rq上的旧负载没有正确衰减,从而导致公平性急剧下降的奇怪行为。具有相同权重控制组的实际工作负载最终分别获得99%和1%(!!)的cpu时间。
当把一个空闲任务通过 pid 附加到 cgroup 节点(附加到cfs_rq)时,通过attach_entity_cfs_rq将任务的旧负载附加到新的cfs_rq上。如果在进入队列/唤醒之前将任务移动到另一个cpu,则负载将移动到cfs_rq->removed 上。这样的移动将发生在对任务执行一个请求时。
然而,负载不会从task_group本身移除,使其看起来像在cfs_rq上有一个恒定的负载。这导致其他同级cfs_rq上的任务的vruntime增加的速度比预期的要快;造成严重的公平性问题。如果在给定的cfs_rq上没有启动其他任务,并且由于cpuset的原因,它不会发生负载卸载,则此负载将永远不会正确卸载。有了这个补丁,负载将在update_blocked_average中被正确地移除。

描述很空洞,我们通过实际程序来复现该问题:
根据 patch-1 中的描述,在cgroup中可以得到如下一个结构:

parent
	-> sub-1 (weight = 1024)
		-> taskA	(时间片 = 1024 = 1024 * task_weight/Sum weight)
	-> sub-2 (weight = 1024)
		-> taskB	(时间片 = 1024 = 1024 * task_weight/Sum weight)

当两个完全相同的任务分别属于一个任务组下的两个子任务组中时,负载相同得出的权重相同则获取到的时间片也相同,如果此时其中一个子任务组负载增加(且不衰减)则相对应的该任务组权重将会变化,那么之前的两个任务能获取到的时间则会发生变化,大致如下:

对 sub-1 加入负载
parent
	-> sub-1 (weight = 1024)
		-> taskA	(时间片 = 小于 1024 = 1024 * task_weight/Sum weight + weight)
	-> sub-2 (weight = 1024)
		-> taskB	(时间片 = 大于 1024 = 1024 * task_weight/Sum weight)

那么反映到用户态看到的则是在同一个cpu上相同两个程序 cpu 利用率并不相等,在一些临界状态出现 taskA : taskB = 30% : 70% 甚至 1% : 99 % 的情况。

复现:
首先一共有三个程序,一个睡眠程序(触发问题),一个控制触发bug程序,一个自动执行脚本(还包括两个相同的压力程序,用于观察 cpu 利用率变化)。

睡眠任务程序如下:

#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define FIFO_IN_FILE "fifo_A"
#define FIFO_OUT_FILE "fifo_B"

static int mypid;
static int fifo_rfd, fifo_wfd;

static int fifo_create(char *fifo_file)
{
        if (access(fifo_file, F_OK) == -1) {
                if (mkfifo(fifo_file, 0777) < 0) {
                        perror("mkfifo");
                        exit(-1);
                }
        }
        return 0;
}

static int fifo_file_create(void)
{
        fifo_create(FIFO_IN_FILE);
        fifo_create(FIFO_OUT_FILE);
        return 0;
}

static void fifo_open(void)
{
        fifo_rfd = open(FIFO_IN_FILE, O_RDWR);
        fifo_wfd = open(FIFO_OUT_FILE, O_RDWR);
        if (fifo_rfd < 0 || fifo_wfd < 0) {
                perror("open fifo");
                exit(-1);
        }
}

static int send_msg(char *buf)
{
	int len;
	char send_buf[64] = {0};

	sprintf(send_buf, "%d %s", mypid, buf);
	printf("sleep: send msg: '%s'\n", send_buf);
	len = write(fifo_wfd, send_buf, strlen(send_buf));
	if (len < 0)
		perror("write fifo");
	
	return len;
}

void delay(int n)
{
	unsigned int i,j,k,sum;

	for (i=0; i<n; i++)
		for (j=0; j<40000; j++)
			for (k=0; k<10000; k++)
				sum++;
}

int main(int argc, char **argv)
{
	int len;
	char buf[128] = {0};
	unsigned long t = 1000;
	if (argc > 1) {
		t = atoi(argv[1]);
		if (t < 0)
			t = 1000;
	}

	memset(buf, 0, sizeof(buf));
	mypid = getpid();
	printf("sleep: mypid: %d\n", mypid);

	fifo_file_create();	// 创建一个 fifo 管道,与主程序通信,以便在设置好睡眠任务的 cgroup/cpuset 后被唤醒
	fifo_open();
	delay(10);	// 施加负载,使得睡眠任务负载在衰减为0之前获得一定高负载,使其在复现程序中有负载能附加到对应的 task_group->load_avg 中。
	send_msg("sleep");	// 通过管道通知主程序睡眠任务的 pid,以便主程序通过 pid cgroup/cpuset。
	while(1) {
		len = read(fifo_rfd, buf, sizeof(buf)); // 进入睡眠状态,使其主程序设置 cgroup/cpuset 时保证触发问题。
        if (len < 0)
            continue;
		printf("sleep: recv: '%s'\n", buf);	// 任务醒来,触发问题。
	}
    while (1)
        usleep(1000000);
	return 0;
}

主程序如下:



#include 
#include 
#include 
#include 

#include 
#include 
#include 
#include 

#define FIFO_IN_FILE "fifo_B"
#define FIFO_OUT_FILE "fifo_A"

static int cg_cpu, cg_cpuset;
static int mypid;
static int fifo_rfd, fifo_wfd;

static int fifo_create(char *fifo_file)
{
	if (access(fifo_file, F_OK) == -1) {
		if (mkfifo(fifo_file, 0777) < 0) {
			perror("mkfifo");
			exit(-1);
		}
	}
	return 0;
}

static int fifo_file_create(void)
{
	fifo_create(FIFO_IN_FILE);
	fifo_create(FIFO_OUT_FILE);
	return 0;
}

static void fifo_open(void)
{
	fifo_rfd = open(FIFO_IN_FILE, O_RDWR);
	fifo_wfd = open(FIFO_OUT_FILE, O_RDWR);
	if (fifo_rfd < 0 || fifo_wfd < 0) {
		perror("open fifo");
		exit(-1);
	}
}

#if 1
#define CGROUP_CPU_CG1		"/sys/fs/cgroup/cpu/zy_test/sub-1/test_sub/cgroup.procs"
#define CGROUP_CPUSET		"/sys/fs/cgroup/cpuset/zy_test/cgroup.procs"
#else
#define CGROUP_CPU_CG1		"/cpu/test/sub-1/sub/cgroup.procs"
#endif

static int cgroup_files_open(void)
{
	cg_cpu = open(CGROUP_CPU_CG1, O_RDWR); // 打开 cgroup 组的 cgroup.procs 后续将任务设置到该任务组。
	cg_cpuset = open(CGROUP_CPUSET, O_RDWR); // 打开 cpuset 组的 cgroup.procs 后续将任务设置到该 cpuset 上运行。
	if (cg_cpu < 0 || cg_cpuset < 0) {
		perror("open cgroup");
		exit(-1);
	}
	return 0;
}

static int send_msg(char *buf)
{
      int len;
      char send_buf[64] = {0};

      sprintf(send_buf, "%d %s", mypid, buf); // 获取到睡眠任务的 pid。
      printf("main: send msg: '%s'\n", send_buf);
      len = write(fifo_wfd, send_buf, strlen(send_buf));
      if (len < 0)
              perror("write fifo");
      return len;
}

static int events(char *buf, int len)
{
	int ret = 0;
	char pid[16] = {0};
	char action[32] = {0};
	char send_buf[64] = {0};

	sscanf(buf, "%s %s", &pid, &action);
	printf("main: recv: pid=%s, action=%s\n", pid, action);

	if (strcmp(action, "sleep") == 0) {
		if (write(cg_cpu, pid, strlen(pid)) < 0) { // 将睡眠任务设置到 CGROUP_CPU_CG1 组中。
			perror("write cg_cpu");
			exit(-1);
		}
		if (write(cg_cpuset, pid, strlen(pid)) < 0) { // 将睡眠任务设置到 CGROUP_CPUSET 组中。
			perror("write cg_cpuset");
			exit(-1);
		}
		send_msg("wakeup"); // 通过管道发送消息,唤醒睡眠任务触发问题。
		exit(0);
	}
}

int main(int argc, char **argv)
{
	int fd, len;
	char buf[1024];

	memset(buf, 0, sizeof(buf));
	mypid = getpid();
	printf("main: mypid: %d\n", mypid);
	fifo_file_create(); // 同睡眠任务相同。
	fifo_open();
	cgroup_files_open(); // 打开需要设置的 cgroup/cpuset 文件
	while(1) {
		memset(buf, 0, sizeof(buf));
		len = read(fifo_rfd, buf, sizeof(buf));
		if (len > 0)
			events(buf, len);
	}
}

执行脚本如下:

#!/bin/sh

CPUSET_TEST_ME=/sys/fs/cgroup/cpuset/zy_test_me
CPUSET_TEST=/sys/fs/cgroup/cpuset/zy_test

CGROUP_TEST_ME=/sys/fs/cgroup/cpu/zy_test_me
CGROUP_TEST=/sys/fs/cgroup/cpu/zy_test
CGROUP_TEST_SUB1=$CGROUP_TEST/sub-1
CGROUP_TEST_SUB2=$CGROUP_TEST/sub-2

# $1 test cpu       36
# $2 task_cpu_me    50
# $3 taskset cpu    45

TEST_CPU=$1
TEST_CPU_ME=$2
TASK_SET_CPU=$3
TASK_SET_CPU_MAIN=$4

mkdir -p $CPUSET_TEST_ME
mkdir -p $CPUSET_TEST

echo $TEST_CPU > $CPUSET_TEST/cpuset.cpus # 设置测试任务能够运行的 cpu,只设置一个 cpu,如 0123...
echo 0 > $CPUSET_TEST/cpuset.mems

mkdir -p $CGROUP_TEST_SUB1/test_sub
mkdir -p $CGROUP_TEST_SUB2/sub
mkdir -p $CGROUP_TEST_ME

# test
taskset -c $TEST_CPU ./stress_app_1 &	# 启动压力程序1,纯压力程序,做累加操作。
taskset -c $TEST_CPU ./stress_app_2 &   # 启动压力程序2,纯压力程序,做累加操作。
taskset -c $TASK_SET_CPU ./sleep_app &  # 启动睡眠程序。
taskset -c $TASK_SET_CPU_MAIN ./yc_main & # 启动主程序,该程序启动后,将会在 sleep_app 睡眠期间
										  # 首先将睡眠任务设置到 sub-1/test_sub 组中,接着将睡眠任务设置到 CPUSET_TEST 指定的 cpu 上。

stress1_pid=`pgrep stress_app_1`
stress2_pid=`pgrep stress_app_2`
sleep_app_pid=`pgrep sleep_app`

echo $stress1_pid > "$CPUSET_TEST"/cgroup.procs		# 设置压力程序1 到 CPUSET_TEST 指定的 cpu 上。
echo $stress1_pid > "$CGROUP_TEST_SUB1"/test_sub/cgroup.procs # 设置压力程序1 到 sub-1/test_sub 组中。

echo $stress2_pid > "$CPUSET_TEST"/cgroup.procs     # 设置压力程序2 到 CPUSET_TEST 指定的 cpu 上。
echo $stress2_pid > "$CGROUP_TEST_SUB2"/sub/cgroup.procs      # 设置压力程序2 到 sub-2/sub 组中。

启动后 cgroup 结构如下:

cpu/
	-> zy_test
		-> sub-1
			-> test_sub
				-> stress_app_1 (50%)
				-> sleep_app	(0%)
		-> sub-2
			-> sub
				-> stress_app_2 (50%)

本地实际情况:

[    5.096284] xxxxx: create cfs rq: cpu 0 ffff9da4790b5200
[    5.099058] xxxxx: create cfs rq: cpu 1 ffff9da4790b5600
[    5.102828] xxxxx: create cfs rq: cpu 2 ffff9da4790b5a00
[    5.110169] xxxxx: create cfs rq: cpu 3 ffff9da4790b5e00
[    5.128566] random: fast init done
/ #
mypid: 1117
mypid: 1118
send msg: '1117 sleep'
recv: pid=1117, action=sleep
[    7.109464] xxxxx: move_group old cfs_rq ffff9da47fc20bc0
[    7.112331] xxxxx: move_group new cfs_rq ffff9da4790b5200
[    7.113948] xxxxx: move_group new cfs_rq weight 0 tg load_avg 1018
[    7.116882] xxxxx: enqueue cfs_rq load avg 0 se 952
[    7.121349] xxxxx: update_tg_load_avg cfs 952 tg 1018
[    7.128334] xxxxx: attach_task_cfs_rq finish cpu 0
[    7.129983] xxxxx: attach cfs_rq weight 0 tg load_avg 1970
[    7.131765] xxxxx: add to removed ffff9da4790b5200 512
send msg: '1118 wakeup'
recv: '1118 wakeup'
/ # top
  PID  PPID USER     STAT   VSZ %VSZ CPU %CPU COMMAND
 1114     1 0        R     1072  0.0   1 17.0 ./stress_2_app
 1113     1 0        R     1072  0.0   1  4.8 ./stress_1_app
    1     0 0        S     2940  0.0   3  0.0 {linuxrc} init
 1099     1 0        S     2940  0.0   0  0.0 -/bin/sh
 1100     1 0        S     2940  0.0   1  0.0 {linuxrc} init
 1128  1099 0        R     2940  0.0   2  0.0 top
 1118     1 0        S     1204  0.0   3  0.0 ./yc_main
 1117     1 0        S     1076  0.0   1  0.0 ./sleep_app
    7     2 0        IW       0  0.0   3  0.0 [kworker/u8:0-ev]
/ # kill -9 1117
/ # top
  PID  PPID USER     STAT   VSZ %VSZ CPU %CPU COMMAND
 1114     1 0        R     1072  0.0   1 20.0 ./stress_2_app
 1113     1 0        R     1072  0.0   1  5.0 ./stress_1_app
    1     0 0        S     2940  0.0   3  0.0 {linuxrc} init
 1099     1 0        S     2940  0.0   0  0.0 -/bin/sh
 1100     1 0        S     2940  0.0   1  0.0 {linuxrc} init
 1129  1099 0        R     2940  0.0   2  0.0 top
 1118     1 0        S     1204  0.0   3  0.0 ./yc_main
    7     2 0        IW       0  0.0   3  0.0 [kworker/u8:0-ev]
  938     2 0        SW       0  0.0   0  0.0 [scsi_eh_0]
   44     2 0        IW       0  0.0   2  0.0 [kworker/u8:3-ev]
   52     2 0        IW       0  0.0   2  0.0 [kworker/2:1-mm_]
  133     2 0        IW       0  0.0   1  0.0 [kworker/1:1-mm_]
/ # cat /proc/sched_debug  | grep ":/test" -A 28 | egrep "(tg_load_avg|test)"
rt_rq[0]:/test/sub-2/sub
rt_rq[0]:/test/sub-1/test_sub
rt_rq[0]:/test/sub-2
rt_rq[0]:/test/sub-1
rt_rq[0]:/test
cfs_rq[1]:/test/sub-2/sub
  .tg_load_avg_contrib           : 1009
  .tg_load_avg                   : 1009
cfs_rq[1]:/test/sub-1/test_sub
  .tg_load_avg_contrib           : 1024
  .tg_load_avg                   : 1976
cfs_rq[1]:/test/sub-1
  .tg_load_avg_contrib           : 531
  .tg_load_avg                   : 1483
cfs_rq[1]:/test/sub-2
  .tg_load_avg_contrib           : 1009
  .tg_load_avg                   : 1009
cfs_rq[1]:/test
  .tg_load_avg_contrib           : 1395
  .tg_load_avg                   : 2347

可以看到即便此时将 sleep_app 任务 kill 掉后,stress_2_app/stress_1_app 在 cpu 1 上的 cpu 利用率不相同,sub-2/sub 组的任务明显获取到更多的 cpu 时间。

查看 /proc/sched_debug 可以看到 cfs_rq[1]:/test/sub-1/test_sub 的负载贡献度为 1024(tg_load_avg_contrib),而对应任务组总的任务负载为 1976(tg_load_avg)差值为 952,打印可以看到enqueue cfs_rq load avg 0 se 952该 952 正是睡眠任务此时的负载(load_avg)并且被累加到了对应的 cfs_rq,再通过cfs_rq传播到了对应的 task_group->load_avg 上。task_group->load_avg 通过上文可知将会影响任务组 se 权重计算,从而影响任务组下任务的运行时间。

待续。。。

你可能感兴趣的:(linux,linux,cfs,group)