一、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); }