Fork me on GitHub
侧边栏

调度器分支之RTG

一、引言


RTG(Related Thread Group)是Linux内核中的一个调度特性,主要作用是优化线程的CPU调度和调频策略。它通过将存在数据共享和通信关联的线程组合在一起,实现以下几个关键功能:

  1. 选核优化:RTG确保相关线程组内的线程尽可能在同一个CPU簇上运行,以利用共享的缓存,提高性能并降低功耗 。
  2. 聚合调频(Frequency Aggregation):RTG通过聚合线程组内的负载来优化动态电压和频率调整(DCVS),使得调频策略更加精准地反映整个线程组的负载情况,而不是单个线程 。
  3. Busy Hysteresis:这是一种新引入的功能,用于避免CPU在负载突然下降时立即进入低功耗模式,而是保持一定的迟滞时间,以维持更稳定的性能表现 。

此外,RTG的实现还涉及到一些特定的数据结构和策略,如related_thread_group结构体,用于跟踪和组织相关线程;以及schedtune.colocate属性,用于标识线程是否属于同一线程组 。在Android系统中,前台应用的相关线程会被加入到一个默认的RTG组中,以实现更优的调度和调频 。随着内核版本的演进,RTG的功能也在不断优化和调整,以适应不同的使用场景和需求 。

主要用于foregrounds和top-app。

网上关于RTG内容很少,本文以作者个人的理解为主,难免存在偏差与瑕疵,欢迎指正。本文以codeaurora的https://source.codeaurora.cn/quic/la/kernel/msm-4.14/代码及msm-5.4进行分析。

在详细介绍rtg之前,需要了解一下rtg的背景。Rtg叫着related thread group.顾名思义“相关线程组”。这个怎么理解呢?

我们设想一种场景,有两个跑分线程,一个叫thread0,另外一个叫thread1.

其中thread0执行一段时间之后唤醒thread1执行,然后自己睡眠。同样的thread1运行一段时间之后唤醒thread0执行,自己睡眠,如此反复。假如thread0跟thread1分别运行在不同的CPU上,示意图如下:

又假设thread0跟thread1每次运行时长都是一样。这时候CPU0跟CPU1的使用率都是50%,一般来讲达不到 80%的CPU使用率这样的DCVS提频条件。

如果假如thread0跟thread1都运行在同一个CPU上呢,如下示意图

那么CPU0始终处于100%的使用率,CPU频率会一直上提,一直到最高。

同样的测试用例,就因为调度时任务运行在不同的CPU上,导致最终的结果完全不一样。与这种情况比较类似的就是android显示里面的应用主线程及render线程。

上面只是举了一个简单的例子,来说明rtg出现的背景,即系统的线程不是孤立的,它们之间存在各种依赖关系。那从不管是调度的角度讲、还是从DCVS的角度,都不能忽视这种存在。而恰恰当前的CFS调度里面忽略了这种关联性,因此RTG横空出世。

