关于linux CFS调度类中sysctl_sched_min_granularity含义的思考

1,662 阅读4分钟

在学习linux进程管理CFS调度类的过程中,有一个变量sysctl_sched_min_granularity,在源码中定义如下:

// https://elixir.bootlin.com/linux/v4.9.81/source/kernel/sched/fair.c#L69
/* CPU 密集型任务的最小抢占粒度
 * Minimal preemption granularity for CPU-bound tasks:
 *
 * (default: 0.75 msec * (1 + ilog(ncpus)), units: nanoseconds)
 */
unsigned int sysctl_sched_min_granularity	= 750000ULL;

在看源码的过程中,产生一个疑问,sysctl_sched_min_granularity是否是CFS中进程占用CPU的最小时间(或者说最小调度粒度时间)呢?产生这个疑问的原因:

    1. 之前在阅读linux源码v4.9.81时,在网上搜索关于sysctl_sched_min_granularity变量的含义,基本都是说代表了进程占用CPU运行的最小时间。
    1. 但是在源码中,没有找到其表示进程占用CPU运行最小时间的代码。
    1. 偶然间看到一篇文章,写到sysctl_sched_min_granularity表示"当一个task错过了唤醒抢占,确保它不会等待一个调度周期才能够被调度.目的是减少调度时延"。而我在源码中,也是只能发现这一层含义。
    1. 最近在学习张师傅"Linux性能分析之CPU篇"课程中进程调度部分时,听到他也是认为sysctl_sched_min_granularity表示进程占用CPU运行的最小时间,这就与我所理解的有一些出入了,于是开始搜集资料找答案。
1.linux源码中的描述

linux源码v4.9.81关于sysctl_sched_min_granularity的使用有如下几个地方:

/*
 * Minimal preemption granularity for CPU-bound tasks:
 * (default: 0.75 msec * (1 + ilog(ncpus)), units: nanoseconds)
 */
unsigned int sysctl_sched_min_granularity = 750000ULL;

//debug用,可以先不看
#ifdef CONFIG_SCHED_DEBUG
int sched_proc_update_handler(struct ctl_table *table, int write,
		void __user *buffer, size_t *lenp,
		loff_t *ppos)
{
    ......
	 sched_nr_latency = DIV_ROUND_UP(sysctl_sched_latency,
					sysctl_sched_min_granularity);
    ......
}
#endif

//当有太多任务(sched_nr_latency)时,我们必须延长这段时间,否则切片会变得太小
static u64 __sched_period(unsigned long nr_running)
{
	if (unlikely(nr_running > sched_nr_latency))
		return nr_running * sysctl_sched_min_granularity;
	else
		return sysctl_sched_latency;
}

//判断是否CFS运行队列中有调度实体能抢占当前调度实体
static void
check_preempt_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr)
{
	unsigned long ideal_runtime, delta_exec;
	struct sched_entity *se;
	s64 delta;
        //curr在一个调度周期内理论上应该运行的真实时间(非虚拟时间)
	ideal_runtime = sched_slice(cfs_rq, curr);
        //实际上curr已经运行的实际真实时间(非虚拟时间)
	delta_exec = curr->sum_exec_runtime - curr->prev_sum_exec_runtime;
        //实际运行时间比理论上应该运行的时间长,表示时间片用完
	if (delta_exec > ideal_runtime) {
		resched_curr(rq_of(cfs_rq));
		return;
	}
	 //确保以微弱优势错过唤醒抢占的任务不必等待完整的切片。
	if (delta_exec < sysctl_sched_min_granularity)
		return;
        //CFS运行队列红黑树最左端调度实体se
	se = __pick_first_entity(cfs_rq);
        //curr与se的虚拟时间相减
	delta = curr->vruntime - se->vruntime;
        //curr更小,则curr继续执行
	if (delta < 0)
		return;
        //se的虚拟时间偏小,且delta > ideal_runtime,
        //其中ideal_runtime是真实时间,希望权重小(ideal_runtime小)的任务更容易被抢占
	if (delta > ideal_runtime)
		resched_curr(rq_of(cfs_rq));
}

