一、Uclamp簡介
1. PELT負載跟蹤算法得到的 task util 與用戶空間的期望有時候會出現分歧,比如對於控制線程或UI線程,PELT計算出的util可能較小,認為是“小”task,而用戶空間則希望調度器將控制線程或UI線程看作“大”task,以便被調度到高性能核運行在高頻點上使任務更快更及時的完成處理。同樣地,對於某些長時間運行的后台task,eg:日志記錄,PELT計算出的task util 可能很大,認為是“大”task,但是對於用戶空間來說,此類task對於完成時間要求並不高,並不希望被當作“大”task,以利於節省系統功耗和緩解發熱。
2. 關於任務的性質、性能/功耗需求用戶空間擁有足夠的信息可以識別,那么若將用戶空間關於任務的信息傳遞給內核任務調度器,則能夠更好的幫助調度器進行任務的調度。Utilization Clamping(uclamp)便是這樣一種機制。
3. uclamp提供了一種用戶空間對於task util進行限制的機制,通過該機制用戶空間可以將task util鉗制在[util_min, util_max]范圍內,而cpu util則由處於其運行隊列上的task的uclamp值決定。通過將util_min設置為一個較大值,使得一個task看起來像一個“大”任務,使CPU運行在高性能狀態,加速任務的處理,提升系統的性能表現;對於一些后台任務,通過將util_max設置為較小值,使其看起來像一個“小”任務,使CPU運行在高能效狀態,以節省系統的功耗。
4. 在 CPU Utilization 和 Task Utilization 兩個維度的跟蹤信號。CPU Utilization 用於指示CPU的繁忙程度,內核調度器使用此信號驅動CPU頻率的調整(schedutil governor生效時);Task Utilization 用於指示一個task對CPU的使用量,表明一個task是“大”還是“小”,此信號可以輔助內核調度器選核。
5. Android Kernel 5.4以前,cgroup中存在一個 schedtune,也提供了與uclamp類似的功能。后來 uclamp 作為 schedtune 的替代方案合入Linux內核主線。下面是二者相關feature對比:
Features | Schedtune | Uclamp |
支持的主線版本 | Before 5.4 | 5.4 and Later |
APIs | 僅支持Cgroup v1,有限的cgroup | 支持Cgroup v1 v2,不限cgroup數量 支持基於proc的system-wide的API 支持基於syscall的per-task的API |
boost實現 | 基於SPC算法,即 util+(max-util)*boost% 而且boost只能為正值,只能boost不能限制 |
uclamp到基於[min, max],可boost可限制 |
低延遲支持 | perfer-idle feature | latency_sensitive feature |
RT任務支持 | 不支持 | 支持 |
二、Uclamp軟件架構
用戶空間進程管理服務將相關 task 的 util clamp 值通過適當接口傳遞到內核空間,調度器基於用戶空間提供的信息通過 schedutil 驅動頻率的調整,也可以基於該信息為task選擇適當的core。
三、Uclamp軟件實現
四、用到的桶算法
對於一個cpu來說,其運行隊列rq上可能同時存在幾個task(running/runnable),那么如何計算 cpu 的 util_min 和 util_max 則非常關鍵。uclmap 使用桶化算法實現這種計算。
將 [0, SCHED_CAPACITY_SCALE] 划分為 UCLAMP_BUCKETS 個區間,區間個數可以通過 CONFIG_UCLAMP_BUCKETS_COUNT 進行配置,每個區間看作一個桶,使用 uclamp_bucket 結構描述。
struct uclamp_bucket { //bucket::value始終是此bucket中所有task 生效clamp(就是enqueue到rq上的)的最大值。 unsigned long value : bits_per(SCHED_CAPACITY_SCALE); //11 //有多少個task位於桶內 unsigned long tasks : BITS_PER_LONG - bits_per(SCHED_CAPACITY_SCALE);//64-11=53 };
存在 UCLAMP_MIN、UCLAMP_MAX 兩個桶集合,rq上所有 task 的 uclamp_min 值放入 UCLAMP_MIN 桶集合、uclam_max 值放入 UCLAMP_MAX 桶集合。多個任務位於同一個桶內,桶的值按最大值聚合原則,即由uclamp值最大的 task 決定。桶與桶之間同樣按最大值聚合原則,即 cpu 的 uclamp 值由 value 值最大的桶決定。這樣可以保證高性能任務其性能需求始終能夠得到滿足。
五、用戶空間接口
1. 基於 procfs 的 system-wide API
/proc/sys/kernel/sched_util_clamp_min /proc/sys/kernel/sched_util_clamp_max /proc/sys/kernel/sched_util_clamp_min_rt_default
取值范圍 [0, SCHED_CAPACITY_SCALE],用於限制 per-group 值和 per-task 設置的值,使之不超過全局設置。默認前兩個文件都是 1024,都是1024就表示不限制,因為原生的uclamp是單向往低處拉的(但MTK在cgroup層級改了這個邏輯,使util在cgroup的限制層級中落在區間內),比如 uclamp[400 800],當util是900的時候,clamp后是800,當util是300的時候,clamp后還是300,而不會限制在400-800之間。MTK和Qcom的BSP都是,第三個文件是0(5.4中還沒有,5.10中有),對RT任務只有一個 min_clamp 的限制。
這三個文件對應同一個處理函數:
int sysctl_sched_uclamp_handler(struct ctl_table *table, int write, void *buffer, size_t *lenp, loff_t *ppos) //sched/core.c { ... //如果修改了 sched_util_clamp_min 文件 if (old_min != sysctl_sched_uclamp_util_min) { uclamp_se_set(&uclamp_default[UCLAMP_MIN], sysctl_sched_uclamp_util_min, false); //此設置路徑userdefined傳false update_root_tg = true; } //如果修改了 sched_util_clamp_max 文件 if (old_max != sysctl_sched_uclamp_util_max) { uclamp_se_set(&uclamp_default[UCLAMP_MAX], sysctl_sched_uclamp_util_max, false); //此設置路徑userdefined傳false update_root_tg = true; } //若是修改了上面兩個文件中的任何一個文件就執行 if (update_root_tg) { //在 enqueue/dequeue 時用於計算CPU的clamp值時進行判斷 static_branch_enable(&sched_uclamp_used); //設置到全局變量 root_task_group :: uclamp_req 成員中 uclamp_update_root_tg(); } //如果修改了 sched_util_clamp_min_rt_default 文件 if (old_min_rt != sysctl_sched_uclamp_util_min_rt_default) { static_branch_enable(&sched_uclamp_used); //設置到系統中所有RT任務的 p->uclamp_req[UCLAMP_MIN] 中 uclamp_sync_util_min_rt_default(); } ... }
通過 sched_util_clamp_min/sched_util_clamp_max 文件設置的值會設置到全局變量 uclamp_default[] 中,同時還會設置到全局變量 root_task_group::uclamp_req[]中。
通過 sched_util_clamp_min_rt_default 文件設置的值會設置到所有RT任務的 p->uclamp_req[UCLAMP_MIN] 中,它會在RT任務選核中起作用。
2. 基於 cgroup 的 per-group API
/dev/cpuctl/<group>/cpu.uclamp.min /dev/cpuctl/<group>/cpu.uclamp.max /dev/cpuctl/<group>/cpu.uclamp.latency_sensitive
基於 cgroup 實現的 per-group 值,該設置值會限制組內各個任務的 per-task 的值。cpu.uclamp.min/cpu.uclamp.max 取值范圍 0.00 - 100.00,格式為兩位小數精度的百分比值,比如設置echo 40 > min 就表示clamp min為 40% * 1024 = 409.6,后者還可以直接echo max,表示100.00。cpu.uclamp.latency_sensitive 只能設置0或1,在CFS選核中起作用,見下文。
注意根目錄 /dev/cpuctl/ 下是沒有uclamp相關接口文件的,應該是放在上面 profs 文件接口給設置了,見對 root_task_group 的設置。
(1) cpu.uclamp.min/cpu.uclamp.max 的響應接口:
static struct cftype cpu_legacy_files[] = { //sched/core.c ... { .name = "uclamp.min", .flags = CFTYPE_NOT_ON_ROOT, .seq_show = cpu_uclamp_min_show, .write = cpu_uclamp_min_write, }, { .name = "uclamp.max", .flags = CFTYPE_NOT_ON_ROOT, .seq_show = cpu_uclamp_max_show, .write = cpu_uclamp_max_write, }, { .name = "uclamp.latency_sensitive", .flags = CFTYPE_NOT_ON_ROOT, .read_u64 = cpu_uclamp_ls_read_u64, .write_u64 = cpu_uclamp_ls_write_u64, }, ... }
先看write接口:
//cpu.uclamp.min/cpu.uclamp.max 寫時對應函數,clamp_id 分別傳參 UCLAMP_MIN/UCLAMP_MAX static ssize_t cpu_uclamp_write(struct kernfs_open_file *of, char *buf, size_t nbytes, loff_t off, enum uclamp_id clamp_id) //sched/core.c { struct uclamp_request req; struct task_group *tg; req = capacity_from_percent(buf); if (req.ret) return req.ret; //同樣使能static branch static_branch_enable(&sched_uclamp_used); mutex_lock(&uclamp_mutex); rcu_read_lock(); tg = css_tg(of_css(of)); if (tg->uclamp_req[clamp_id].value != req.util) //設置到 tg->uclamp_req[]中,此設置路徑userdefined傳false uclamp_se_set(&tg->uclamp_req[clamp_id], req.util, false); /* 由於不可恢復的轉換舍入,我們會跟蹤確切的請求值 */ tg->uclamp_pct[clamp_id] = req.percent; //注意是percent值 /* Update effective clamps to track the most restrictive value */ cpu_util_update_eff(of_css(of)); rcu_read_unlock(); mutex_unlock(&uclamp_mutex); return nbytes; } static inline void uclamp_se_set(struct uclamp_se *uc_se, unsigned int value, bool user_defined) //sched/core.c { uc_se->value = value; uc_se->bucket_id = uclamp_bucket_id(value); uc_se->user_defined = user_defined; } static inline unsigned int uclamp_bucket_id(unsigned int clamp_value) { /* 默認分20個桶,將1024均分到[0, 19], 兩個宏分別為 51, 20 */ return min_t(unsigned int, clamp_value / UCLAMP_BUCKET_DELTA, UCLAMP_BUCKETS - 1); } cpu.uclamp.min/cpu.uclamp.max 文件 cat 時,若 tg->uclamp_req[clamp_id].value == SCHED_CAPACITY_SCALE,打印“max”,否則打印的是 task_group::uclamp_pct[clamp_id] 這 個是個百分比值。 static inline void cpu_uclamp_print(struct seq_file *sf, enum uclamp_id clamp_id) //sched/core.c { struct task_group *tg; u64 util_clamp; u64 percent; u32 rem; rcu_read_lock(); tg = css_tg(seq_css(sf)); util_clamp = tg->uclamp_req[clamp_id].value; rcu_read_unlock(); if (util_clamp == SCHED_CAPACITY_SCALE) { seq_puts(sf, "max\n"); return; } percent = tg->uclamp_pct[clamp_id]; percent = div_u64_rem(percent, POW10(UCLAMP_PERCENT_SHIFT), &rem); seq_printf(sf, "%llu.%0*u\n", percent, UCLAMP_PERCENT_SHIFT, rem); }
需要使能 CONFIG_UCLAMP_TASK_GROUP 才有效。
(2) cpu.uclamp.latency_sensitive 響應接口
static int cpu_uclamp_ls_write_u64(struct cgroup_subsys_state *css, struct cftype *cftype, u64 ls) //sched/core.c { struct task_group *tg; if (ls > 1) return -EINVAL; tg = css_tg(css); tg->latency_sensitive = (unsigned int) ls; return 0; } static u64 cpu_uclamp_ls_read_u64(struct cgroup_subsys_state *css, struct cftype *cft) //sched/core.c { struct task_group *tg = css_tg(css); return (u64) tg->latency_sensitive; }
直接賦值給 task_group::latency_sensitive,此成員的使用位置:
位置1:
在為 CFS 任務選核時,在 find_energy_efficient_cpu() 中若判斷任務所在組設置了 latency_sensitive 屬性,趨向於選擇idle cpu,若是沒有idle cpu可選,就算空余算力最大的CPU;若是沒有設置 latency_sensitive 屬性,那就趨向於選擇最節能的CPU。
位置2:
MTK 在 find_energy_efficient_cpu() 中注冊了 hook,函數為 mtk_find_energy_efficient_cpu() 若是 hook 中選到核了,位置1就不再生效了。此 hook 的邏輯為若是 perf_ioctl 沒有設置 uclamp_min_ls,那么 latency_sensitive 的取值和原生的一致,否則取值為 p->uclamp_req[UCLAMP_MIN].value > 0 ? 1 : 0,若是判斷為 latency_sensitive 的,就優先選擇idle 核,若沒有 idle 核就依次選最空閑的具有最大算力的核和有最大空余算力的核,判讀時考慮了從idle退出的延遲。
僅從cgroup上,目前有MTK只有神經網絡相關的binder線程單獨創建了一個group,設置了這個 latency_sensitive 屬性。
3. 基於 systemcall 的 per-task API
#include <sched.h> int sched_setattr(pid_t pid, struct sched_attr *attr, unsigned int flags); int sched_getattr(pid_t pid, struct sched_attr *attr, unsigned int size, unsigned int flags); //早期內核版本是不支持的,里面沒有util成員 struct sched_attr { __u32 size; __u32 sched_policy; __u64 sched_flags; /* SCHED_NORMAL, SCHED_BATCH */ __s32 sched_nice; /* SCHED_FIFO, SCHED_RR */ __u32 sched_priority; /* SCHED_DEADLINE */ __u64 sched_runtime; __u64 sched_deadline; __u64 sched_period; /* Utilization hints 應該是uclamp的設置*/ __u32 sched_util_min; __u32 sched_util_max; };
通過 sched_setattr 系統調用,每個任務都可以自主的設置各自的 uclamp_min 和 uclamp_max,以滿足自身的性能/功耗需求,但是該設置值會受制於 cgroup 設置值和系統全局設置的值,取值范圍 0 - SCHED_CAPACITY_SCALE。
系統調用響應函數:
SYSCALL_DEFINE3(sched_setattr, pid_t, pid, struct sched_attr __user *, uattr, unsigned int, flags) //kernel/sched/core.c { struct sched_attr attr; struct task_struct *p; int retval; if (!uattr || pid < 0 || flags) //flags只能傳0 return -EINVAL; retval = sched_copy_attr(uattr, &attr); if ((int)attr.sched_policy < 0) return -EINVAL; if (attr.sched_flags & SCHED_FLAG_KEEP_POLICY) attr.sched_policy = SETPARAM_POLICY; p = find_process_by_pid(pid); if (likely(p)) { if (attr.sched_flags & SCHED_FLAG_KEEP_PARAMS) get_params(p, &attr); //RT只賦值 sched_priority,CFS只賦值 sched_nice retval = sched_setattr(p, &attr); //這里設置 put_task_struct(p); } return retval; } int sched_setattr(struct task_struct *p, const struct sched_attr *attr) { return __sched_setscheduler(p, attr, true, true); } EXPORT_SYMBOL_GPL(sched_setattr); int sched_setattr_nocheck(struct task_struct *p, const struct sched_attr *attr) { return __sched_setscheduler(p, attr, false, true); } EXPORT_SYMBOL_GPL(sched_setattr_nocheck); static int __sched_setscheduler(struct task_struct *p, const struct sched_attr *attr, bool user, bool pi) //sched/core.c { ... //用戶空間設置需要 capable(CAP_SYS_NICE) 權限 if (user && !capable(CAP_SYS_NICE)) { if (fair_policy(policy)) { if (attr->sched_nice < task_nice(p) && !can_nice(p, attr->sched_nice)) return -EPERM; } //檢查目標進程是否具有與當前進程的 UID 匹配的 UID /* Can't change other user's priorities: */ if (!check_same_owner(p)) return -EPERM; //此版本內核不支持用戶空間 uclamp /* Can't change util-clamps */ if (attr->sched_flags & SCHED_FLAG_UTIL_CLAMP) return -EPERM; } //但是內核空間的 uclamp 是支持的 /* Update task specific "requested" clamps */ if (attr->sched_flags & SCHED_FLAG_UTIL_CLAMP) { //執行 //這個函數中調用了 static_branch_enable(&sched_uclamp_used); retval = uclamp_validate(p, attr); } //在設置前會先將任務dequeue,設置完后再enqueue __setscheduler(rq, p, attr, pi); //其它屬性設置 __setscheduler_uclamp(p, attr); //uclamp位置 }
結論:目前5.10版本的Linux內核還不支持用戶空間通過 sched_setattr 系統調用設置 uclamp 屬性。但是內核空間的設置是支持的,內核空間使用 sched_setattr_nocheck() 進行設置,最終會設置到 p->uclamp_req[] 中,如何使用參考:
static int sched_uclamp_set(struct task_struct *p, int util_min, int util_max, bool reset) { struct sched_attr attr = {}; attr.sched_policy = -1; attr.sched_flags = SCHED_FLAG_KEEP_ALL | SCHED_FLAG_UTIL_CLAMP | SCHED_FLAG_RESET_ON_FORK; if (reset) { attr.sched_util_min = -1; attr.sched_util_max = -1; } else { attr.sched_util_min = util_min; attr.sched_util_max = util_max; } return sched_setattr_nocheck(p, &attr); }
sched_setattr_nocheck 最終會調用到 __setscheduler_uclamp:
static void __setscheduler_uclamp(struct task_struct *p, const struct sched_attr *attr) { enum uclamp_id clamp_id; //執行reset的設置 for_each_clamp_id(clamp_id) { struct uclamp_se *uc_se = &p->uclamp_req[clamp_id]; unsigned int value; //對MIN或MAX不是reset繼續 if (!uclamp_reset(attr, clamp_id, uc_se)) continue; /* * RT by default have a 100% boost value that could be modified at runtime. */ if (unlikely(rt_task(p) && clamp_id == UCLAMP_MIN)) //RT任務reset后uclamp[MIN]取 sched_util_clamp_min_rt_default 的值 value = sysctl_sched_uclamp_util_min_rt_default; else //非RT任務reset到沒有clamp的狀態,MIN就是0,MAX就是1024 value = uclamp_none(clamp_id); //若是reset的,user_defined 傳 false uclamp_se_set(uc_se, value, false); } if (likely(!(attr->sched_flags & SCHED_FLAG_UTIL_CLAMP))) return; //下面是真正的設置,只有內核中per-task的設置user_defined才傳true if (attr->sched_flags & SCHED_FLAG_UTIL_CLAMP_MIN && attr->sched_util_min != -1) { uclamp_se_set(&p->uclamp_req[UCLAMP_MIN], attr->sched_util_min, true); //沒有使用 trace_android_vh_setscheduler_uclamp(p, UCLAMP_MIN, attr->sched_util_min); } if (attr->sched_flags & SCHED_FLAG_UTIL_CLAMP_MAX && attr->sched_util_max != -1) { uclamp_se_set(&p->uclamp_req[UCLAMP_MAX], attr->sched_util_max, true); //沒有使用 trace_android_vh_setscheduler_uclamp(p, UCLAMP_MAX, attr->sched_util_max); } }
cat /proc/<pid>/sched 可以看 uclamp 值和 effective uclamp 值(就是此時任務在rq上是對其util的限制范圍)
# cat /proc/<pid>/sched se.avg.util_avg : 49 //util值 se.avg.util_est.ewma : 42 //util_est值 se.avg.util_est.enqueued : 19 uclamp.min : 163 //取自p->uclamp_req[UCLAMP_MIN].value,是內核接口sched_setattr_nocheck()請求的設置值 uclamp.max : 180 effective uclamp.min : 163 //uclamp_eff_value()返回的值,是受全局和per-cgroup設置值限制后的值 effective uclamp.max : 180
proc_sched_show_task() //debug.c { __PS("uclamp.min", p->uclamp_req[UCLAMP_MIN].value); __PS("uclamp.max", p->uclamp_req[UCLAMP_MAX].value); __PS("effective uclamp.min", uclamp_eff_value(p, UCLAMP_MIN)); //這個是被全局限制的 __PS("effective uclamp.max", uclamp_eff_value(p, UCLAMP_MAX)); }
4. 設置總結
(1) 全局的設置會設置到全局數組 uclamp_default[] 和 全局數組 root_task_group :: uclamp_req[] 里面,user_defined 為fasle; per-cgroup的設置會設置到 task_group::uclamp_req[] 里面,user_defined 為fasle; per-task的設置5.10內核還不支持用戶空間對uclamp的設置,內核中的設置會設置到 task_struct::uclamp_req[] 中,user_defined 為true。
(2) 三個設置路徑在設置后都執行了 static_branch_enable(&sched_uclamp_used),使用位置是 enqueue_task/enqueue_task 位置,若是三個路徑都沒有設置過,那么 sched_uclamp_used 就為 False,功能等於沒有被啟動。
五、相關函數分析
1. 相關數據結構
//per-task: struct uclamp_se { //uclamp_max/min對應的值 unsigned int value : bits_per(SCHED_CAPACITY_SCALE); //11bit //uclamp_max/min對應的值落在那個桶的id unsigned int bucket_id : bits_per(UCLAMP_BUCKETS); //5bit //enqueue此task時寫1,dequeue此task時寫0,標記此se是否在rq隊列上 unsigned int active : 1; //system-wide和per-cgroup的設置為0,內核per-task的設置為1 unsigned int user_defined : 1; }; //per-rq: struct uclamp_rq { unsigned int value; struct uclamp_bucket bucket[UCLAMP_BUCKETS]; //20 }; struct uclamp_bucket { //bucket::value始終是此bucket中所有task 生效clamp(就是enqueue到rq上的)的最大值。 unsigned long value : bits_per(SCHED_CAPACITY_SCALE); //11 //有多少個task位於桶內 unsigned long tasks : BITS_PER_LONG - bits_per(SCHED_CAPACITY_SCALE);//64-11=53 }; struct task_struct { ... /* * 請求的clamp值保存從內核空間 sched_setattr()設置的值, * 目前用戶空間的設置還不支持 */ struct uclamp_se uclamp_req[UCLAMP_CNT]; //2 /* * Effective clamp values used for a scheduling entity. * Must be updated with task_rq_lock() held. * * uclamp_rq_inc_id: 中更新,保存的 uclamp_eff_get()的返回值, * 即是受system-wide和per-cgroup uclmap設置對per-task設置值 * MIN/MAX的最大值進行限制后的值。 */ struct uclamp_se uclamp[UCLAMP_CNT]; }; struct task_group { ... /* The two decimal precision [%] value requested from user-space */ unsigned int uclamp_pct[UCLAMP_CNT]; /* Clamp values requested for a task group */ struct uclamp_se uclamp_req[UCLAMP_CNT]; /* Effective clamp values used for a task group */ struct uclamp_se uclamp[UCLAMP_CNT]; /* Latency-sensitive flag used for a task group */ unsigned int latency_sensitive; }; struct rq { ... struct uclamp_rq uclamp[UCLAMP_CNT] ____cacheline_aligned; unsigned int uclamp_flags; #define UCLAMP_FLAG_IDLE 0x01 ... }
2. 對CPU的clamp的設置位置:
enqueue_task
uclamp_rq_inc
dequeue_task
uclamp_rq_dec
(1) 先來看 uclamp_rq_inc 函數:
static inline void uclamp_rq_inc(struct rq *rq, struct task_struct *p) { enum uclamp_id clamp_id; /* * Avoid any overhead until uclamp is actually used by the userspace. * * The condition is constructed such that a NOP is generated when * sched_uclamp_used is disabled. */ //若是沒有通過接口設置uclamp,這里直接就返回了 if (!static_branch_unlikely(&sched_uclamp_used)) return; //CFS和RT調度類的是靜態使能的 if (unlikely(!p->sched_class->uclamp_enabled)) return; //只有MIN和MAX for_each_clamp_id(clamp_id) uclamp_rq_inc_id(rq, p, clamp_id); /* Reset clamp idle holding when there is one RUNNABLE task */ //清除idle標志 if (rq->uclamp_flags & UCLAMP_FLAG_IDLE) rq->uclamp_flags &= ~UCLAMP_FLAG_IDLE; } /*enqueue_task--> 入隊時規划task到rq bucket中, 並更新uclamp_rq::value*/ static inline void uclamp_rq_inc_id(struct rq *rq, struct task_struct *p, enum uclamp_id clamp_id) { struct uclamp_rq *uc_rq = &rq->uclamp[clamp_id]; struct uclamp_se *uc_se = &p->uclamp[clamp_id]; struct uclamp_bucket *bucket; lockdep_assert_held(&rq->lock); /* Update task effective clamp */ //這里返回的不一定是 p->uclamp[clamp_id] 了,而是被限制后的,可能是per-cgroup的也可能是system-wide的 p->uclamp[clamp_id] = uclamp_eff_get(p, clamp_id); bucket = &uc_rq->bucket[uc_se->bucket_id]; bucket->tasks++; uc_se->active = true; /* * 若之前是idle,執行復位直接將 uc_se->value 設置到 uc_rq->value 中, * 復位后繼續往下執行,uc_rq->value 仍然是最大值。 */ uclamp_idle_reset(rq, clamp_id, uc_se->value); /* * Local max aggregation: rq buckets always track the max * "requested" clamp value of its RUNNABLE tasks. * 本地最大聚合:rq 存儲桶始終跟蹤其 RUNNABLE 任務的最大“請求”鉗位值。 */ //更新 bucket->value,使其保持為桶內最大 if (bucket->tasks == 1 || uc_se->value > bucket->value) bucket->value = uc_se->value; //更新 uc_rq->value,使其保持為桶內最大 if (uc_se->value > READ_ONCE(uc_rq->value)) WRITE_ONCE(uc_rq->value, uc_se->value); } /* * 獲取任務有效的uclamp設置: * * 看以看出,內核per-task的uclamp設置要受到per-cgroup和system_wide的uclamp設置值的限制,只限制max/min的最大值,不限制最小值。 * * 可見優先級 system_wide > per-cgroup > per-task * * system_wide 可設置為[1024, 1024]以起到不做限制的目的。 */ static inline struct uclamp_se uclamp_eff_get(struct task_struct *p, enum uclamp_id clamp_id) { //uclamp_min/max 的最大值先受到 per-group 的uclamp設置值限制一下 struct uclamp_se uc_req = uclamp_tg_restrict(p, clamp_id); //唯一使用 uclamp_default[]的位置 struct uclamp_se uc_max = uclamp_default[clamp_id]; struct uclamp_se uc_eff; int ret = 0; trace_android_rvh_uclamp_eff_get(p, clamp_id, &uc_max, &uc_eff, &ret); //mtk_uclamp_eff_get if (ret) return uc_eff; //uclamp_min/max 的最大值再受system-wide的uclamp設置值再限制一次 /* System default restrictions always apply */ if (unlikely(uc_req.value > uc_max.value)) return uc_max; return uc_req; }
要想達到不被 system-wide 這個全局的設置影響,將 sched_util_clamp_min 和 sched_util_clamp_max 文件都設置為最大 1024 即可。因為大小兩端都是大於全局的設置往低處拉。
/* * per-task的設置MIN/MAX的最大值受到per-cgroup設置值的限制 */ static inline struct uclamp_se uclamp_tg_restrict(struct task_struct *p, enum uclamp_id clamp_id) { struct uclamp_se uc_req = p->uclamp_req[clamp_id]; #ifdef CONFIG_UCLAMP_TASK_GROUP struct uclamp_se uc_max; /* * Tasks in autogroups or root task group will be * restricted by system defaults. */ if (task_group_is_autogroup(task_group(p))) return uc_req; //沒有cgroup分組的任務返回自己的uclamp值 if (task_group(p) == &root_task_group) return uc_req; //獲取任務所在group的uclamp值 uc_max = task_group(p)->uclamp[clamp_id]; /* * 若 task 的 req 是非 user_defined,也就是說是per-cgroup和 * system-wide設置下來的,直接返回group的uclamp; * 若是user_defined為true,也就是內核per-task的設置,uclamp_min/ * uclamp_max的最大值則要受到per-group clamp的限制。 */ if (uc_req.value > uc_max.value || !uc_req.user_defined) return uc_max; #endif return uc_req; } static inline void uclamp_idle_reset(struct rq *rq, enum uclamp_id clamp_id, unsigned int clamp_value) { /* Reset max-clamp retention only on idle exit */ if (!(rq->uclamp_flags & UCLAMP_FLAG_IDLE)) return; WRITE_ONCE(rq->uclamp[clamp_id].value, clamp_value); }
注:Kernel-5.10中 uclamp_tg_restrict()有變化,是直接clamp(value, tg_min, tg_max),cgroup不再是對per-task的min和max邊界都向下拉了。上面備注是Google原生內核的,但是MTK加了一個hook,mtk_uclamp_eff_get():
//uc_eff 是 uclamp_eff_get()的返回值,ret是uclamp_eff_get()判斷是否繼續往下執行的 void mtk_uclamp_eff_get(void *data, struct task_struct *p, enum uclamp_id clamp_id, struct uclamp_se *uc_max, struct uclamp_se *uc_eff, int *ret) { struct uclamp_se group_uclamp = task_group(p)->uclamp[clamp_id]; *uc_eff = p->uclamp_req[clamp_id]; if (task_group_is_autogroup(task_group(p))) goto sys_restriction; //若是沒有分組,保持邏輯不變 if (task_group(p) == &root_task_group) goto sys_restriction; //若是任務p在某個cgroup中 switch (clamp_id) { case UCLAMP_MIN: if (uc_eff->value < group_uclamp.value) //MIN值的最小值受per-cgroup設置值的限制 *uc_eff = group_uclamp; break; case UCLAMP_MAX: if (uc_eff->value > group_uclamp.value) //MAX值的最大值都per-cgroup設置值的限制 *uc_eff = group_uclamp; break; default: WARN_ON_ONCE(1); break; } sys_restriction: if (uc_eff->value > uc_max->value) //最后MAX/MIN的最大值再受system-wide設置值的限制 *uc_eff = *uc_max; *ret = 1; }
執行 hook 后 uclamp_eff_get() 的邏輯變化的部分為:任務p若是在某個cgroup中,其MIN的最小值也要受到per-cgroup設置值的限制,利於性能。
(2) 再看 uclamp_rq_dec 函數:
static inline void uclamp_rq_dec(struct rq *rq, struct task_struct *p) { enum uclamp_id clamp_id; /* * Avoid any overhead until uclamp is actually used by the userspace. * * The condition is constructed such that a NOP is generated when * sched_uclamp_used is disabled. */ //若是沒有通過接口設置uclamp,這里直接就返回了。 if (!static_branch_unlikely(&sched_uclamp_used)) return; //CFS和RT調度類的是靜態使能的 if (unlikely(!p->sched_class->uclamp_enabled)) return; //只有MIN/MAX for_each_clamp_id(clamp_id) uclamp_rq_dec_id(rq, p, clamp_id); } /* * 當任務從 rq 中dequeue時,任務引用的鉗位桶被釋放。如果這是對 rq 的最大active * 鉗位值的最后一個任務的引用,則更新 rq 的鉗位值。 * * 被引用計數的任務和 rq 的緩存鉗位值都應始終有效。如果檢測到它們無效,則作為防 * 御性編程,強制執行預期狀態並發出警告。 */ static inline void uclamp_rq_dec_id(struct rq *rq, struct task_struct *p, enum uclamp_id clamp_id) { struct uclamp_rq *uc_rq = &rq->uclamp[clamp_id]; struct uclamp_se *uc_se = &p->uclamp[clamp_id]; struct uclamp_bucket *bucket; unsigned int bkt_clamp; unsigned int rq_clamp; lockdep_assert_held(&rq->lock); //主要擔心enqueue后才使能的sched_uclamp_used,不是active的,說明enqueue時沒有對其調用inc,所以這里不用dec if (unlikely(!uc_se->active)) return; bucket = &uc_rq->bucket[uc_se->bucket_id]; SCHED_WARN_ON(!bucket->tasks); if (likely(bucket->tasks)) bucket->tasks--; uc_se->active = false; /* * 保持“本地最大聚合”簡單並接受(可能)在同一存儲桶中過度提升一些 * RUNNABLE 任務。 只要不再有 RUNNABLE 任務對其進行引用計數,rq 鉗 * 位桶值就會重置為其基值。 */ /* * 若rq上還有task,是不會更新 uc_rq->value 的,即使最大的util減去了, * 也不更新,對功耗的影響待評估!########## */ if (likely(bucket->tasks)) return; /* 下面對應這個 bucket 中沒有任務的情況 */ rq_clamp = READ_ONCE(uc_rq->value); //Defensive programming: 這永遠都不應該發生 SCHED_WARN_ON(bucket->value > rq_clamp); //大於不會出現,但是等於可能會出現 if (bucket->value >= rq_clamp) { bkt_clamp = uclamp_rq_max_value(rq, clamp_id, uc_se->value); WRITE_ONCE(uc_rq->value, bkt_clamp); } } static inline unsigned int uclamp_rq_max_value(struct rq *rq, enum uclamp_id clamp_id, unsigned int clamp_value) { struct uclamp_bucket *bucket = rq->uclamp[clamp_id].bucket; int bucket_id = UCLAMP_BUCKETS - 1; /* * Since both min and max clamps are max aggregated, find the * top most bucket with tasks in. * 由於最小和最大鉗位都是最大聚合的,因此找到包含任務的最上面的桶。 */ for ( ; bucket_id >= 0; bucket_id--) { if (!bucket[bucket_id].tasks) continue; return bucket[bucket_id].value; } /* No tasks -- default clamp values */ //一個任務都沒有了,就返回idle時對應的clamp值 return uclamp_idle_value(rq, clamp_id, clamp_value); } static inline unsigned int uclamp_idle_value(struct rq *rq, enum uclamp_id clamp_id, unsigned int clamp_value) { /* * Avoid blocked utilization pushing up the frequency when we go * idle (which drops the max-clamp) by retaining the last known max-clamp. * 通過保持上次的 max-clamp 來避免當進入idle時頻點降低。 */ if (clamp_id == UCLAMP_MAX) { rq->uclamp_flags |= UCLAMP_FLAG_IDLE; return clamp_value; } //MIN: 0 return uclamp_none(UCLAMP_MIN); }
看來這個 UCLAMP_FLAG_IDLE 就是當 rq->uclamp[clamp_id] 中所有的 bucket 中都沒有 runnable 任務時設置,然后將最后一個 dequeue 的任務的 p->uclamp[MAX].value 賦值給 uc_rq->value 以避免頻點降的過低。
這是 core.c 中的代碼,並且沒有區分CFS線程還是RT線程。
(3) rq->uclamp_flags 之 UCLAMP_FLAG_IDLE 使用邏輯
除了上面列出的函數有對idle標志操作外,還有下面這個函數:
static inline void uclamp_rq_reinc_id(struct rq *rq, struct task_struct *p, enum uclamp_id clamp_id) { //不在隊列上非runnable任務不更新 if (!p->uclamp[clamp_id].active) return; uclamp_rq_dec_id(rq, p, clamp_id); uclamp_rq_inc_id(rq, p, clamp_id); /* * Make sure to clear the idle flag if we've transiently reached 0 active tasks on rq. * 如果我們在 rq 上暫時達到 0 個活動任務,請確保清除空閑標志 ? */ if (clamp_id == UCLAMP_MAX && (rq->uclamp_flags & UCLAMP_FLAG_IDLE)) rq->uclamp_flags &= ~UCLAMP_FLAG_IDLE; }
其被 uclamp_update_active() 唯一調用:
static inline void uclamp_update_active(struct task_struct *p, enum uclamp_id clamp_id) { struct rq_flags rf; struct rq *rq; rq = task_rq_lock(p, &rf); uclamp_rq_reinc_id(rq, p, clamp_id); task_rq_unlock(rq, p, &rf); }
調用路徑:
/proc/sys/kernel/sched_util_clamp_min //system-wide接口 /proc/sys/kernel/sched_util_clamp_max //system-wide接口 sysctl_sched_uclamp_handler uclamp_update_root_tg //core.c cgroup_mkdir //cgroup.c 應該是 mkdir創建一個cgroup分組時調用 cgroup_apply_control //cgroup.c cgroup_apply_control_enable css_create online_css //cgroup.c cpu_cgrp_subsys.css_online //回調 cpu_cgroup_css_online //core.c //cpu.uclamp.min 文件接口 cpu_uclamp_min_write //core.c //cpu.uclamp.max 文件接口 cpu_uclamp_max_write //core.c cpu_uclamp_write //core.c cpu_util_update_eff uclamp_update_active_tasks //遍歷cgroup所有任務進行設置 uclamp_update_active uclamp_rq_reinc_id
總結:由上面貼出的函數可知,當 rq->uclamp[clamp_id] 中所有的 bucket 中都沒有 runnable 任務時設置將最后一個 dequeue 的任務的 p->uclamp[MAX].value 賦值給 uc_rq->value 以避免頻點降的過低。當新插入一個 runnable 任務時,在 uclamp_idle_reset() 中將 rq->uclamp[clamp_id].value 賦值為任務 uclamp_eff_get(p, clamp_id) 限制后的值,然后在 uclamp_rq_inc() 中清除 idle 標志。
3. 將 p->uclamp_req[] 更新到 p->uclamp[] 的時機
enqueue_task --> uclamp_rq_inc --> uclamp_rq_inc_id() 中更新為 uclamp_eff_get(p, clamp_id) 的返回值,原生邏輯即是受system-wide和per-cgroup uclmap設置對per-task設置值MIN/MAX的最大值進行限制后的值。
4. p->uclamp[] 起作用分析
(1) 在任務入隊列和出隊列時,在 uclamp_rq_inc_id()/uclamp_rq_dec_id() 中更新,然后用來去更新 rq->uclamp[clamp_id].bucket[] 和 rq->uclamp[clamp_id].value。
(2) 在 uclamp_task_util() 和 task_fits_capacity() 中使用
unsigned long uclamp_eff_value(struct task_struct *p, enum uclamp_id clamp_id) { struct uclamp_se uc_eff; /* Task currently refcounted: use back-annotated (effective) value */ if (p->uclamp[clamp_id].active) return (unsigned long)p->uclamp[clamp_id].value; uc_eff = uclamp_eff_get(p, clamp_id); //返回全局和cgroup限制MIN/MAX最大值后的值 return (unsigned long)uc_eff.value; } static inline unsigned long uclamp_task_util(struct task_struct *p) //fair.c { //返回的是 uclamp 限制后的 util_est return clamp(task_util_est(p), uclamp_eff_value(p, UCLAMP_MIN), uclamp_eff_value(p, UCLAMP_MAX)); } //對任務選核有影響,快速路徑中判斷了 static inline int task_fits_capacity(struct task_struct *p, long capacity) //fair.c { return fits_capacity(uclamp_task_util(p), capacity); //使用的是clamp后的util }
uclamp_task_util() 會在選核的快速路徑中的 select_idle_capacity() select_idle_sibling()使用,主要是對喚醒任務選核的快速路徑。
task_fits_capacity() 則會在 update_misfit_status() find_energy_efficient_cpu() detach_tasks() update_sg_wakeup_stats() 中使用。
(3) rt_task_fits_capacity() 中使用
static inline bool rt_task_fits_capacity(struct task_struct *p, int cpu) { unsigned int min_cap; unsigned int max_cap; unsigned int cpu_cap; /* Only heterogeneous systems can benefit from this check */ if (!static_branch_unlikely(&sched_asym_cpucapacity)) return true; min_cap = uclamp_eff_value(p, UCLAMP_MIN); max_cap = uclamp_eff_value(p, UCLAMP_MAX); cpu_cap = capacity_orig_of(cpu); return cpu_cap >= min(min_cap, max_cap); }
在rt任務選核任務中起作用,kernel-5.10 中rt任務也考慮了算力。
總結起來就是對任務的util進行uclmap影響其rq的uclamp值、選核、負載均衡。
5. rq->uclamp[clamp_id] 更新邏輯分析
per-rq 的 uclamp_rq 結構有 value 和 bucket 兩個成員,在 enqueue_task 、dequeue_task 中更新,並始終保持為其rq隊列上任務的effective uclamp值的最大值。
6. rq->uclamp[clamp_id] 起作用分析
(1) 設置opp時起作用
mtk_set_cpu_min_opp_single
mtk_set_cpu_min_opp_shared
(2) 調單CPU頻率時起作用,rq->uclamp[clamp_id].value 會影響CPU頻點。
sugov_update_single
sugov_next_freq_shared
mtk_uclamp_rq_util_with
(3) 為CFS任務選核時,EAS選核路徑中跳過算力不足的cpu(hook中同邏輯),有利於為任務向上選核。
find_energy_efficient_cpu //fair.c util = uclamp_rq_util_with(cpu_rq(cpu), util, p); if (!fits_capacity(util, cpu_cap)) continue;
六、相關DEBUG方法
1. trace sched_task_uclamp
update_load_avg //fair.c propagate_entity_load_avg //fair.c set_task_rq_fair //fair.c 為任務選核時更新任務負載 sync_entity_load_avg //fair.c __update_load_avg_blocked_se //pelt.c update_load_avg //fair.c __update_load_avg_se //pelt.c trace_pelt_se_tp sched_task_uclamp_hook //sched_main.c trace_sched_task_uclamp(p->pid, util, p->uclamp[UCLAMP_MIN].active, p->uclamp[UCLAMP_MIN].value, p->uclamp[UCLAMP_MAX].value, uc_min_req->user_defined, uc_min_req->value, uc_max_req->user_defined, uc_max_req->value);
更新任務負載時進行trace,trace打印內容見下文。
2. trace sched_queue_task
dequeue_task //core.c enqueue_task //core.c sched_queue_task_hook trace_sched_queue_task(cpu, p->pid, type, util, rq->uclamp[UCLAMP_MIN].value, rq->uclamp[UCLAMP_MAX].value, p->uclamp[UCLAMP_MIN].value, p->uclamp[UCLAMP_MAX].value);
任務enqueue/dequeue時 trace 任務的 uclamp 值。
3. trace sugov_ext_util
cpufreq_update_util sugov_next_freq_shared //cpufreq_sugov_main.c sugov_update_single trace_sugov_ext_util(sg_cpu->cpu, util, umin, umax); //umin/umax 為rq->uclamp[UCLAMP_MIN/MAX].value,主要看umin
調頻路徑中 trace rq 的 uclamp 值。
七、測試
1. 優先級限制生效測試
# echo 50 > /dev/cpuctl/top-app/cpu.uclamp.min //50%為512, # cat /proc/<pid>/sched uclamp.min : 16 uclamp.max : 24 effective uclamp.min : 512 //立即變為512(MTK hook的MIN也向cgroup的MIN進行限制) effective uclamp.max : 24 # echo 100 > /proc/sys/kernel/sched_util_clamp_min //全局最高優先級min限制為100 # cat /proc/<pid>/sched effective uclamp.min : 100 //立即變為100 # echo 800 > /proc/sys/kernel/sched_util_clamp_min # cat /proc/<pid>/sched effective uclamp.min : 512 //重新回到512
2. 查看trace
(1) trace_sched_task_uclamp
trace_sched_task_uclamp(p->pid, util, p->uclamp[UCLAMP_MIN].active, p->uclamp[UCLAMP_MIN].value, p->uclamp[UCLAMP_MAX].value, p->uclamp_req[UCLAMP_MIN].user_defined, p->uclamp_req[UCLAMP_MIN].value, p->uclamp_req[UCLAMP_MAX].user_defined, p->uclamp_req[UCLAMP_MAX].value); //trace是依次打印每一個參數的值: # echo 50 > /dev/cpuctl/top-app/cpu.uclamp.min //cgroup的clamp_min設50% main-7710 [007] d..3 2159.461272: sched_task_uclamp: pid=7710 util=74 active=0 min=512 max=180 min_ud=1 min_req=163 max_ud=1 max_req=180 # echo 80 > /dev/cpuctl/top-app/cpu.uclamp.min //cgroup的clamp_min設80% main-7710 [007] d.h2 2170.227392: sched_task_uclamp: pid=7710 util=83 active=1 min=819 max=24 min_ud=1 min_req=16 max_ud=1 max_req=24 # echo 20 > /proc/sys/kernel/sched_util_clamp_min //全局的clamp_min設20% main-7710 [006] d..3 2189.762599: sched_task_uclamp: pid=7710 util=89 active=0 min=20 max=24 min_ud=1 min_req=8 max_ud=1 max_req=24
注意:cgroup的限制值和全局限制值是體現在 p->uclamp[] 上,而不是 p->uclamp_req[] 上。
(2) trace_sched_queue_task
trace_sched_queue_task(cpu, p->pid, type, util, rq->uclamp[UCLAMP_MIN].value, rq->uclamp[UCLAMP_MAX].value, p->uclamp[UCLAMP_MIN].value, p->uclamp[UCLAMP_MAX].value);
沒有任何clamp限制時的打印:
kworker/u16:3-25905 [002] d..2 9251.745822: sched_queue_task: cpu=2 pid=25905 enqueue=-1 cfs_util=38 min=0 max=1024 task_min=0 task_max=1024
(3) trace_sugov_ext_util
trace_sugov_ext_util(sg_cpu->cpu, util, umin, umax); trace也是依次打印每一個參數的值: util: 取自 sugov_get_util() //clamp_min取p和rq二者的較大值,clamp_max取p的,返回限制后的結果。 umin: 取自 rq->uclamp[UCLAMP_MIN].value //是其上的所有se的最大的那個值 umax: 取自 rq->uclamp[UCLAMP_MAX].value main-7710 [006] d..7 2159.461842: sugov_ext_util: cpu=5 util=517 min=512 max=180 Binder:904_1-1002 [004] d..3 2189.730267: sugov_ext_util: cpu=6 util=819 min=819 max=24 RenderThread-7905 [006] d..5 2189.765668: sugov_ext_util: cpu=4 util=124 min=20 max=1024