由于rtg的发展有一些历史,直接看最新的代码会比较费解,所以我们从codeaurora的msm-4.14 版本(https://source.codeaurora.cn/quic/la/kernel/msm-4.14/)的代码开始了解一下rtg的真面目。

二、RTG之选核优化

先看一下这个结构体的定义

其中id为每一个related_thread_group的唯一识别号,从1开始,系统默认的rtg组的DEFAULT_CGROUP_COLOC_ID就是1。

tasks为一个链表,里面都是属于该组的进程,通过add_task_to_group及remove_task_from_group来进行添加或者删除。

list为group组所在的链表,其中active_related_thread_groups为一个全局链表,里面都是所有不同id的related_thread_group。一个新的group创建的时候会被加入到这个链表。

preferred_cluster是一个struct sched_cluster类型的结构体指针,指整个rtg组的线程调度时优先选择推荐的cluster(CPU簇)。这里对这个结构体不展开描述,不影响我们对于rtg的理解。

last_update是记录_set_preferred_cluster中每次preferred_cluster更新的时间戳。用来避免频繁的更新preferred_cluster。

上面介绍了几个关键的字段,也为此实现了相关的工具函数。

系统中默认创建了一个id为DEFAULT_CGROUP_COLOC_ID的组。并且在stune cgroup的top-app组里面默认打开。

那么top-app里面的所有线程的这个属性都为1了。具体见cgroup的实现walt_schedgp_attach函数。这样相关的线程都被sync_cgroup_colocation

加入到default的RTG组里面了。至此我们知道android系统的top-app组,即前台应用的相关线程被加入到一个id为DEFAULT_CGROUP_COLOC_ID的related thread group组里面。这里需要注意一点是schedtune.colocate不支持动态更新,原因是在于只有在做cgroup切换的时候,才把相关的任务加到rtg组里面,后面也仅仅只有新创建的new task被wake_up_new_task中调用add_new_task_to_grp被加入到DEFAULT_CGROUP_COLOC_ID组。即使支持动态开关,也只有下一次应用切换的时候才会生效。因为干脆不支持动态开关。

至此related_thread_group结构体里面的主要字段都做了基本的介绍。下面从preferred_cluster字段开始,沿着这条线继续介绍代码。我们看到当一个线程加入到这个组里面的时候,函数add_task_to_group会调用_set_preferred_cluster函数。下面我们对这个函数详细的介绍一下。

首先可以看到last_update字段的用处了,防止_set_preferred_cluster被频繁的调用。这个约束时间最短为窗口周期的1/10;

这个函数继续看group组里面的是否有线程被设置了SCHED_BOOST_ON_BIG属性(per-task的属性,辅助进行任务调度选核的策略)这样的任务摆核策略,如果是的话,就直接将整个组boost到大核上。然后通过遍历group里面的所有tasks,计算累计负载。然后通过best_cluster选择一个preferred_cluster。

4490 PID VID

best_cluster的代码比较简单,输入参数为上面提到的累计负载。通过sched_group_upmigrate及sched_group_downmigrate来判断选择一个合适的CPU簇。注意这个代码的注释,返回一个可以满足group所需算力的CPU簇。

sched_group_upmigrate和sched_group_downmigrate这两个参数是不是跟sched_upmigrate和sched_downmigrate很像?只不过sched_upmigrate跟sched_downmigrate是用来判断单个任务是否需要运行在大核的条件。

这里就引出了rtg的第一个作用,即RTG希望将group组里面的关联线程都放在同一个CPU簇上。为什么需要这么做呢?文章开头的地方讲到了,related_thread_group里面都是一些存在相关联系的线程(虽然当前top-app组稍显粗暴,而且不够全面,没有考虑到锁、binder通信等等各种情况),它们之间大概率会存在数据的共享。而一个cluster之间是存在共享cache的,因此可以合理的利用这些cache来提升性能降低功耗。还有一个原因就是后面要讲到的RTG的第二个功能。

在CFS的find_energy_efficient_cpu里面可以看到。在后续的task的选核中,针对rtg里面的线程,进行选核上的一些优先。具体代码不详细展开描述。

1

在后续的kernel版本升级及代码演进中,又进行了一些功能的调整,比如在5.4内核上,引入skip_min来替代perferredcluster,并通过sched_min_task_util_for_colocation来过滤掉负载较低的任务,当任务负载低于sched_min_task_util_for_colocation时,其选核时的优先调度大核任然可以持续维持sched_task_unfilter_period的时间(这个名字也很有趣,不过滤的时间周期,迟滞一段时间)。整体功能并没有发生大的变化。

三、RTG之聚合调频

RTG的第二个功能是任务负载的聚合调频(Frequency aggregation)。回到文章开头讲到跑分场景,让thread0跟 thread1在两种场景下(运行在两个CPU vs 运行在同一个CPU上)的效果一致。需要一种策略来进行DCVS调频的优化。RTG的第二个功能也就是这么做的。

这段代码在walt_irq_work里面,我们看一下它是怎么实现的。

1

在每一个rq里面都有一个struct group_cpu_timegrp_time的字段。

2

其中curr_runnable_sum跟prev_runable_sum是用来统计当前窗口跟前一个窗口的任务。nt_curr_runnable_sum跟nt_prev_runnable_sum中的nt指new task,是针对新创建线程的一种单独统计。那么哪些task是new task呢?简单粗暴,p->ravg.active_windows < SCHED_NEW_TASK_WINDOWS; active_windos 少于SCHED_NEW_TASK_WINDOWS(5)的任务即为new task。

其中curr_runnable_sum跟prev_runable_sum是用来统计当前窗口跟前一个窗口的任务。nt_curr_runnable_sum跟nt_prev_runnable_sum中的nt指new task,是针对新创建线程的一种单独统计。那么哪些task是new task呢?简单粗暴,p->ravg.active_windows < SCHED_NEW_TASK_WINDOWS; active_windos 少于SCHED_NEW_TASK_WINDOWS(5)的任务即为new task。

下面从update_cpu_busy_time这个函数开始,这个函数是用来计算更新每一个任务的负载的。

3

除了rq->grp_time里面有这四个变量之外,在rq里面也存在这4个同名的变量。那么为什么会同时存在这4个变量呢?

4

当线程p->grp不为空,即p属于一个related_thread_group里面的一个线程。那么curr_runnable_sum, prev_runnable_sum, nt_curr_runnable_sum,nt_prev_runnable_sum这4个指针就不再指向rq里面的相应字段了,而是指向了rq->grp_time里面的字段。

这说明什么呢?说明walt针对grp组相关线程的负载做了单独的统计,并没有直接累计到rq里面去。为什么需要单独统计呢?这就是为了方便在做负载聚合调频的时候能够快速的进行计算。

对于rq->grp_time的具体操作除了上面的update_cpu_busy_time,还有fixup_busy_time做任务迁核的修正,transfer_busy_time用于任务加入或者退出某个group时候的修正。rollover_cpu_window用于在window窗口周期到期的时候的一些数据处理。

最后我们看一下任务负载聚合的地方。在freq_policy_load里面。这个函数是用来给schedutil governor查询负载的时候用的。

1

当sched_freq_aggr_en为true的时候,一个rq上的负载等于其本身负载加上cluster->coloc_boost_load 也就是group在cluster上累计的负载。

当sched_freq_aggr_en为false的时候,负载就等于rq->prev_runnable_sum加上这个rq上rtg类型的线程的负载。这也就是我们通常意义上CPU的任务累计负载。

这里面的sched_freq_aggr_en是一个全局变量。具体是在kernel/sched/boost.c里面进行操作的。sched_boost里面支持多种boost的设置。

2

由于现在将top-app组里面的所有线程都加入到默认的related_thread_group里面的方式过于简单粗暴,所以在频率的聚合上进行了一些限制,只有在sched_boost设置为FULL_THROTTLE_BOOST或者RESTRAINED_BOOST的时候才开启负载聚合调频。这里面再次遵循一种设计原则,scheduler/DCVS提供机制,上层业务在合适的地方合理的利用这些机制,机制跟业务场景需求的合理组合搭配。当然当前基于top-app的线程做负载聚合是不够理想,以android系统的system server为例,它是一个系统服务,当他在处理来至于top-app线程的binder请求的时候,应该动态将其也归为top-app组,当处理完成后,需要将其恢复到原来的组里面。

四、RTG之Busy Hysteresis

RTG在新的内核版本上新引入的第三个功能叫着Busy Hysteresis,也是基于RTG的colocation这个功能,在后面的内核版本里面引入的。这时候perferredcluster已经被调整为skip_min。Hysteresis指迟滞,意思就是CPUbusy会持续迟滞一段时间。这个迟滞具体是指什么意思呢?且听慢慢道来。涉及的变量如下

3

沿着函数sched_update_hyst_times可以大概了解一下这几个变量的含义。

4

其中sysctl_sched_busy_hyst_enable_cpus是一个bitmap,每一个bit用来表示相应的cpu是否打开了busy_hyst功能。

sysctl_sched_busy_hyst名字比较费解。不过如果看sysctl接口的定义就比较一目了然。

1

名字叫sched_busy_hyst_ns,是一个迟滞时间,单位为ns。

per_cpu(coloc_hyst_time, cpu) 这个per-cpu的赋值比较有意思,除了sysctl_sched_coloc_busy_hyst_cpu这个时间之外(默认定义为39ms),还有一个rtgb_active, rtgb_active最后又是跟related_thread_group的skin_min有关。

coloc_hyst_busy是根据百分比以及cpu的capacity换算过来的一个阈值。后面会具体在update_busy_hyst_end_time中被用到。

函数sched_update_hyst_times被_set_preferred_cluster调用,当skin_min发生变化的时候会更新相关的hyst的信息(如下图)。

2

另外还有一个函数update_busy_hyst_end_time,被用来更新hyst end time,也就是迟滞的结束时间点。

3
当dequeue为true,也就是cpu上任务数量减少的时候,并且cpu的负载大于coloc_hyst_busy(上面讲过了,基于CPU算力以及一个预制的百分比算出来的一个阈值)。这个busy_hyst_end_time的变量,我们后面会发现在sched_lpm_disallowed_time函数中被使用。

4

posted @ 2024-08-21 10:06  yooooooo  阅读(0)  评论(0)    收藏  举报