一、WALT簡介
1. WALT(Windows-Assist Load Tracing),從字面意思來看,是以window作為輔助項來跟蹤cpu load,用來表現cpu當前的loading情況,用於后續任務調度、遷移、負載均衡等功能。在 load 的基礎上,添加對於demand的記錄用於之后的預測。只統計runable和running time。
2. WALT由Qcom研發,主要用於移動設備對性能功耗要求比較高的場景,在與用戶交互時需要盡快響應,要能及時反應負載的增加和減少以驅動頻點及時的變化。當前的PELT負載跟蹤算法更主要的是體現負載的連續性,對於突變性質的負載的反應不是很友好,負載上升慢,下降也慢。
3. 打開 CONFIG_SCHED_WALT 使能此feature。
4. 輔助計算項 window 的划分方法是將系統自啟動開始以一定時間作為一個周期,分別統計不同周期內 Task 的 Loading 情況,並將其更新到Runqueue中;目前 Kernel 中是設置的一個 window 的大小是20ms,統計 5 個window內的Loading情況,當然,這也可以根據具體的項目需求進行配置。
二、相關數據結構
(1) 嵌入在 task_struct 中的 walt_task_struct
/* * 'mark_start' 標記窗口內事件的開始(任務喚醒、任務開始執行、任務被搶占) * 'sum' 表示任務在當前窗口內的可運行程度。它包含運行時間和等待時間,並按頻率進行縮放。//就是在當前窗口的運行時間吧 * 'sum_history' 跟蹤在之前的 RAVG_HIST_SIZE 窗口中看到的 'sum' 的歷史記錄。任務完全休眠的窗口將被忽略。 * 'demand' 表示在以前的 sysctl_sched_ravg_hist_size 窗口中看到的最大總和(根據window_policy選的)。 'demand'可以為任務驅動頻率的改變。####### * 'curr_window_cpu' 代表任務對當前窗口各CPU上cpu繁忙時間的貢獻 * 'prev_window_cpu' 表示任務對前一個窗口中各個 CPU 上的 cpu 繁忙時間的貢獻 * 'curr_window' 表示 curr_window_cpu 中所有條目的總和 * 'prev_window' 代表 prev_window_cpu 中所有條目的總和 * 'pred_demand' 代表任務當前預測的cpu繁忙時間 * 'busy_buckets' 將歷史繁忙時間分組到用於預測的不同桶中 * 'demand_scaled' 表示任務的需求縮放到 1024 //就是上面demand成員縮放到1024 */ struct walt_task_struct { u64 mark_start; u32 sum, demand; //sum在 add_to_task_demand 中更新 u32 coloc_demand; //存的是5個歷史窗口的平均值 u32 sum_history[RAVG_HIST_SIZE_MAX]; u32 *curr_window_cpu, *prev_window_cpu; //這個是per-cpu的 u32 curr_window, prev_window; u32 pred_demand; u8 busy_buckets[NUM_BUSY_BUCKETS]; //10個 u16 demand_scaled; u16 pred_demand_scaled; u64 active_time; //is_new_task中判斷此值是小於100ms就認為是新任務,rollover_task_window是唯一更新位置 u32 unfilter; //update_history中對其進行賦值,colocate中選核時,是否需要跳過小核判斷了它 u64 cpu_cycles; ... }
(2) 嵌入在 rq 中的 walt_rq
struct walt_rq { ... struct walt_sched_stats walt_stats; u64 window_start; u32 prev_window_size; u64 task_exec_scale; //walt_sched_init_rq中初始化為1024 u64 curr_runnable_sum; u64 prev_runnable_sum; u64 nt_curr_runnable_sum; u64 nt_prev_runnable_sum; //nt 應該是walt認為的new task的意思 u64 cum_window_demand_scaled; struct group_cpu_time grp_time; /* * #define DECLARE_BITMAP_ARRAY(name, nr, bits) unsigned long name[nr][BITS_TO_LONGS(bits)] * unsigned long top_tasks_bitmap[2][BITS_TO_LONGS(1000)]; //只跟蹤curr和prev兩個窗口的情況。 */ DECLARE_BITMAP_ARRAY(top_tasks_bitmap, NUM_TRACKED_WINDOWS, NUM_LOAD_INDICES); u8 *top_tasks[NUM_TRACKED_WINDOWS]; //2 指針數組 u8 curr_table; //只使用兩個window進行跟蹤,標識哪個是curr的,curr和prev構成一個環形數組,不停翻轉 int prev_top; //應該是rq->wrq.top_tasks[]中前一個窗最大值的下標 int curr_top; //是rq->wrq.top_tasks[]中當前窗最大值的下標 u64 cycles; ... }; struct walt_sched_stats { int nr_big_tasks; u64 cumulative_runnable_avg_scaled; //只統計runnable任務的,在update_window_start中賦值給rq->wrq.cum_window_demand_scaled u64 pred_demands_sum_scaled; unsigned int nr_rtg_high_prio_tasks; };
三、負載計算函數
1. walt算法負載計算入口函數
/* event 取 TASK_UPDATE 等,由於每個tick中斷中都會調度,一般兩次執行統計的 wc-ms 一般不會超過4ms */ void walt_update_task_ravg(struct task_struct *p, struct rq *rq, int event, u64 wallclock, u64 irqtime) //walt.c { u64 old_window_start; /* 還沒初始化或時間沒更新,直接返回 */ if (!rq->wrq.window_start || p->wts.mark_start == wallclock) return; lockdep_assert_held(&rq->lock); /* 更新ws,返回最新的ws */ old_window_start = update_window_start(rq, wallclock, event); /* 對應還沒初始化的情況, ws是per-rq的,ms是per-task的,wc是全局的 */ if (!p->wts.mark_start) { update_task_cpu_cycles(p, cpu_of(rq), wallclock); goto done; } /*更新 rq->wrq.task_exec_scale 和 p->wts.cpu_cycles = cur_cycles; */ update_task_rq_cpu_cycles(p, rq, event, wallclock, irqtime); /*更新任務的負載和歷史記錄,返回 wc-ms 的差值,也就是距離上次統計任務運行的時間值 */ update_task_demand(p, rq, event, wallclock); /*更新任務和rq的window相關統計信息,記錄per-rq的prev和curr兩個窗口內任務負載分布情況 */ update_cpu_busy_time(p, rq, event, wallclock, irqtime); /*更新預測需求*/ update_task_pred_demand(rq, p, event); if (event == PUT_PREV_TASK && p->state) { p->wts.iowaited = p->in_iowait; } trace_sched_update_task_ravg(p, rq, event, wallclock, irqtime, &rq->wrq.grp_time); trace_sched_update_task_ravg_mini(p, rq, event, wallclock, irqtime, &rq->wrq.grp_time); done: /* 更新per-task的 ms,ms是在動態變化的 */ p->wts.mark_start = wallclock; /*構成一個內核線程,每個窗口執行一次*/ run_walt_irq_work(old_window_start, rq); }
此函數中的兩個trace解析:
(1) trace_sched_update_task_ravg(p, rq, event, wallclock, irqtime, &rq->wrq.grp_time);
參數原型:
(struct task_struct *p, struct rq *rq, enum task_event evt, u64 wallclock, u64 irqtime, struct group_cpu_time *cpu_time)
打印內容:
<idle>-0 [004] d..2 50167.767150: sched_update_task_ravg: wc 50167994699141 ws 50167988000001 delta 6699140 event PICK_NEXT_TASK cpu 4 cur_freq 434 cur_pid 0 task 17043 (kworker/u16:5) ms 50167994687631 delta 11510 demand 3340045 coloc_demand: 1315008 sum 1235016 irqtime 0 pred_demand 3340045 rq_cs 1112353 rq_ps 4085339 cur_window 930130 (0 136431 573963 0 219736 0 0 0 ) prev_window 2138941 (222138 156646 973556 0 219811 566790 0 0 ) nt_cs 2513 nt_ps 20395 active_time 100000000 grp_cs 0 grp_ps 1691783, grp_nt_cs 0, grp_nt_ps: 0 curr_top 58 prev_top 133
字段解析:
wc:為參數4 wallclock; ws: 為 window_start,取自 rq->wrq.window_start; delta:取自 wallclock - rq->wrq.window_start 的差值。 event:task_event_names[參數3], 字符串表示的事件類型 cpu:取自 rq->cpu cur_freq:取自 rq->wrq.task_exec_scale,update_task_rq_cpu_cycles()中,若不使用 use_cycle_counter,賦值為 cpu_capaticy * (freq / maxfreq) cur_pid: 取自 rq->curr->pid task:取自參數1 p 的 p->pid kworker/u16:5:取自參數1 p 的 p->comm ms:是 mark_start 取自 p->wts.mark_start delta:打印中有兩個同名的delta,這是第二個,取自 wallclock - p->wts.mark_start demand:取自 p->wts.demand,單位是ns,就是根據 p->wts.sum 取平均或和最近窗口兩者之間的最大值 coloc_demand:取自 p->wts.coloc_demand sum:取自 p->wts.sum,表示最近一個窗口運行時間之和,單位ns,在將其更新到history數組后,清0. irqtime:取自參數4 pred_demand:取自 p->wts.pred_demand rq_cs:取自 rq->wrq.curr_runnable_sum 表示 rq_ps:取自 rq->wrq.prev_runnable_sum 表示 cur_window:取自 p->wts.curr_window,表示任務在當前窗口中所有cpu上的運行時間之和,是后面數組的累加。 (0 136431 573963 0 219736 0 0 0 ):取自 p->wts.curr_window_cpu per-cpu的,表示任務在當前窗口中在每個cpu上運行的時間 prev_window:取自 p->wts.prev_window (222138 156646 973556 0 219811 566790 0 0 ):取自 p->wts.prev_window_cpu 也是per-cpu的,表示任務在前一個窗口中在每個cpu上運行的時間 nt_cs:取自 rq->wrq.nt_curr_runnable_sum nt應該表示的是new task的縮寫 nt_ps:取自 rq->wrq.nt_prev_runnable_sum active_time:取自 p->wts.active_time is_new_task()中判斷它,唯一更新位置rollover_task_window()中調用is_new_task()判斷是新任務時 p->wts.active_time += task_rq(p)->wrq.prev_window_size; grp_cs:取自 cpu_time ? cpu_time->curr_runnable_sum : 0 根據最后一個參數來判斷是更新rq的還是更新rtg group的 grp_ps:取自 cpu_time ? cpu_time->prev_runnable_sum : 0 grp_nt_cs:取自 cpu_time ? cpu_time->nt_curr_runnable_sum : 0 grp_nt_ps:取自 cpu_time ? cpu_time->nt_prev_runnable_sum : 0 curr_top:取自 rq->wrq.curr_top 記錄的是當前窗口中 rq->wrq.top_tasks[]中最大值的下標 prev_top:取自 rq->wrq.prev_top 記錄的是前一個窗口中 rq->wrq.top_tasks[]中最大值的下標
(2) trace_sched_update_task_ravg_mini(p, rq, event, wallclock, irqtime, &rq->wrq.grp_time);
參數原型:
(struct task_struct *p, struct rq *rq, enum task_event evt, u64 wallclock, u64 irqtime, struct group_cpu_time *cpu_time)
打印內容:
<idle>-0 [005] d..2 280546.887141: sched_update_task_ravg_mini: wc 112233604355205 ws 112233596000001 delta 8355204 event PUT_PREV_TASK cpu 5 task 0 (swapper/5) ms 112233604337548 delta 17657 demand 2400000 rq_cs 1374618 rq_ps 1237818 cur_window 0 prev_window 0 grp_cs 0 grp_ps 0
字段解析:
wc:取自參數 wallclock ws:取自 rq->wrq.window_start delta:取自 wallclock - rq->wrq.window_start event:取自 task_event_names[evt] cpu:取自 rq->cpu task:取自 p->pid swapper/5:取自 p->comm ms:取自 p->wts.mark_start delta:兩個同名,這是第二個,取自 wallclock - p->wts.mark_start demand:取自 p->wts.demand rq_cs:取自 rq->wrq.curr_runnable_sum rq_ps:取自 rq->wrq.prev_runnable_sum cur_window:取自 p->wts.curr_window prev_window:取自 p->wts.prev_window grp_cs:取自 cpu_time ? cpu_time->curr_runnable_sum : 0 grp_ps:取自 cpu_time ? cpu_time->prev_runnable_sum : 0
2. walt_update_task_ravg 的調用路徑
tick_setup_sched_timer //tick_sched.c timer到期回調函數中指定 tick_sched_timer update_process_times //time.c tick中斷中調用 scheduler_tick //core.c 周期定時器中斷,傳參(rq->curr, rq, TASK_UPDATE, wallclock, 0) //任務顯式阻塞或設置 TIF_NEED_RESCHED 並且在中斷或返回用戶空間調度點或preempt_enable() __schedule //core.c 在這個主調度器函數中調用了三次,若選出的prev != next,調用兩次,分別傳參(prev, rq, PUT_PREV_TASK, wallclock, 0)和(next, rq, PICK_NEXT_TASK, wallclock, 0),若選出的prev == next,傳參(prev, rq, TASK_UPDATE, wallclock, 0) __irq_enter //hardirq.h __handle_domain_irq()中調用,中斷入口:handle_arch_irq=gic_handle_irq-->handle_domain_irq __do_softirq //softirq.c account_irq_enter_time //vtime.h account_irq_exit_time //vtime.h irqtime_account_irq //cputime.c 若curr是idle task,並且是在硬中斷或軟中斷上下文則調用,否則調用walt_sched_account_irqstart walt_sched_account_irqend //walt.c,傳參(curr, rq, IRQ_UPDATE, wallclock, delta); move_queued_task __migrate_swap_task try_to_wake_up //core.c 當新選出的cpu和任務之前運行的不是同一個cpu調用 dl_task_offline_migration push_dl_task pull_dl_task detach_task push_rt_task pull_rt_task set_task_cpu //core.c 若新選出的cpu和任務之前的cpu不是同一個cpu,對任務進行遷移,然后調用,此時task->on_rq = TASK_ON_RQ_MIGRATING fixup_busy_time //walt.c 連續調用三次,分別傳參 (task_rq(p)->curr, task_rq(p), TASK_UPDATE, wallclock, 0)和(dest_rq->curr, dest_rq, TASK_UPDATE, wallclock, 0)和(p, task_rq(p), TASK_MIGRATE, wallclock, 0) cpufreq_freq_transition_end //cpufreq.c set_cpu_freq()中在設置頻點前調用cpufreq_freq_transition_begin,設置后調用這個函數 cpufreq_notify_post_transition //cpufreq.c 相同參數調用兩次 notifier_trans_block.notifier_call //回調,對應val=CPUFREQ_POSTCHANGE時通知 cpufreq_notifier_trans //walt.c 兩層循環,對freq_domain_cpumask中的每一個cpu,對cluster中的每一個cpu,都調用,傳參(rq->curr, rq, TASK_UPDATE, wallclock, 0) sync_cgroup_colocation //walt.c cpu_cgrp_subsys.attach=cpu_cgroup_attach-->walt_schedgp_attach中對每一個cpuset都調用 sched_group_id_write //qc_vas.c 對應/proc/<pid>/sched_group_id __sched_set_group_id //傳參group_id=0才調用 remove_task_from_group //walt.c 傳參(rq, p->wts.grp, p, REM_TASK) __sched_set_group_id //傳參group_id非0才調用 add_task_to_group //walt.c 傳參(rq, grp, p, ADD_TASK) transfer_busy_time //walt.c 連續調用兩次,分別傳參(rq->curr, rq, TASK_UPDATE, wallclock, 0)和(p, rq, TASK_UPDATE, wallclock, 0) fixup_busy_time //當task的cpu和參數cpu不是同一個時調用 walt_proc_user_hint_handler //walt.c /proc/sys/kernel/sched_user_hint作用load = load * (sched_user_hint / 100) 維持1s后清0 walt_migration_irq_work.func //walt.c irq_work 結構的回調 walt_update_task_ravg //又回來了,work的響應函數中queue work,構成一個"內核線程不"停執行 run_walt_irq_work //walt.c 若新的window_start和舊的不是同一個就調用 walt_cpufreq_irq_work.func //walt.c irq_work 結構的回調 walt_irq_work //walt.c 對每個cluster的每個cpu都調用,傳參(rq->curr, rq, TASK_UPDATE, wallclock, 0) wake_up_q wake_up_process wake_up_state default_wake_function try_to_wake_up walt_try_to_wake_up //walt.h 連續調用兩次,分別傳參(rq->curr, rq, TASK_UPDATE, wallclock, 0)和(p, rq, TASK_WAKE, wallclock, 0) walt_update_task_ravg
walt_update_task_ravg 通過參數 event 可以控制哪些事件不更新負載。
3. update_window_start 函數
/* 唯一調用路徑:walt_update_task_ravg --> this */ static u64 update_window_start(struct rq *rq, u64 wallclock, int event) //walt.c { s64 delta; int nr_windows; u64 old_window_start = rq->wrq.window_start; delta = wallclock - rq->wrq.window_start; if (delta < 0) { printk_deferred("WALT-BUG CPU%d; wallclock=%llu is lesser than window_start=%llu", rq->cpu, wallclock, rq->wrq.window_start); SCHED_BUG_ON(1); } /* sched_ravg_window 默認是20ms, 不足一個窗口就不更新,直接退出*/ if (delta < sched_ravg_window) return old_window_start; /* 下面是delta大於一個window的,計算歷經的整窗的個數 */ nr_windows = div64_u64(delta, sched_ravg_window); rq->wrq.window_start += (u64)nr_windows * (u64)sched_ravg_window; /* 更新ws */ rq->wrq.cum_window_demand_scaled = rq->wrq.walt_stats.cumulative_runnable_avg_scaled; rq->wrq.prev_window_size = sched_ravg_window; return old_window_start; }
可以看到,rq->wrq.window_start、rq->wrq.cum_window_demand_scaled 是最先更新的。然后返回舊的 window_start,
4. update_task_cpu_cycles 函數
static void update_task_cpu_cycles(struct task_struct *p, int cpu, u64 wallclock) //walt.c { if (use_cycle_counter) p->wts.cpu_cycles = read_cycle_counter(cpu, wallclock); }
在 p->wts.mark_start 為0的時候,調用這個函數,應該是做初始化的。
5. update_task_rq_cpu_cycles 函數
/* 唯一調用路徑 walt_update_task_ravg --> this */ static void update_task_rq_cpu_cycles(struct task_struct *p, struct rq *rq, int event, u64 wallclock, u64 irqtime) //walt.c { u64 cur_cycles; u64 cycles_delta; u64 time_delta; int cpu = cpu_of(rq); lockdep_assert_held(&rq->lock); if (!use_cycle_counter) { /* freq / maxfreq * cpu_capacity, arch_scale_cpu_capacity 為函數 topology_get_cpu_scale */ rq->wrq.task_exec_scale = DIV64_U64_ROUNDUP(cpu_cur_freq(cpu) * arch_scale_cpu_capacity(cpu), rq->wrq.cluster->max_possible_freq); return; } cur_cycles = read_cycle_counter(cpu, wallclock); /*return rq->wrq.cycles;*/ /* * 如果當前任務是空閑任務並且 irqtime == 0,CPU 確實空閑並且它的循環計數器可能沒有增加。 * 我們仍然需要估計的 CPU 頻率來計算 IO 等待時間。 在這種情況下使用先前計算的頻率。 */ if (!is_idle_task(rq->curr) || irqtime) { if (unlikely(cur_cycles < p->wts.cpu_cycles)) //這應該是溢出了 cycles_delta = cur_cycles + (U64_MAX - p->wts.cpu_cycles); else cycles_delta = cur_cycles - p->wts.cpu_cycles; cycles_delta = cycles_delta * NSEC_PER_MSEC; if (event == IRQ_UPDATE && is_idle_task(p)) /* * 在空閑任務的 mark_start 和 IRQ 處理程序進入時間之間的時間是 CPU 周期計數器停止時間段。 * 在 IRQ 處理程序進入 walt_sched_account_irqstart() 時,補充空閑任務的 cpu 周期計數器,因 * 此cycles_delta 現在表示 IRQ 處理程序期間增加的周期,而不是從進入空閑到 IRQ 退出之間的時間段。 * 因此使用 irqtime 作為時間增量。 */ time_delta = irqtime; else time_delta = wallclock - p->wts.mark_start; SCHED_BUG_ON((s64)time_delta < 0); /* (cycles_delta * cpu_capacity) / (time_delta * max_freq) = cycles_delta/time_delta * cpu_capacity/max_freq*/ rq->wrq.task_exec_scale = DIV64_U64_ROUNDUP(cycles_delta * arch_scale_cpu_capacity(cpu), time_delta * rq->wrq.cluster->max_possible_freq); trace_sched_get_task_cpu_cycles(cpu, event, cycles_delta, time_delta, p); } p->wts.cpu_cycles = cur_cycles; }
其中Trace:
trace_sched_get_task_cpu_cycles(cpu, event, cycles_delta, time_delta, p);
參數原型:
(int cpu, int event, u64 cycles, u64 exec_time, struct task_struct *p)
打印內容:
shell svc 7920-7921 [006] d..4 53723.502493: sched_get_task_cpu_cycles: cpu=6 event=2 cycles=105682000000 exec_time=78229 freq=1350931 legacy_freq=2035200 max_freq=2035200 task=19304 (kworker/u16:5)
字段解析:
前4個字段直接來自參數, freq:取自 cycles/exec_time, 其中 cycles 是乘以了 NSEC_PER_MSEC 的,exec_time 的單位是ns。 legacy_freq:取自 cpu_rq(cpu)->wrq.cluster->max_possible_freq,單位KHz max_freq:取自 cpu_rq(cpu)->wrq.cluster->max_possible_freq * cpu_rq(cpu)->cpu_capacity_orig / SCHED_CAPACITY_SCALE task:取自 p->pid kworker/u16:5:取自 p->comm
6. update_history 解析
update_task_demand 中若判斷不需要更新 task 的 p->wts.sum, 但是又有新窗口產生時,調用這個函數更新歷史負載。
/* * 當一個任務的新窗口開始時調用,記錄最近結束的窗口的 CPU 使用率。 通常'samples'應該是1。 * 比如說,當一個實時任務同時運行而不搶占幾個窗口時,它可以 > 1,也就是說連續運行3個窗口才 * 更新的話,samples就傳3。 * * update_task_demand()調用傳參:(rq, p, p->wts.sum, 1, event) sum 是幾5個窗口的 */ static void update_history(struct rq *rq, struct task_struct *p, u32 runtime, int samples, int event) //walt.c { u32 *hist = &p->wts.sum_history[0]; int ridx, widx; u32 max = 0, avg, demand, pred_demand; u64 sum = 0; u16 demand_scaled, pred_demand_scaled; /* Ignore windows where task had no activity */ if (!runtime || is_idle_task(p) || !samples) goto done; /* Push new 'runtime' value onto stack */ /* hist[5]中的元素向后移動samples個位置,runtime值插入到hist[0]中,hist[0]是最新的時間 */ widx = sched_ravg_hist_size - 1; /* 5-1=4 */ ridx = widx - samples; //widx=4, samples=1, ridx=3; samples=2, ridx=2 for (; ridx >= 0; --widx, --ridx) { hist[widx] = hist[ridx]; sum += hist[widx]; //此循環 sum = hist[4] + hist[3] + hist[2] + hist[1] if (hist[widx] > max) max = hist[widx]; //max保存最近4個窗中的最大值 } /* * 若samples=1, hist[0] = runtime * 若samples=2, hist[0] = runtime, hist[1] = runtime * ... */ for (widx = 0; widx < samples && widx < sched_ravg_hist_size; widx++) { hist[widx] = runtime; //hist[0]中存放的是最近的一個窗中運行的時間 sum += hist[widx]; //sum再加上hist[0] if (hist[widx] > max) max = hist[widx]; //max保存的是最近5個窗中最大的值了 } /* 將p->wts.sum放入history數組后就清0了, 也說明這個sum是一個窗的sum值 */ p->wts.sum = 0; /*可以通過 sched_window_stats_policy 文件進行配置下面4種window policy */ if (sysctl_sched_window_stats_policy == WINDOW_STATS_RECENT) { //為0,返回最近一個窗口的運行時間值 demand = runtime; } else if (sysctl_sched_window_stats_policy == WINDOW_STATS_MAX) { //為1,返回最近5個窗口運行時間的最大值 demand = max; } else { avg = div64_u64(sum, sched_ravg_hist_size); //求最近5個窗口運行時間的平均值 if (sysctl_sched_window_stats_policy == WINDOW_STATS_AVG) //為3,返回最近5個窗口平均運行時間值 demand = avg; else demand = max(avg, runtime); //為2,默認配置,返回最近5個窗口平均運行時間值 與 最近1個窗口運行時間值中的較大的那個 } pred_demand = predict_and_update_buckets(p, runtime); /* demand_scaled = demand/(window_size/1024) == (demand / window_size) * 1024 * 傳參demand可以認為是p的負載了 */ demand_scaled = scale_demand(demand); /* pred_demand_scaled = pred_demand/(window_size/1024) == (pred_demand / window_size) * 1024 */ pred_demand_scaled = scale_demand(pred_demand); /* * 限流的deadline調度類任務出隊列時不改變p->on_rq。 由於出隊遞減 walt stats 避免再次遞減它。 * 當窗口滾動時,累積窗口需求被重置為累積可運行平均值(來自運行隊列上的任務的貢獻)。如果當前任務已經出隊, * 則它的需求不包括在累積可運行平均值中。所以將任務需求單獨添加到累積窗口需求中。 */ /*這里增加的是rq上的統計值,不是per-entity的了*/ if (!task_has_dl_policy(p) || !p->dl.dl_throttled) { if (task_on_rq_queued(p)) { fixup_walt_sched_stats_common(rq, p, demand_scaled, pred_demand_scaled); /*這里加的是demand_scaled的差值*/ } else if (rq->curr == p) { walt_fixup_cum_window_demand(rq, demand_scaled); } } /*賦值給per-entiry上的統計值,demand_scaled 對 p->wts.demand_scaled 的賦值一定要保證,這是walt負載跟蹤算法重要的部分*/ p->wts.demand = demand; /* 對應一個窗中運行的時間(根據window policy不同而有差異) */ p->wts.demand_scaled = demand_scaled; /* 對應一個窗中運行的時間(根據window policy不同而有差異)縮放到1024 */ ############# p->wts.coloc_demand = div64_u64(sum, sched_ravg_hist_size); /*5個窗口運行時間之和除以5,即5個窗口的平均運行時間*/ p->wts.pred_demand = pred_demand; p->wts.pred_demand_scaled = pred_demand_scaled; /* demand_scaled 大於指定的閾值時,會做一些事情 */ if (demand_scaled > sysctl_sched_min_task_util_for_colocation) { p->wts.unfilter = sysctl_sched_task_unfilter_period; /*單位是ns,默認值是100ms*/ } else { if (p->wts.unfilter) p->wts.unfilter = max_t(int, 0, p->wts.unfilter - rq->wrq.prev_window_size); //相當於衰減一個窗口的大小 } done: trace_sched_update_history(rq, p, runtime, samples, event); }
其中Trace:
trace_sched_update_history(rq, p, runtime, samples, event);
參數原型:
(struct rq *rq, struct task_struct *p, u32 runtime, int samples, enum task_event evt)
打印內容:
sched_update_history: 24647 (kworker/u16:15): runtime 279389 samples 1 event TASK_WAKE demand 717323 coloc_demand 717323 pred_demand 279389 (hist: 279389 88058 520130 1182596 1516443) cpu 1 nr_big 0
字段解析:
24647:取自 p->pid kworker/u16:15:取自 p->comm runtime:來自參數3,表示最近一個窗口中的運行時間,也是 p->wts.sum 的值 samples:來自參數4,表示更新幾個窗的歷史 event:取自 task_event_names[event] demand:取自 p->wts.demand,是scale之前的根據不同window policy計算出來的負載值 coloc_demand:取自 p->wts.coloc_demand,即5個窗口的平均值 pred_demand:取自 p->wts.pred_demand,表示預測的負載需求 (hist: 279389 88058 520130 1182596 1516443):取自 p->wts.sum_history[5],是任務在最近5個窗口中分別運行的時間 cpu:取自 rq->cpu nr_big:取自 rq->wrq.walt_stats.nr_big_tasks
用於預測任務的 demand 的 bucket 相關更新:
static inline u32 predict_and_update_buckets(struct task_struct *p, u32 runtime) //walt.c { int bidx; u32 pred_demand; if (!sched_predl) //為1 return 0; /* 根據傳入的時間值獲得一個桶的下標,桶一共有10個成員 */ bidx = busy_to_bucket(runtime); /* 使用 p->wts.busy_buckets 用於計算 */ pred_demand = get_pred_busy(p, bidx, runtime); /* 更新 p->wts.busy_buckets */ bucket_increase(p->wts.busy_buckets, bidx); return pred_demand; } static inline int busy_to_bucket(u32 normalized_rt) { int bidx; bidx = mult_frac(normalized_rt, NUM_BUSY_BUCKETS, max_task_load()); /*args1*10/16; arg1*arg2/arg3*/ bidx = min(bidx, NUM_BUSY_BUCKETS - 1); //min(p->wts.sum * 10 / 16, 9) 運行一個滿窗是桶10,運行1ms-2ms返回1 /* 合並最低的兩個桶。 最低頻率落入第二桶,因此繼續預測最低桶是沒有用的。*/ if (!bidx) bidx++; return bidx; } /* * get_pred_busy - 計算運行隊列上的任務的預測需求 * * @p:正在更新預測的任務 * @start: 起始桶。 返回的預測不應低於此桶。 * @runtime:任務的運行時間。 返回的預測不應低於此運行時。 * 注意:@start 可以從@runtime 派生。 傳入它只是為了在某些情況下避免重復計算。 * * 根據傳入的@runtime 為任務@p 返回一個新的預測繁忙時間。該函數搜索表示繁忙時間等於或大於@runtime * 的桶,並嘗試找到用於預測的桶。 一旦找到,它會搜索歷史繁忙時間並返回落入桶中的最新時間。 如果不 * 存在這樣的繁忙時間,則返回該桶的中間值。 */ /*假設傳參是p->wts.sum=8ms,那么傳參就是(p, 5, 8),*/ static u32 get_pred_busy(struct task_struct *p, int start, u32 runtime) { int i; u8 *buckets = p->wts.busy_buckets; //10個元素 u32 *hist = p->wts.sum_history; //5個元素 u32 dmin, dmax; u64 cur_freq_runtime = 0; int first = NUM_BUSY_BUCKETS, final; //從最大值10開始找 u32 ret = runtime; /* skip prediction for new tasks due to lack of history */ /* 由於累積運行時間小於100ms的新任務缺少歷史運行時間,不對其進行預測 */ if (unlikely(is_new_task(p))) goto out; /* find minimal bucket index to pick */ /* 找到最小的桶下標進行pick, 只要桶中有數據就選擇 */ for (i = start; i < NUM_BUSY_BUCKETS; i++) { if (buckets[i]) { first = i; break; } } /* 若沒找到桶下標,就直接返回 runtime,注意 runtime 可能大於10 */ if (first >= NUM_BUSY_BUCKETS) goto out; /* 計算用於預測的桶 */ final = first; /* 確定預測桶的需求范圍 */ if (final < 2) { /* 最低的兩個桶合並 */ dmin = 0; final = 1; } else { dmin = mult_frac(final, max_task_load(), NUM_BUSY_BUCKETS); //final * 20 / 10, max_task_load返回一個滿窗 } dmax = mult_frac(final + 1, max_task_load(), NUM_BUSY_BUCKETS); //(final + 1) * 20 / 10 /* * search through runtime history and return first runtime that falls * into the range of predicted bucket. * 搜索運行歷史並返回落在預測桶范圍內的第一個運行。在最近的5個窗口中查找 */ for (i = 0; i < sched_ravg_hist_size; i++) { if (hist[i] >= dmin && hist[i] < dmax) { ret = hist[i]; break; } } /* no historical runtime within bucket found, use average of the bin * 若找不到存儲桶內的歷史運行時間,就使用垃圾桶的平均值 */ if (ret < dmin) ret = (dmin + dmax) / 2; /* * 在窗口中間更新時,運行時間可能高於所有記錄的歷史記錄。 始終至少預測運行時間。 */ ret = max(runtime, ret); out: /* 由於 cur_freq_runtime 是0,所以 pct 恆為0 */ trace_sched_update_pred_demand(p, runtime, mult_frac((unsigned int)cur_freq_runtime, 100, sched_ravg_window), ret); return ret; } /* * bucket_increase - 更新所有桶的計數 * * @buckets:跟蹤任務繁忙時間的桶數組 * @idx: 要被遞增的桶的索引 * * 每次完成一個完整的窗口時,運行時間落入 (@idx) 的桶計數增加。 所有其他桶的計數都會衰減。 * 根據桶中的當前計數,增加和衰減的速率可能不同。 */ /*傳參: (p->wts.busy_buckets, bidx)*/ static inline void bucket_increase(u8 *buckets, int idx) { int i, step; for (i = 0; i < NUM_BUSY_BUCKETS; i++) { //10 if (idx != i) { //不相等就衰減 if (buckets[i] > DEC_STEP) //2 buckets[i] -= DEC_STEP; //2 else buckets[i] = 0; } else { //相等 step = buckets[i] >= CONSISTENT_THRES ? INC_STEP_BIG : INC_STEP; //16 16 8 if (buckets[i] > U8_MAX - step) //255-step buckets[i] = U8_MAX; //255 else buckets[i] += step; //就是加step,上面判斷是為了不要溢出 } } }
其中Trace:
trace_sched_update_pred_demand(p, runtime, mult_frac((unsigned int)cur_freq_runtime, 100, sched_ravg_window), ret);
參數原型:
(struct task_struct *p, u32 runtime, int pct, unsigned int pred_demand)
打印內容:
sched_update_pred_demand: 1174 (Binder:1061_2): runtime 556361 pct 0 cpu 1 pred_demand 556361 (buckets: 0 255 0 0 0 0 0 0 0 0)
字段解析:
1174:取自 p->pid Binder:1061_2:取自 p->comm runtime:取自參數2 pct:取自參數3 cpu:取自task_cpu(p) pred_demand:取自參數4 (buckets: 0 255 0 0 0 0 0 0 0 0):取自 p->wts.busy_buckets[10]
/* * update_history --> this,如果task在rq上才會調用傳參: (rq, p, demand_scaled, pred_demand_scaled),參數是縮放到0--1024后的 * 也就是說這個函數里面計算的包含 runnable 的 */ static void fixup_walt_sched_stats_common(struct rq *rq, struct task_struct *p, u16 updated_demand_scaled, u16 updated_pred_demand_scaled) { /* p->wts.demand_scaled 約是由 p->wts.sum scale后得來的(window plicy策略影響), 后者是一個窗口中任務運行的時長。新的減舊的,結果處於[-1024,1024] */ s64 task_load_delta = (s64)updated_demand_scaled - p->wts.demand_scaled; /* p->wts.pred_demand_scaled 是由桶算法預測得來的 */ s64 pred_demand_delta = (s64)updated_pred_demand_scaled - p->wts.pred_demand_scaled; /* 直接加上傳入的增量,注意增量可能是負數,一個進程的負載變低了,差值就是負數了*/ fixup_cumulative_runnable_avg(&rq->wrq.walt_stats, task_load_delta, pred_demand_delta); /*累加demand_scaled的增量*/ walt_fixup_cum_window_demand(rq, task_load_delta); /*上下兩個函數都是對rq->wrq.中的成員賦值*/ } /* * 如果task在rq上調用路徑:update_history --> fixup_cumulative_runnable_avg 傳參:(&rq->wrq.walt_stats, task_load_delta, pred_demand_delta) * 傳參為時間差值。 */ static inline void fixup_cumulative_runnable_avg(struct walt_sched_stats *stats, s64 demand_scaled_delta, s64 pred_demand_scaled_delta) { /* * 增量差值可正可負,rq 的 cumulative_runnable_avg_scaled 初始化后就只有在這里有賦值了。 * 這里根據根據當前窗口負載值快速變化。 */ stats->cumulative_runnable_avg_scaled += demand_scaled_delta; BUG_ON((s64)stats->cumulative_runnable_avg_scaled < 0); stats->pred_demands_sum_scaled += pred_demand_scaled_delta; BUG_ON((s64)stats->pred_demands_sum_scaled < 0); }
說明 rq->wrq.walt_stats.cumulative_runnable_avg_scaled 和 rq->wrq.walt_stats.pred_demands_sum_scaled 統計的只是 runnable 狀態的負載值。這里加上有符號的delta值,可以快速的反應runnable狀態的負載的變化。
/* * 如果task在rq上調用路徑:update_history --> fixup_walt_sched_stats_common --> this 傳參:(rq, task_load_delta) * 如果rq->curr == p 時調用路徑:update_history --> this 傳參:(rq, demand_scaled) * 說明這里面更新的成員統計的級包括 runnable 的分量,也保留 running 的分量 */ static inline void walt_fixup_cum_window_demand(struct rq *rq, s64 scaled_delta) { rq->wrq.cum_window_demand_scaled += scaled_delta; if (unlikely((s64)rq->wrq.cum_window_demand_scaled < 0)) rq->wrq.cum_window_demand_scaled = 0; }
rq->wrq.cum_window_demand_scaled 統計的既包括 runnable 的又包括 running 的。runnable 的累加的是差值,而 running 的累加的直接是 demand_scaled 的值,若是一部分 runnable 的任務變成 running 了,前者減少,后者增加,體現在結果上可能是不變的。
7. update_task_demand 函數
/* * 計算任務的cpu需求和/或更新任務的cpu需求歷史 * * ms = p->wts.mark_start * wc = wallclock * ws = rq->wrq.window_start * * 三種可能: * a) 任務事件包含在一個窗口中。 16ms per-window, window_start < mark_start < wallclock * ws ms wc * | | | * V V V * |---------------| * * 在這種情況下,如果事件是合適的 p->wts.sum 被更新(例如:event == PUT_PREV_TASK) * * b) 任務事件跨越兩個窗口。mark_start < window_start < wallclock * * ms ws wc * | | | * V V V * ------|------------------- * * 在這種情況下,如果事件是合適的 p->wts.sum 更新為 (ws - ms) ,然后記錄一個新的窗口的采樣,如果事件是合 * 適的然后將 p->wts.sum 設置為 (wc - ws) 。 * * c) 任務事件跨越兩個以上的窗口。 * * ms ws_tmp ws wc * | | | | * V V V V * ---|-------|-------|-------|-------|------ * | | * |<------ nr_full_windows ------>| * * 在這種情況下,如果事件是合適的,首先 p->wts.sum 更新為 (ws_tmp - ms) ,p->wts.sum 被記錄,然后,如果 * event 是合適的 window_size 的 'nr_full_window' 樣本也被記錄,最后如果 event 是合適的,p->wts.sum 更新 * 到 (wc - ws)。 * * 重要提示:保持 p->wts.mark_start 不變,因為 update_cpu_busy_time() 依賴它! * */ /* walt_update_task_ravg-->this 唯一調用位置 */ static u64 update_task_demand(struct task_struct *p, struct rq *rq, int event, u64 wallclock) //walt.c { u64 mark_start = p->wts.mark_start; //進來時還沒更新 u64 delta, window_start = rq->wrq.window_start; //進來時已經更新了 int new_window, nr_full_windows; u32 window_size = sched_ravg_window; //20ms u64 runtime; new_window = mark_start < window_start; //若為真說明經歷了新窗口 /* 若判斷不需要更新負載,直接更新歷史 p->wts.sum_history[],而沒有更新 p->wts.sum */ if (!account_busy_for_task_demand(rq, p, event)) { if (new_window) { /* * 如果計入的時間沒有計入繁忙時間,並且新的窗口開始, * 則只需要關閉前一個窗口與預先存在的需求。 多個窗口 * 可能已經過去,但由於空窗口被丟棄,因此沒有必要考慮這些。 * * 如果被累積的時間沒有被計入繁忙時間,並且有新的窗口開始, * 則只需要與預先存在需求的前一個窗口被關閉。 雖然可能有多 * 個窗口已經流逝了,但由於WALT算法是空窗口會被丟棄掉,因 * 此沒有必要考慮這些。 */ update_history(rq, p, p->wts.sum, 1, event); } return 0; } /* 下面是需要更新的情況了 */ /* (1) 還是同一個窗口,對應上面的情況a */ if (!new_window) { /* 簡單的情況 - 包含在現有窗口中的繁忙時間。*/ return add_to_task_demand(rq, p, wallclock - mark_start); } /* (2) 下面就是跨越了窗口,先求情況b */ /* 繁忙時間至少跨越兩個窗口。 暫時將 window_start 倒回到 mark_start 之后的第一個窗口邊界。*/ delta = window_start - mark_start; nr_full_windows = div64_u64(delta, window_size); window_start -= (u64)nr_full_windows * (u64)window_size; /* Process (window_start - mark_start) first */ /* 這里累加的是 情況b/情況c 中ws_tmp-ms這段的delta值 */ runtime = add_to_task_demand(rq, p, window_start - mark_start); /* Push new sample(s) into task's demand history */ /* 將最開始的不足一個window窗口大小的delta計算出來的p->wts.sum放入歷史數組中 */ update_history(rq, p, p->wts.sum, 1, event); /* (3) 下面就對應情況c了,由於c和b都有最開始不足一個窗口的一段,在上面計算b時一並計算了 */ if (nr_full_windows) { u64 scaled_window = scale_exec_time(window_size, rq); //等於直接return window_size /* 一下子更新 nr_full_windows 個窗口的負載到歷史窗口負載中,每個窗口都是滿窗 */ update_history(rq, p, scaled_window, nr_full_windows, event); /* runtime 累積運行時間進行累加 ==>只要搞清什么時候標記ms和什么時候調用這個函數計算負載,就可以知道計算的是哪段的 ######## */ runtime += nr_full_windows * scaled_window; } /* 將 window_start 滾回當前以處理當前窗口,以便於計算當前窗口中的剩余部分。*/ window_start += (u64)nr_full_windows * (u64)window_size; /* 這里是計算情況b和情況c的wc-ws段 */ mark_start = window_start; runtime += add_to_task_demand(rq, p, wallclock - mark_start); //runtime 繼續累加 /* 返回值表示此次 update_task_demand 更新的時間值,是 wc-ms 的差值 */ return runtime; }
此函數中始終沒有更新回去 p->wts.mark_start,其是在 walt_update_task_ravg 函數最后更新的。rq->wrq.window_start 在上面第一個函數中就更新了。
/* update_task_demand --> this */ static int account_busy_for_task_demand(struct rq *rq, struct task_struct *p, int event) //walt.c { /* (1) 不需要統計 idle task 的 demand,直接返回*/ if (is_idle_task(p)) return 0; /* * 當一個任務被喚醒時,它正在完成一段非繁忙時間。 同樣,如果等待時間 * 不被視為繁忙時間,那么當任務開始運行或遷移時,它並未運行並且正在完成 * 一段非繁忙時間。 */ /*就是這些情況跳過統計,!SCHED_ACCOUNT_WAIT_TIME 恆為假,所以是只判斷了 TASK_WAKE */ /* (2) 是喚醒事件 或 不需要計算walit事件並且事件是pick和migrate, 不需要更新 */ if (event == TASK_WAKE || (!SCHED_ACCOUNT_WAIT_TIME && (event == PICK_NEXT_TASK || event == TASK_MIGRATE))) return 0; /* (3) idle進程退出的時候也不需要統計 */ if (event == PICK_NEXT_TASK && rq->curr == rq->idle) return 0; /* * TASK_UPDATE can be called on sleeping task, when its moved between related groups */ /*context_switch()的時候更改的rq->curr*/ /* (4) 若是update事件,且p是curr任務,需要更新。否則若p在隊列上需要更新,不在隊列上不需要更新 */ if (event == TASK_UPDATE) { if (rq->curr == p) return 1; return p->on_rq ? SCHED_ACCOUNT_WAIT_TIME : 0; //這里可調整是否記錄任務在rq上的等待的時間 } /* (5) 都不滿足,默認是需要更新 */ return 1; }
p是idle task,或 事件是 TASK_WAKE,或idle任務退出時的 PICK_NEXT_TASK 事件,或事件是 TASK_UPDATE 但是 p 不是curr任務也沒有在rq上,就不需要計算busy time。只有事件是 TASK_UPDATE,且任務p是 rq->curr 任務或者 p是在rq 上等待,則需要更新。若不需要更新的話,又產生了新的窗口,那就調用 update_history()更新負載歷史就退出了。
/* update_task_demand --> this 唯一調用路徑也是在 walt_update_task_ravg 中 */ static u64 add_to_task_demand(struct rq *rq, struct task_struct *p, u64 delta) //walt.c { /* delta = (delta * rq->wrq.task_exec_scale) >> 10, 由於 rq->wrq.task_exec_scale 初始化為1024,所以還是delta*/ delta = scale_exec_time(delta, rq); /* 這里更新了 p->wts.sum,並將最大值鉗位在一個窗口大小*/ p->wts.sum += delta; if (unlikely(p->wts.sum > sched_ravg_window)) p->wts.sum = sched_ravg_window; return delta; }
更新 p->wts.sum 值,並且返回 delta 值。這也是 sum 的唯一更新位置,唯一調用路徑也是從 walt_update_task_ravg 函數調用下來的。
8. update_cpu_busy_time 函數
/* walt_update_task_ravg --> this 這是唯一調用路徑,傳參(p, rq, event, wallclock, irqtime)*/ static void update_cpu_busy_time(struct task_struct *p, struct rq *rq, int event, u64 wallclock, u64 irqtime) { int new_window, full_window = 0; int p_is_curr_task = (p == rq->curr); u64 mark_start = p->wts.mark_start; u64 window_start = rq->wrq.window_start; //walt_update_task_ravg-->update_window_start 最先更新的rq->wrq.window_start u32 window_size = rq->wrq.prev_window_size; u64 delta; u64 *curr_runnable_sum = &rq->wrq.curr_runnable_sum; u64 *prev_runnable_sum = &rq->wrq.prev_runnable_sum; u64 *nt_curr_runnable_sum = &rq->wrq.nt_curr_runnable_sum; u64 *nt_prev_runnable_sum = &rq->wrq.nt_prev_runnable_sum; bool new_task; struct walt_related_thread_group *grp; int cpu = rq->cpu; u32 old_curr_window = p->wts.curr_window; new_window = mark_start < window_start; if (new_window) full_window = (window_start - mark_start) >= window_size; /* 處理每個任務的窗口翻轉。 我們不關心空閑任務。*/ if (!is_idle_task(p)) { if (new_window) /* 將 p->wts 的 curr_window 賦值給 prev_window,然后將 curr_window 清0 */ rollover_task_window(p, full_window); } new_task = is_new_task(p); //運行時間小於5個窗口的任務 /* p是curr任務並且有了個新窗口才執行 */ if (p_is_curr_task && new_window) { /* rq的一些成員,prev_*_sum=curr_*_sum, 然后將 curr_*_sum 賦值為0 */ rollover_cpu_window(rq, full_window); rollover_top_tasks(rq, full_window); //這里面已經更新了rq->wrq.curr_table ############ } /* 判斷是否需要記錄 */ if (!account_busy_for_cpu_time(rq, p, irqtime, event)) goto done; /*----下面就是需要計算的了----*/ grp = p->wts.grp; if (grp) { struct group_cpu_time *cpu_time = &rq->wrq.grp_time; /* 注意:指向更改了! */ curr_runnable_sum = &cpu_time->curr_runnable_sum; prev_runnable_sum = &cpu_time->prev_runnable_sum; nt_curr_runnable_sum = &cpu_time->nt_curr_runnable_sum; nt_prev_runnable_sum = &cpu_time->nt_prev_runnable_sum; } if (!new_window) { /* * account_busy_for_cpu_time() = 1 所以忙時間需要計入當前窗口。 * 沒有翻轉,因為我們沒有啟動一個新窗口。 這方面的一個例子是當 * 任務開始執行然后在同一窗口內休眠時。 */ if (!irqtime || !is_idle_task(p) || cpu_is_waiting_on_io(rq)) delta = wallclock - mark_start; else delta = irqtime; delta = scale_exec_time(delta, rq); //等於直接return delta *curr_runnable_sum += delta; if (new_task) *nt_curr_runnable_sum += delta; if (!is_idle_task(p)) { p->wts.curr_window += delta; p->wts.curr_window_cpu[cpu] += delta; } goto done; } /*----下面就是有一個新窗口的情況了----*/ if (!p_is_curr_task) { /* * account_busy_for_cpu_time() = 1 所以忙時間需要計入當前窗口。 * 一個新窗口也已啟動,但 p 不是當前任務,因此窗口不會翻轉 * - 只需拆分並根據需要將計數分為 curr 和 prev。 僅在為當前任 * 務處理新窗口時才會翻轉窗口。 * * irqtime 不能由不是當前正在運行的任務的任務計算。 */ if (!full_window) { /* 一個完整的窗口還沒有過去,計算對上一個完成的窗口的部分貢獻。*/ delta = scale_exec_time(window_start - mark_start, rq); p->wts.prev_window += delta; p->wts.prev_window_cpu[cpu] += delta; } else { /* 由於至少一個完整的窗口已經過去,對前一個窗口的貢獻是一個完整的窗口(window_size) */ delta = scale_exec_time(window_size, rq); p->wts.prev_window = delta; p->wts.prev_window_cpu[cpu] = delta; } *prev_runnable_sum += delta; if (new_task) *nt_prev_runnable_sum += delta; /* 只占當前窗口的一部分繁忙時間 */ delta = scale_exec_time(wallclock - window_start, rq); *curr_runnable_sum += delta; if (new_task) *nt_curr_runnable_sum += delta; p->wts.curr_window = delta; /*對當前窗的貢獻直接復制給當前窗*/ p->wts.curr_window_cpu[cpu] = delta; goto done; } /*----下面p是當前任務的情況了----*/ if (!irqtime || !is_idle_task(p) || cpu_is_waiting_on_io(rq)) { /* * account_busy_for_cpu_time() = 1 所以忙時間需要計入當前窗口。 一個新窗口已經啟動, * p 是當前任務,因此需要翻轉。 如果以上三個條件中的任何一個為真,那么這個繁忙的時 * 間就不能算作 irqtime。 * * 空閑任務的繁忙時間不需要計算。 * * 一個例子是一個任務開始執行,然后在新窗口開始后休眠。 */ if (!full_window) { /* 一個完整的窗口還沒有過去,計算對上一個完整的窗口的部分貢獻。*/ delta = scale_exec_time(window_start - mark_start, rq); //等效直接返回window_start - mark_start if (!is_idle_task(p)) { p->wts.prev_window += delta; p->wts.prev_window_cpu[cpu] += delta; } } else { /* 由於至少一個完整的窗口已經過去,對前一個窗口的貢獻是完整的窗口(window_size)*/ delta = scale_exec_time(window_size, rq); if (!is_idle_task(p)) { p->wts.prev_window = delta; p->wts.prev_window_cpu[cpu] = delta; } } /* 在這里通過覆蓋 prev_runnable_sum 和 curr_runnable_sum 中的值來完成翻轉。*/ *prev_runnable_sum += delta; if (new_task) *nt_prev_runnable_sum += delta; /* 計算在當前窗口忙時的一片時間 */ delta = scale_exec_time(wallclock - window_start, rq); *curr_runnable_sum += delta; if (new_task) *nt_curr_runnable_sum += delta; if (!is_idle_task(p)) { p->wts.curr_window = delta; p->wts.curr_window_cpu[cpu] = delta; } goto done; } /*---- 下面就對應 irqtime && is_idle_task(p) && !cpu_is_waiting_on_io(rq) 的情況了,並且累積上面的條件 ----*/ if (irqtime) { /* * account_busy_for_cpu_time() = 1 所以忙時間需要計入當前窗口。 * 一個新窗口已經啟動,p 是當前任務,因此需要翻轉。 當前任務必 * 須是空閑任務,因為不為其他任何任務計算irqtime。 * * 空閑一段時間后,每次我們處理 IRQ 活動時都會計算 Irqtime,因 * 此我們知道 IRQ 繁忙時間為 wallclock - irqtime。 */ SCHED_BUG_ON(!is_idle_task(p)); mark_start = wallclock - irqtime; /* * 滾動窗口。 如果 IRQ 繁忙時間只是在當前窗口中,那么這就是所有需要計算的。 */ if (mark_start > window_start) { *curr_runnable_sum = scale_exec_time(irqtime, rq); //等效於直接返回irqtime,因為是idle線程,之前應該是0的 return; } /*---下面是ms<=ws---*/ /* * IRQ 繁忙時間跨越多個窗口。 先處理當前窗口開始前的忙時間。 */ delta = window_start - mark_start; if (delta > window_size) delta = window_size; delta = scale_exec_time(delta, rq); *prev_runnable_sum += delta; //這直接加不會超過一個窗的大小嗎? /* Process the remaining IRQ busy time in the current window. 處理當前窗口中剩余的 IRQ 忙時間。*/ delta = wallclock - window_start; rq->wrq.curr_runnable_sum = scale_exec_time(delta, rq); return; } done: if (!is_idle_task(p)) update_top_tasks(p, rq, old_curr_window, new_window, full_window); }
值更新當前窗口和前一個窗口的busy時間,主要用於更新任務的: p->wts.curr_window、p->wts.curr_window_cpu[cpu],更新rq 的 rq->wrq.curr_runnable_sum、rq->wrq.prev_runnable_sum,若是一個walt認為的新任務,還更新 rq->wrq.nt_curr_runnable_sum、rq->wrq.nt_prev_runnable_sum。然后是更新 top-task 的一些成員
下面分別是對 task、cpu、top_tasks 維護的 window 進行更新。有一個新的窗口到來時更新,若更新時已經經歷了一個或多個完整的window,那么對prev和curr window 相關的描述結構進行清理備用。
static u32 empty_windows[NR_CPUS]; /* 將 p->wts 的 curr_window 賦值給 prev_window,然后將 curr_window 清0 */ static void rollover_task_window(struct task_struct *p, bool full_window) { u32 *curr_cpu_windows = empty_windows; //數組,每個cpu一個 u32 curr_window; int i; /* Rollover the sum */ curr_window = 0; /* 若經歷了一個full_window, prev和curr window都清理待用 */ if (!full_window) { curr_window = p->wts.curr_window; curr_cpu_windows = p->wts.curr_window_cpu; } p->wts.prev_window = curr_window; p->wts.curr_window = 0; /* Roll over individual CPU contributions 滾動每個 CPU 的貢獻 */ for (i = 0; i < nr_cpu_ids; i++) { p->wts.prev_window_cpu[i] = curr_cpu_windows[i]; p->wts.curr_window_cpu[i] = 0; } if (is_new_task(p)) p->wts.active_time += task_rq(p)->wrq.prev_window_size; //active_time 的唯一更新位置, walt認為的新任務 }
清理的是任務的 p->wts.prev_window_cpu、p->wts.curr_window、p->wts.prev_window_cpu[]、p->wts.curr_window_cpu[]
/* * rq的一些成員,prev_*_sum=curr_*_sum, 然后將 curr_*_sum 賦值為0,將curr賦值給prev, * 若是有經歷了多個窗口curr和prev窗口都需要清理待用。 */ static void rollover_cpu_window(struct rq *rq, bool full_window) { u64 curr_sum = rq->wrq.curr_runnable_sum; u64 nt_curr_sum = rq->wrq.nt_curr_runnable_sum; u64 grp_curr_sum = rq->wrq.grp_time.curr_runnable_sum; u64 grp_nt_curr_sum = rq->wrq.grp_time.nt_curr_runnable_sum; if (unlikely(full_window)) { curr_sum = 0; nt_curr_sum = 0; grp_curr_sum = 0; grp_nt_curr_sum = 0; } rq->wrq.prev_runnable_sum = curr_sum; rq->wrq.nt_prev_runnable_sum = nt_curr_sum; rq->wrq.grp_time.prev_runnable_sum = grp_curr_sum; rq->wrq.grp_time.nt_prev_runnable_sum = grp_nt_curr_sum; rq->wrq.curr_runnable_sum = 0; rq->wrq.nt_curr_runnable_sum = 0; rq->wrq.grp_time.curr_runnable_sum = 0; rq->wrq.grp_time.nt_curr_runnable_sum = 0; }
清理的是 rq->wrq 的 和 rq->wrq.grp_time 的 prev_runnable_sum、curr_runnable_sum、nt_prev_runnable_sum、nt_curr_runnable_sum
static void rollover_top_tasks(struct rq *rq, bool full_window) { /* 跟蹤的是2個,構成一個環形數組 */ u8 curr_table = rq->wrq.curr_table; u8 prev_table = 1 - curr_table; int curr_top = rq->wrq.curr_top; /*將prev window的數據結構清理后待用*/ clear_top_tasks_table(rq->wrq.top_tasks[prev_table]); //memset(arg, 0, NUM_LOAD_INDICES * sizeof(u8)); clear_top_tasks_bitmap(rq->wrq.top_tasks_bitmap[prev_table]);//將bit數組的內容清0,然后將 NUM_LOAD_INDICES bit設置為1 /*若是已經經歷了多個window,那么之前標記的curr window也是舊窗口了,需要清理待用*/ if (full_window) { curr_top = 0; clear_top_tasks_table(rq->wrq.top_tasks[curr_table]); clear_top_tasks_bitmap(rq->wrq.top_tasks_bitmap[curr_table]); } /*兩個window的下標進行翻轉,curr-->prev,prev-->curr*/ rq->wrq.curr_table = prev_table; rq->wrq.prev_top = curr_top; rq->wrq.curr_top = 0; }
清理的是 rq->wrq 的 top_task 相關的成員。
然后調用 account_busy_for_cpu_time 判斷清理后任務的和cpu的是否還需要更新上去
/* update_cpu_busy_time-->this, 傳參(rq, p, irqtime, event) */ static int account_busy_for_cpu_time(struct rq *rq, struct task_struct *p, u64 irqtime, int event) { if (is_idle_task(p)) { /* TASK_WAKE && TASK_MIGRATE is not possible on idle task! idle task不可能出現喚醒和遷移 */ if (event == PICK_NEXT_TASK) return 0; /* PUT_PREV_TASK, TASK_UPDATE && IRQ_UPDATE are left */ return irqtime || cpu_is_waiting_on_io(rq); } if (event == TASK_WAKE) return 0; if (event == PUT_PREV_TASK || event == IRQ_UPDATE) return 1; /* * TASK_UPDATE can be called on sleeping task, when its moved between related groups * TASK_UPDATE 當它在相關組之間移動時可能在睡眠的任務上調用, */ if (event == TASK_UPDATE) { if (rq->curr == p) return 1; return p->on_rq ? SCHED_FREQ_ACCOUNT_WAIT_TIME : 0; //在rq上和或正在遷移是1,但是冒號前后都是0 } /* TASK_MIGRATE, PICK_NEXT_TASK left */ return SCHED_FREQ_ACCOUNT_WAIT_TIME; //0 }
top_task 維護的窗口更新:
/* * update_cpu_busy_time-->this 若p不是idle任務,就調用,傳參(p, rq, old_curr_window, new_window, full_window) * @ old_curr_window:取自 p->wts.curr_window,表示p在窗口翻轉前在當前窗口的運行時間 * @ new_window:bool值,若ms<ws為真 * @ full_window:bool值,若ws-ms>window_size為真 */ static void update_top_tasks(struct task_struct *p, struct rq *rq, u32 old_curr_window, int new_window, bool full_window) { /* 只使用兩個窗口進行跟蹤,當前是0,perv就是1,當前是1,prev就是0,兩個數據結構構成一個環形緩存區 */ u8 curr = rq->wrq.curr_table; u8 prev = 1 - curr; u8 *curr_table = rq->wrq.top_tasks[curr]; u8 *prev_table = rq->wrq.top_tasks[prev]; int old_index, new_index, update_index; u32 curr_window = p->wts.curr_window; u32 prev_window = p->wts.prev_window; bool zero_index_update; /* 兩個窗的運行時間相等或新窗口還沒有到來 */ if (old_curr_window == curr_window && !new_window) return; /* 在一個窗中運行的時間越長,index就越大, 參數是一個窗口中的運行時長*/ old_index = load_to_index(old_curr_window); new_index = load_to_index(curr_window); if (!new_window) { zero_index_update = !old_curr_window && curr_window; if (old_index != new_index || zero_index_update) { if (old_curr_window) curr_table[old_index] -= 1; //上一個窗口的累計值衰減 if (curr_window) curr_table[new_index] += 1; //新窗口的累計值增加 if (new_index > rq->wrq.curr_top) rq->wrq.curr_top = new_index; //更新rq->wrq.curr_top成員 } if (!curr_table[old_index]) __clear_bit(NUM_LOAD_INDICES - old_index - 1, rq->wrq.top_tasks_bitmap[curr]); //這個bit數組表示此運行時間下有沒有計數值 if (curr_table[new_index] == 1) __set_bit(NUM_LOAD_INDICES - new_index - 1, rq->wrq.top_tasks_bitmap[curr]); return; } /*---下面是new_window!=0的情況了---*/ /* * 對於此任務來說窗口已經翻轉。 當我們到達這里時,curr/prev 交換已經發生。 * 所以我們需要對新索引使用 prev_window 。 */ update_index = load_to_index(prev_window); if (full_window) { //至少有一個滿窗 /* * 這里有兩個案例。 要么'p' 運行了整個窗口,要么根本不運行。 在任何一種情況下, * prev 表中都沒有條目。 如果 'p' 運行整個窗口,我們只需要在 prev 表中創建一個 * 新條目。 在這種情況下,update_index 將對應於 sched_ravg_window,因此我們可 * 以無條件地更新頂部索引。 */ if (prev_window) { prev_table[update_index] += 1; rq->wrq.prev_top = update_index; } if (prev_table[update_index] == 1) __set_bit(NUM_LOAD_INDICES - update_index - 1, rq->wrq.top_tasks_bitmap[prev]); } else { //產生了新窗口,但是還沒達到一個滿窗 zero_index_update = !old_curr_window && prev_window; if (old_index != update_index || zero_index_update) { if (old_curr_window) prev_table[old_index] -= 1; prev_table[update_index] += 1; if (update_index > rq->wrq.prev_top) rq->wrq.prev_top = update_index; /* 減為0是清理對應bit,首次設置為1時設置相應bit。top_tasks_bitmap[]在任務遷移時有使用 */ if (!prev_table[old_index]) __clear_bit(NUM_LOAD_INDICES - old_index - 1, rq->wrq.top_tasks_bitmap[prev]); if (prev_table[update_index] == 1) __set_bit(NUM_LOAD_INDICES - update_index - 1, rq->wrq.top_tasks_bitmap[prev]); } } if (curr_window) { curr_table[new_index] += 1; if (new_index > rq->wrq.curr_top) rq->wrq.curr_top = new_index; if (curr_table[new_index] == 1) __set_bit(NUM_LOAD_INDICES - new_index - 1, rq->wrq.top_tasks_bitmap[curr]); } }
top_tasks 的維護中也使用到了桶,新窗運行時間對應的 curr_table[]成員加1,之前窗口運行時間對應的 prev_table[] 成員減1。
9. update_task_pred_demand 函數
/* * 在窗口翻轉時計算任務的預測需求。如果任務當前窗口繁忙時間超過預測需求,則在此處更新以反映任務需求。 */ void update_task_pred_demand(struct rq *rq, struct task_struct *p, int event) { u32 new, old; u16 new_scaled; if (!sched_predl) //1 return; if (is_idle_task(p)) return; if (event != PUT_PREV_TASK && event != TASK_UPDATE && (!SCHED_FREQ_ACCOUNT_WAIT_TIME || (event != TASK_MIGRATE && event != PICK_NEXT_TASK))) return; /* * 當它在相關組之間移動時,TASK_UPDATE 可以在睡眠任務上調用。 */ if (event == TASK_UPDATE) { if (!p->on_rq && !SCHED_FREQ_ACCOUNT_WAIT_TIME) return; } new = calc_pred_demand(p); old = p->wts.pred_demand; if (old >= new) return; /*---下面就是 new > old 的情況---*/ new_scaled = scale_demand(new); //new/window_size*1024 /* p是on rq的狀態並且不是已經被throttle的deadline任務 */ if (task_on_rq_queued(p) && (!task_has_dl_policy(p) || !p->dl.dl_throttled)) fixup_walt_sched_stats_common(rq, p, p->wts.demand_scaled, new_scaled); p->wts.pred_demand = new; p->wts.pred_demand_scaled = new_scaled; }
注意,這里再次調用了 fixup_walt_sched_stats_common,在 walt_update_task_ravg 函數中,在 update_history 中已經調用過一次,進入條件也相同,也是p在隊列上。
static inline u32 calc_pred_demand(struct task_struct *p) { /* 預測的需求比當前窗口的大,就返回預測的需求 */ if (p->wts.pred_demand >= p->wts.curr_window) return p->wts.pred_demand; return get_pred_busy(p, busy_to_bucket(p->wts.curr_window), p->wts.curr_window); }
get_pred_busy 和 busy_to_bucket 兩個函數上面都有列出。
10. run_walt_irq_work 函數
static inline void run_walt_irq_work(u64 old_window_start, struct rq *rq) //walt.c { u64 result; /*若是還是同一個窗,直接退出*/ if (old_window_start == rq->wrq.window_start) return; /* * atomic64_cmpxchg(*ptr, old, new) 函數功能是:將old和ptr指向的內容比較,如果相等, * 則將new寫入到ptr指向的內存中,並返回old,如果不相等,則返回ptr指向的內容。 */ result = atomic64_cmpxchg(&walt_irq_work_lastq_ws, old_window_start, rq->wrq.window_start); if (result == old_window_start) { walt_irq_work_queue(&walt_cpufreq_irq_work); //觸發回調 walt_irq_work(),構成一個"內核線程",循環往復執行 trace_walt_window_rollover(rq->wrq.window_start); } }
walt_irq_work_queue 會觸發 walt_irq_work() 被調用,這個函數中又會調用 walt_update_task_ravg,walt_update_task_ravg 函數會在每個tick中調用,這里這樣實現可能是針對沒有tick的場景使用。
其中Trace:
trace_walt_window_rollover(rq->wrq.window_start);
參數原型:
(u64 window_start)
打印內容:
//20ms間隔執行一次 <idle>-0 [002] d..2 48262.320451: walt_window_rollover: window_start=48262548000001 <idle>-0 [001] d.h2 48262.340457: walt_window_rollover: window_start=48262568000001
字段解析:
window_start 就是打印 rq->wrq.window_start 的記錄的時間值,單位是ns.
四、總結
1. WALT負載計算算法是基於窗口的,對window有一個rollover的操作,只跟蹤curr和prev兩個窗口,curr窗口的下標由 wrq.curr_table 指向,兩個窗口構成一個喚醒緩沖區,prev和curr進行不斷切換。
2. walt_update_task_ravg 函數通過其 event 成員決定對哪些事件計算負載,再根據其調用路徑和其調用函數對是否是在rq上,是否是p=rq->curr可以判斷統計的是哪部分的負載。
3. 預測負載這塊,對於任務和CPU都使用了桶,任務是10個桶,對於cpu的curr和prev兩個窗口分別是1000個成員,命中累加,不命中衰減。
4. walt_update_task_ravg 在tick的調用路徑中有調用,應該是為了無tick情況下walt仍然能正常工作,使用irq_work構成一個內核線程以一個窗口的周期來更新窗口。
五、補充
1. task util的獲取:task_util() WALT: p->wts.demand_scaled;PELT: p->se.avg.util_avg
2. cpu util的獲取:cpu_util_cum() WALT: rq->wrq.cum_window_demand_scaled;PELT: rq->cfs.avg.util_avg
3. task_util() 函數
static inline unsigned long task_util(struct task_struct *p) { #ifdef CONFIG_SCHED_WALT if (likely(!walt_disabled && sysctl_sched_use_walt_task_util)) return p->wts.demand_scaled; #endif return READ_ONCE(p->se.avg.util_avg); }