从上述关于sysctl_sched_min_granularity的代码,得不出sysctl_sched_min_granularity表示进程占用CPU运行的最小时间的结论。只能得出在check_preempt_tick()函数中,sysctl_sched_min_granularity可用来表示:delta_exec <= ideal_runtime && delta_exec > sysctl_sched_min_granularity成立,即一个调度实体运行时间未达到理论运行时间且运行时间大于sysctl_sched_min_granularity,则可以判断是否可以进行抢占,确保以错过唤醒抢占的任务不必等待完整的切片。 原因如下:

企业微信截图_16424076887857.png

ideal_runtime < delta_exec < sysctl_sched_min_granularity的话,发生抢占,sysctl_sched_min_granularity就不是最小调度粒度时间了。举例如下:

假设一个CFS队列中有:1个nice值-20的进程(权重88761),9个nice值为19的进程(权重15),
sysctl_sched_min_granularit = 0.75ms

总权重:total_weight = 88761 * 1 + 15 * 9 = 88896
调度周期:total_time = nr_running * sysctl_sched_min_granularity = (1 + 9) * 0.75ms = 7.5ms
每个nice值为19的进程的时间片:
ideal_runtime_19 = (weight_19 * total_time) / total_weight = (15 * 7.5ms) / 88,896 = 0.0013ms
nice值-20的进程的时间片:
ideal_runtime_-20 = (weight_-20 * total_time) / total_weight = (88761 * 7.5ms) / 88,896 = 7.4886ms

ideal_runtime_19 = 0.0013ms < sysctl_sched_min_granularit = 0.75ms


nr_running > 8
则total_time = nr_running * sysctl_sched_min_granularity
假设x个进程nice值小于0,平均执行时间为y1;(nr_running - x)个进程nice值大于0,平均执行时间为y2
x * y + (nr_running - x) * y2 = total_time
则
y1 > sysctl_sched_min_granularity成立
y2 < sysctl_sched_min_granularity成立

另外,尝试去搜了一下最新版本的linux源码,发现v5.12.3关于sysctl_sched_min_granularity的使用有改变,新增了如下使用:

static u64 sched_slice(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
	unsigned int nr_running = cfs_rq->nr_running;
        //表示调度实体在一个调度周期中的理论真实运行时间,省略了计算过程
	u64 slice;
        ......
        //如果sched_feature有设置BASE_SLICE,则强制slice最小为sysctl_sched_min_granularity
        //则表示进程占用CPU的最小运行时间为sysctl_sched_min_granularity
        if (sched_feat(BASE_SLICE))
		slice = max(slice, (u64)sysctl_sched_min_granularity);

	return slice;
}

有了这一部分新增的代码,在sched_feature有设置BASE_SLICE的时候,进程占用CPU的最小运行时间为sysctl_sched_min_granularity。

2.Android 11中的描述

在目前我使用的Android11中,内核版本为5.4,是没有上面的这个新增的patch的,故sysctl_sched_min_granularity还不是表示进程占用CPU的最小运行时间。在/sys/kernel/debug/sched_features中也没有设置BASE_SLICE这一项。

1642403610.png

3.下一步计划

1.是否在linux内核v5.12.3版本之后的系统上,sched_feature默认有设置BASE_SLICE,需要在实际系统上进行验证?

待验证

2.是否张师傅所说的sysctl_sched_min_granularity表示进程占用CPU的最小运行时间指的是v5.12.3之后的版本?

与张师傅沟通,经过一番讨论,张师傅也认为sysctl_sched_min_granularity表示进程占用CPU的最小运行时间的结论是有一定问题的。在此,也特别感谢张师傅百忙之中,不厌其烦地给我答疑解惑!

真正理解linux内核源码,对一切抱有好奇、怀疑的态度,从源码中寻找答案,确实是一件振奋人心、让人开心的事情!