RT: scheduler migration/wakeup enhancements
cover RT: scheduler migration/wakeup enhancements
Ingo, Steven, Thomas,
Please consider this series for inclusion in 23-rt6, as it has shown to make a substantial improvement in our local testing. Independent verification and/or comments/review are more than welcome.
-Greg
---------
RT: scheduler migration/wakeup enhancements
This series applies to 23.1-rt5 and includes numerous tweaks to the scheduler. The primary goal of this set of patches is to further improve wake-up latencies (particularly on larger SMP systems) and decrease migration overhead. This is accomplished by making improvements on several fronts:
1) We factor in CPU topology in the routing decision to pick the best migration target according to cache hierarchy.
2) We moved some CFS load calculation code as a member function of the CFS sched_class. This removes this unecessary code from the RT fastpath for tasks in the RT sched_class.
3) We make further improvements against non-migratable tasks by factoring in the RQ overload state, instead of just the RQ depth.
4) We replace the linear priority search with a 2-d algorithm.
In past -rt releases, latencies could become quickly absymal on larger SMP (8+ cpus) to the order of 350us+. The recent work in -rt2 and -rt4 dropped this figure by a large margin, bringing things in the order of approximately ~120us. This new series improves upon this work even further, bringing latencies down to the sub 80us mark on our reference 8-way Intel C2D 5335 Xeon.
These figures were obtained by simultaneous execution of:
# ./cyclictest -n -p 90 -t 8 -d 100 -i 100 # while true; do make mrproper; make alldefconfig; make -j 128; done
for long durations. The following are the results from one particular run, though the results are similar across various short and long term trials in our labs.
23.1-rt5-baseline
--------------------------
138.60 110.62 70.96 23/808 10246
T: 0 ( 5179) P:90 I:100 C:9011636 Min: 2 Act: 4 Avg: 4 Max: 117
T: 1 ( 5180) P:89 I:200 C:4505819 Min: 2 Act: 5 Avg: 4 Max: 95
T: 2 ( 5181) P:88 I:300 C:3003879 Min: 2 Act: 9 Avg: 5 Max: 85
T: 3 ( 5182) P:87 I:400 C:2252910 Min: 2 Act: 3 Avg: 4 Max: 75
T: 4 ( 5183) P:86 I:500 C:1802328 Min: 2 Act: 7 Avg: 5 Max: 71
T: 5 ( 5184) P:85 I:600 C:1501940 Min: 2 Act: 4 Avg: 5 Max: 74
T: 6 ( 5185) P:84 I:700 C:1287377 Min: 2 Act: 6 Avg: 6 Max: 85
T: 7 ( 5186) P:83 I:800 C:1126455 Min: 2 Act: 4 Avg: 5 Max: 75
23.1-rt5-gh
--------------------------
146.47 127.99 85.35 30/815 32289
T: 0 ( 5027) P:90 I:100 C:10856538 Min: 2 Act: 4 Avg: 4 Max:
60
T: 1 ( 5028) P:89 I:200 C:5428270 Min: 2 Act: 7 Avg: 5 Max: 57
T: 2 ( 5029) P:88 I:300 C:3618846 Min: 2 Act: 5 Avg: 5 Max: 48
T: 3 ( 5030) P:87 I:400 C:2714135 Min: 2 Act: 7 Avg: 5 Max: 61
T: 4 ( 5031) P:86 I:500 C:2171308 Min: 2 Act: 6 Avg: 6 Max: 51
T: 5 ( 5032) P:85 I:600 C:1809424 Min: 2 Act: 5 Avg: 7 Max: 59
T: 6 ( 5033) P:84 I:700 C:1550935 Min: 2 Act: 6 Avg: 6 Max: 54
T: 7 ( 5034) P:83 I:800 C:1357068 Min: 2 Act: 7 Avg: 6 Max: 62
PATCH 1/8 RT: Consistency cleanup for this_rq usage
"this_rq" is normally used to denote the RQ on the current cpu (i.e. "cpu_rq(this_cpu)"). So clean up the usage of this_rq to be more consistent with the rest of the code.
---
kernel/sched_rt.c | 46 +++++++++++++++++++++++----------------------- 1 files changed, 23 insertions(+), 23 deletions(-)
kernel/sched_rt.c +23 -23
@@ -307,7 +307,7 @@ static struct task_struct *pick_next_highest_task_rt(struct rq *rq,/* Will lock the rq it finds */ static struct rq *find_lock_lowest_rq (struct task_struct *task, -struct rq *this_rq) +struct rq *rq) {struct rq *lowest_rq =NULL ;cpumask_t cpu_mask;
@@ -321,21 +321,21 @@ static struct rq *find_lock_lowest_rq(struct task_struct *task,* Scan each rq for the lowest prio. */ for_each_cpu_mask (cpu, cpu_mask) { -struct rq *rq = &per_cpu (runqueues, cpu); +struct rq *curr_rq = &per_cpu (runqueues, cpu); -if (cpu == this_rq->cpu) +if (cpu == rq->cpu)continue ;/* We look for lowest RT prio or non-rt CPU */ -if (rq->rt.highest_prio >=MAX_RT_PRIO ) { - lowest_rq = rq; +if (curr_rq->rt.highest_prio >=MAX_RT_PRIO ) { + lowest_rq = curr_rq;break ; }/* no locking for now */ -if (rq->rt.highest_prio > task->prio && - (!lowest_rq || rq->rt.highest_prio > lowest_rq->rt.highest_prio)) { - lowest_rq = rq; +if (curr_rq->rt.highest_prio > task->prio && + (!lowest_rq || curr_rq->rt.highest_prio > lowest_rq->rt.highest_prio)) { + lowest_rq = curr_rq; } }
@@ -343,16 +343,16 @@ static struct rq *find_lock_lowest_rq(struct task_struct *task,break ;/* if the prio of this runqueue changed, try again */ -if (double_lock_balance (this_rq, lowest_rq)) { +if (double_lock_balance (rq, lowest_rq)) {/* * We had to unlock the run queue. In * the mean time, task could have * migrated already or had its affinity changed. * Also make sure that it wasn't scheduled on its rq. */ -if (unlikely (task_rq (task) != this_rq || +if (unlikely (task_rq (task) != rq || !cpu_isset (lowest_rq->cpu, task->cpus_allowed) || -task_running (this_rq, task) || +task_running (rq, task) || !task->se.on_rq)) {spin_unlock (&lowest_rq->lock); lowest_rq =NULL ;
@@ -377,21 +377,21 @@ static struct rq *find_lock_lowest_rq(struct task_struct *task,* running task can migrate over to a CPU that is running a task * of lesser priority. */ -static int push_rt_task (struct rq *this_rq) +static int push_rt_task (struct rq *rq) {struct task_struct *next_task;struct rq *lowest_rq;int ret =0 ;int paranoid =RT_MAX_TRIES ; -assert_spin_locked (&this_rq->lock); +assert_spin_locked (&rq->lock); - next_task =pick_next_highest_task_rt (this_rq,-1 ); + next_task =pick_next_highest_task_rt (rq,-1 );if (!next_task)return 0 ; retry: -if (unlikely (next_task == this_rq->curr)) { +if (unlikely (next_task == rq->curr)) {WARN_ON (1 );return 0 ; }
@@ -401,24 +401,24 @@ static int push_rt_task(struct rq *this_rq)* higher priority than current. If that's the case * just reschedule current. */ -if (unlikely (next_task->prio < this_rq->curr->prio)) { -resched_task (this_rq->curr); +if (unlikely (next_task->prio < rq->curr->prio)) { +resched_task (rq->curr);return 0 ; } -/* We might release this_rq lock */ +/* We might release rq lock */ get_task_struct (next_task);/* find_lock_lowest_rq locks the rq if found */ - lowest_rq =find_lock_lowest_rq (next_task, this_rq); + lowest_rq =find_lock_lowest_rq (next_task, rq);if (!lowest_rq) {struct task_struct *task;/* -* find lock_lowest_rq releases this_rq->lock +* find lock_lowest_rq releases rq->lock * so it is possible that next_task has changed. * If it has, then try again. */ - task =pick_next_highest_task_rt (this_rq,-1 ); + task =pick_next_highest_task_rt (rq,-1 );if (unlikely (task != next_task) && task && paranoid--) {put_task_struct (next_task); next_task = task;
@@ -429,13 +429,13 @@ static int push_rt_task(struct rq *this_rq)assert_spin_locked (&lowest_rq->lock); -deactivate_task (this_rq, next_task,0 ); +deactivate_task (rq, next_task,0 );set_task_cpu (next_task, lowest_rq->cpu);activate_task (lowest_rq, next_task,0 );resched_task (lowest_rq->curr); -schedstat_inc (this_rq, rto_pushed); +schedstat_inc (rq, rto_pushed);spin_unlock (&lowest_rq->lock);
PATCH 2/8 RT: Remove some CFS specific code from the wakeup path of RT tasks
In reply to Gregory HaskinsThe current wake-up code path tries to determine if it can optimize the wake-up to "this_cpu" by computing load calculations. The problem is that these calculations are only relevant to CFS tasks where load is king. For RT tasks, priority is king. So the load calculation is completely wasted bandwidth.
Therefore, we create a new sched_class interface to help with pre-wakeup routing decisions and move the load calculation as a function of CFS task's class.
---
include/linux/sched.h | 1 kernel/sched.c | 135 ++++------------------------------------------- kernel/sched_fair.c | 134 +++++++++++++++++++++++++++++++++++++++++++++++ kernel/sched_idletask.c | 9 +++ kernel/sched_rt.c | 10 +++ 5 files changed, 165 insertions(+), 124 deletions(-)
include/linux/sched.h +1 -0
@@ -1033,6 +1033,7 @@ struct sched_class {void (*enqueue_task) (struct rq *rq,struct task_struct *p,int wakeup);void (*dequeue_task) (struct rq *rq,struct task_struct *p,int sleep);void (*yield_task) (struct rq *rq,struct task_struct *p); +int (*select_task_rq)(struct task_struct *p,int sync);void (*check_preempt_curr) (struct rq *rq,struct task_struct *p);
kernel/sched.c +11 -124
180 lines not on this page · raw message
kernel/sched_fair.c +134 -0
146 lines not on this page · raw message
kernel/sched_idletask.c +9 -0
@@ -5,6 +5,12 @@* handled in sched_fair.c) */ +#ifdef CONFIG_SMP +static int select_task_rq_idle (struct task_struct *p,int sync) +{ +return task_cpu (p);/* IDLE tasks as never migrated */ +} +#endif /* CONFIG_SMP */ /* * Idle tasks are unconditionally rescheduled: */
@@ -58,6 +64,9 @@ static struct sched_class idle_sched_class __read_mostly = {/* dequeue is not valid, we print a debug message there: */ .dequeue_task = dequeue_task_idle, +#ifdef CONFIG_SMP + .select_task_rq = select_task_rq_idle, +#endif /* CONFIG_SMP */ .check_preempt_curr = check_preempt_curr_idle,
kernel/sched_rt.c +10 -0
@@ -202,6 +202,13 @@ yield_task_rt(struct rq *rq, struct task_struct *p)requeue_task_rt (rq, p); } +#ifdef CONFIG_SMP +static int select_task_rq_rt (struct task_struct *p,int sync) +{ +return task_cpu (p); +} +#endif /* CONFIG_SMP */ +/* * Preempt the current task with a newly woken task if needed: */
@@ -718,6 +725,9 @@ static struct sched_class rt_sched_class __read_mostly = { .enqueue_task = enqueue_task_rt, .dequeue_task = dequeue_task_rt, .yield_task = yield_task_rt, +#ifdef CONFIG_SMP + .select_task_rq = select_task_rq_rt, +#endif /* CONFIG_SMP */ .check_preempt_curr = check_preempt_curr_rt,
PATCH 3/8 RT: Break out the search function
In reply to Gregory HaskinsIsolate the search logic into a function so that it can be used later in places other than find_locked_lowest_rq().
---
kernel/sched_rt.c | 62 ++++++++++++++++++++++++++++++++--------------------- 1 files changed, 37 insertions(+), 25 deletions(-)
kernel/sched_rt.c +37 -25
@@ -312,43 +312,55 @@ static struct task_struct *pick_next_highest_task_rt(struct rq *rq,return next; } -/* Will lock the rq it finds */ -static struct rq *find_lock_lowest_rq (struct task_struct *task, -struct rq *rq) +static int find_lowest_rq (struct task_struct *task) { -struct rq *lowest_rq =NULL ; -cpumask_t cpu_mask;int cpu; -int tries; +cpumask_t cpu_mask; +struct rq *lowest_rq =NULL ;cpus_and (cpu_mask, cpu_online_map, task->cpus_allowed); -for (tries =0 ; tries <RT_MAX_TRIES ; tries++) { -/* -* Scan each rq for the lowest prio. -*/ -for_each_cpu_mask (cpu, cpu_mask) { -struct rq *curr_rq = &per_cpu (runqueues, cpu); +/* +* Scan each rq for the lowest prio. +*/ +for_each_cpu_mask (cpu, cpu_mask) { +struct rq *rq =cpu_rq (cpu); -if (cpu == rq->cpu) -continue ; +if (cpu == rq->cpu) +continue ; -/* We look for lowest RT prio or non-rt CPU */ -if (curr_rq->rt.highest_prio >=MAX_RT_PRIO ) { - lowest_rq = curr_rq; -break ; - } +/* We look for lowest RT prio or non-rt CPU */ +if (rq->rt.highest_prio >=MAX_RT_PRIO ) { + lowest_rq = rq; +break ; + } -/* no locking for now */ -if (curr_rq->rt.highest_prio > task->prio && - (!lowest_rq || curr_rq->rt.highest_prio > lowest_rq->rt.highest_prio)) { - lowest_rq = curr_rq; - } +/* no locking for now */ +if (rq->rt.highest_prio > task->prio && + (!lowest_rq || rq->rt.highest_prio > lowest_rq->rt.highest_prio)) { + lowest_rq = rq; } + } + +return lowest_rq ? lowest_rq->cpu :-1 ; +} + +/* Will lock the rq it finds */ +static struct rq *find_lock_lowest_rq (struct task_struct *task, +struct rq *rq) +{ +struct rq *lowest_rq =NULL ; +int cpu; +int tries; + +for (tries =0 ; tries <RT_MAX_TRIES ; tries++) { + cpu =find_lowest_rq (task); -if (!lowest_rq) +if (cpu ==-1 )break ; + lowest_rq =cpu_rq (cpu); +/* if the prio of this runqueue changed, try again */ if (double_lock_balance (rq, lowest_rq)) {/*
PATCH 4/8 RT: Allow current_cpu to be included in search
In reply to Gregory HaskinsIt doesn't hurt if we allow the current CPU to be included in the search. We will just simply skip it later if the current CPU turns out to be the lowest.
We will use this later in the series
---
kernel/sched_rt.c | 5 +---- 1 files changed, 1 insertions(+), 4 deletions(-)
kernel/sched_rt.c +1 -4
@@ -326,9 +326,6 @@ static int find_lowest_rq(struct task_struct *task)for_each_cpu_mask (cpu, cpu_mask) {struct rq *rq =cpu_rq (cpu); -if (cpu == rq->cpu) -continue ; -/* We look for lowest RT prio or non-rt CPU */ if (rq->rt.highest_prio >=MAX_RT_PRIO ) { lowest_rq = rq;
@@ -356,7 +353,7 @@ static struct rq *find_lock_lowest_rq(struct task_struct *task,for (tries =0 ; tries <RT_MAX_TRIES ; tries++) { cpu =find_lowest_rq (task); -if (cpu ==-1 ) +if ((cpu ==-1 ) || (cpu == rq->cpu))break ; lowest_rq =cpu_rq (cpu);
PATCH 5/8 RT: Pre-route RT tasks on wakeup
In reply to Gregory HaskinsIn the original patch series that Steven Rostedt and I worked on together, we both took different approaches to low-priority wakeup path. I utilized "pre-routing" (push the task away to a less important RQ before activating) approach, while Steve utilized a "post-routing" approach. The advantage of my approach is that you avoid the overhead of a wasted activate/deactivate cycle and peripherally related burdens. The advantage of Steve's method is that it neatly solves an issue preventing a "pull" optimization from being deployed.
In the end, we ended up deploying Steve's idea. But it later dawned on me that we could get the best of both worlds by deploying both ideas together, albeit slightly modified.
The idea is simple: Use a "light-weight" lookup for pre-routing, since we only need to approximate a good home for the task. And we also retain the post-routing push logic to clean up any inaccuracies caused by a condition of "priority mistargeting" caused by the lightweight lookup. Most of the time, the pre-routing should work and yield lower overhead. In the cases where it doesnt, the post-router will bat cleanup.
---
kernel/sched_rt.c | 19 +++++++++++++++++++ 1 files changed, 19 insertions(+), 0 deletions(-)
kernel/sched_rt.c +19 -0
@@ -202,9 +202,28 @@ yield_task_rt(struct rq *rq, struct task_struct *p)requeue_task_rt (rq, p); } +static int find_lowest_rq (struct task_struct *task); +#ifdef CONFIG_SMP static int select_task_rq_rt (struct task_struct *p,int sync) { +struct rq *rq =task_rq (p); + +/* +* If the task will not preempt the RQ, try to find a better RQ +* before we even activate the task +*/ +if ((p->prio >= rq->rt.highest_prio) + && (p->nr_cpus_allowed >1 )) { +int cpu =find_lowest_rq (p); + +return (cpu ==-1 ) ?task_cpu (p) : cpu; + } + +/* +* Otherwise, just let it ride on the affined RQ and the +* post-schedule router will push the preempted task away +*/ return task_cpu (p); }#endif /* CONFIG_SMP */
PATCH 6/8 RT: Optimize our cpu selection based on topology
In reply to Gregory HaskinsThe current code base assumes a relatively flat CPU/core topology and will route RT tasks to any CPU fairly equally. In the real world, there are various toplogies and affinities that govern where a task is best suited to run with the smallest amount of overhead. NUMA and multi-core CPUs are prime examples of topologies that can impact cache performance.
Fortunately, linux is already structured to represent these topologies via the sched_domains interface. So we change our RT router to consult a combination of topology and affinity policy to best place tasks during migration.
---
kernel/sched.c | 1 + kernel/sched_rt.c | 99 +++++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 88 insertions(+), 12 deletions(-)
kernel/sched.c +1 -0
@@ -26,6 +26,7 @@* 2007-07-01 Group scheduling enhancements by Srivatsa Vaddagiri * 2007-10-22 RT overload balancing by Steven Rostedt * (with thanks to Gregory Haskins) +* 2007-11-05 RT migration/wakeup tuning by Gregory Haskins */ #include <linux/mm.h>
kernel/sched_rt.c +87 -12
121 lines not on this page · raw message
PATCH 7/8 RT: Optimize rebalancing
In reply to Gregory HaskinsWe have logic to detect whether the system has migratable tasks, but we are not using it when deciding whether to push tasks away. So we add support for considering this new information.
---
kernel/sched.c | 2 ++ kernel/sched_rt.c | 10 ++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-)
kernel/sched.c +2 -0
@@ -268,6 +268,7 @@ struct rt_rq {unsigned long rt_nr_uninterruptible;/* highest queued rt task prio */ int highest_prio; +int overloaded; };/*
@@ -6869,6 +6870,7 @@ void __init sched_init(void) rq->migration_thread =NULL ;INIT_LIST_HEAD (&rq->migration_queue); rq->rt.highest_prio =MAX_RT_PRIO ; + rq->rt.overloaded =0 ;#endif atomic_set (&rq->nr_iowait,0 );
kernel/sched_rt.c +8 -2
@@ -28,10 +28,12 @@ static inline cpumask_t *rt_overload(struct rq *rq)static inline void rt_set_overload (struct rq *rq) {cpu_set (rq->cpu, *rt_overload_mask (rq->cpu)); + rq->rt.overloaded =1 ; }static inline void rt_clear_overload (struct rq *rq) {cpu_clear (rq->cpu, *rt_overload_mask (rq->cpu)); + rq->rt.overloaded =0 ; }static void update_rt_migration (struct task_struct *p,struct rq *rq)
@@ -496,6 +498,9 @@ static int push_rt_task(struct rq *rq)assert_spin_locked (&rq->lock); +if (!rq->rt.overloaded) +return 0 ; + next_task =pick_next_highest_task_rt (rq,-1 );if (!next_task)return 0 ;
@@ -737,7 +742,7 @@ static void schedule_tail_balance_rt(struct rq *rq)* the lock was owned by prev, we need to release it * first via finish_lock_switch and then reaquire it here. */ -if (unlikely (rq->rt.rt_nr_running >1 )) { +if (unlikely (rq->rt.overloaded)) {spin_lock (&rq->lock);push_rt_tasks (rq);schedstat_inc (rq, rto_schedule_tail);
@@ -749,7 +754,8 @@ static void wakeup_balance_rt(struct rq *rq, struct task_struct *p) {if (unlikely (rt_task (p)) && !task_running (rq, p) && - (p->prio >= rq->curr->prio)) { + (p->prio >= rq->rt.highest_prio) && + rq->rt.overloaded) {push_rt_tasks (rq);schedstat_inc (rq, rto_wakeup); }
PATCH 8/8 RT: Use a 2-d bitmap for searching lowest-pri CPU
In reply to Gregory HaskinsThe current code use a linear algorithm which causes scaling issues on larger SMP machines. This patch replaces that algorithm with a 2-dimensional bitmap to reduce latencies in the wake-up path.
---
kernel/Makefile | 1 kernel/sched.c | 4 + kernel/sched_cpupri.c | 186 +++++++++++++++++++++++++++++++++++++++++++++++++ kernel/sched_cpupri.h | 10 +++ kernel/sched_rt.c | 52 ++------------ 5 files changed, 210 insertions(+), 43 deletions(-)
kernel/Makefile +1 -0
@@ -66,6 +66,7 @@ obj-$(CONFIG_RELAY) += relay.o obj-$(CONFIG_SYSCTL) += utsname_sysctl.o obj-$(CONFIG_TASK_DELAY_ACCT) += delayacct.o obj-$(CONFIG_TASKSTATS) += taskstats.o tsacct.o +obj-$(CONFIG_SMP) += sched_cpupri.o ifneq ($(CONFIG_SCHED_NO_NO_OMIT_FRAME_POINTER),y) # According to Alan Modra <[email protected]>, the -fno-omit-frame-pointer is
kernel/sched.c +4 -0
@@ -70,6 +70,8 @@#include <asm/tlb.h> +#include "sched_cpupri.h" +/* * Scheduler clock - returns current time in nanosec units. * This is default implementation.
@@ -6842,6 +6844,8 @@ void __init sched_init(void) fair_sched_class.next = &idle_sched_class; idle_sched_class.next =NULL ; +cpupri_init (); +for_each_possible_cpu (i) {struct rt_prio_array *array;struct rq *rq;
kernel/sched_cpupri.c new file +186 -0
186 lines not on this page · raw message
kernel/sched_cpupri.h new file +10 -0
@@ -0,0 +1,10 @@ +#ifndef _LINUX_CPUPRI_H +#define _LINUX_CPUPRI_H + +#include <linux/sched.h> + +int cpupri_find (struct task_struct *p,cpumask_t *lowest_mask); +void cpupri_set (int cpu,int pri); +void cpupri_init (void ); + +#endif /* _LINUX_CPUPRI_H */
kernel/sched_rt.c +9 -43
78 lines not on this page · raw message