sched/fair: Fix race between runtime distribution and assignment
authorHuaixin Chang <changhuaixin@linux.alibaba.com>
Fri, 27 Mar 2020 03:26:25 +0000 (11:26 +0800)
committerIngo Molnar <mingo@kernel.org>
Wed, 8 Apr 2020 09:35:19 +0000 (11:35 +0200)
Currently, there is a potential race between distribute_cfs_runtime()
and assign_cfs_rq_runtime(). Race happens when cfs_b->runtime is read,
distributes without holding lock and finds out there is not enough
runtime to charge against after distribution. Because
assign_cfs_rq_runtime() might be called during distribution, and use
cfs_b->runtime at the same time.

Fibtest is the tool to test this race. Assume all gcfs_rq is throttled
and cfs period timer runs, slow threads might run and sleep, returning
unused cfs_rq runtime and keeping min_cfs_rq_runtime in their local
pool. If all this happens sufficiently quickly, cfs_b->runtime will drop
a lot. If runtime distributed is large too, over-use of runtime happens.

A runtime over-using by about 70 percent of quota is seen when we
test fibtest on a 96-core machine. We run fibtest with 1 fast thread and
95 slow threads in test group, configure 10ms quota for this group and
see the CPU usage of fibtest is 17.0%, which is far more than the
expected 10%.

On a smaller machine with 32 cores, we also run fibtest with 96
threads. CPU usage is more than 12%, which is also more than expected
10%. This shows that on similar workloads, this race do affect CPU
bandwidth control.

Solve this by holding lock inside distribute_cfs_runtime().

Fixes: c06f04c70489 ("sched: Fix potential near-infinite distribute_cfs_runtime() loop")
Reviewed-by: Ben Segall <bsegall@google.com>
Signed-off-by: Huaixin Chang <changhuaixin@linux.alibaba.com>
Signed-off-by: Peter Zijlstra (Intel) <peterz@infradead.org>
Signed-off-by: Ingo Molnar <mingo@kernel.org>
Link: https://lore.kernel.org/lkml/20200325092602.22471-1-changhuaixin@linux.alibaba.com/
kernel/sched/fair.c

index fb025e9..95cbd9e 100644 (file)
@@ -4836,11 +4836,10 @@ void unthrottle_cfs_rq(struct cfs_rq *cfs_rq)
                resched_curr(rq);
 }
 
-static u64 distribute_cfs_runtime(struct cfs_bandwidth *cfs_b, u64 remaining)
+static void distribute_cfs_runtime(struct cfs_bandwidth *cfs_b)
 {
        struct cfs_rq *cfs_rq;
-       u64 runtime;
-       u64 starting_runtime = remaining;
+       u64 runtime, remaining = 1;
 
        rcu_read_lock();
        list_for_each_entry_rcu(cfs_rq, &cfs_b->throttled_cfs_rq,
@@ -4855,10 +4854,13 @@ static u64 distribute_cfs_runtime(struct cfs_bandwidth *cfs_b, u64 remaining)
                /* By the above check, this should never be true */
                SCHED_WARN_ON(cfs_rq->runtime_remaining > 0);
 
+               raw_spin_lock(&cfs_b->lock);
                runtime = -cfs_rq->runtime_remaining + 1;
-               if (runtime > remaining)
-                       runtime = remaining;
-               remaining -= runtime;
+               if (runtime > cfs_b->runtime)
+                       runtime = cfs_b->runtime;
+               cfs_b->runtime -= runtime;
+               remaining = cfs_b->runtime;
+               raw_spin_unlock(&cfs_b->lock);
 
                cfs_rq->runtime_remaining += runtime;
 
@@ -4873,8 +4875,6 @@ next:
                        break;
        }
        rcu_read_unlock();
-
-       return starting_runtime - remaining;
 }
 
 /*
@@ -4885,7 +4885,6 @@ next:
  */
 static int do_sched_cfs_period_timer(struct cfs_bandwidth *cfs_b, int overrun, unsigned long flags)
 {
-       u64 runtime;
        int throttled;
 
        /* no need to continue the timer with no bandwidth constraint */
@@ -4914,24 +4913,17 @@ static int do_sched_cfs_period_timer(struct cfs_bandwidth *cfs_b, int overrun, u
        cfs_b->nr_throttled += overrun;
 
        /*
-        * This check is repeated as we are holding onto the new bandwidth while
-        * we unthrottle. This can potentially race with an unthrottled group
-        * trying to acquire new bandwidth from the global pool. This can result
-        * in us over-using our runtime if it is all used during this loop, but
-        * only by limited amounts in that extreme case.
+        * This check is repeated as we release cfs_b->lock while we unthrottle.
         */
        while (throttled && cfs_b->runtime > 0 && !cfs_b->distribute_running) {
-               runtime = cfs_b->runtime;
                cfs_b->distribute_running = 1;
                raw_spin_unlock_irqrestore(&cfs_b->lock, flags);
                /* we can't nest cfs_b->lock while distributing bandwidth */
-               runtime = distribute_cfs_runtime(cfs_b, runtime);
+               distribute_cfs_runtime(cfs_b);
                raw_spin_lock_irqsave(&cfs_b->lock, flags);
 
                cfs_b->distribute_running = 0;
                throttled = !list_empty(&cfs_b->throttled_cfs_rq);
-
-               lsub_positive(&cfs_b->runtime, runtime);
        }
 
        /*
@@ -5065,10 +5057,9 @@ static void do_sched_cfs_slack_timer(struct cfs_bandwidth *cfs_b)
        if (!runtime)
                return;
 
-       runtime = distribute_cfs_runtime(cfs_b, runtime);
+       distribute_cfs_runtime(cfs_b);
 
        raw_spin_lock_irqsave(&cfs_b->lock, flags);
-       lsub_positive(&cfs_b->runtime, runtime);
        cfs_b->distribute_running = 0;
        raw_spin_unlock_irqrestore(&cfs_b->lock, flags);
 }