[PATCH v5 20/29] cgroup/cpuset: Give back the bandwidth an attach did not move
From: Mathieu Desnoyers
Date: Sun Nov 08 2026 - 13:57:22 EST
Commit 2ef269ef1ac0 ("cgroup/cpuset: Free DL BW in case can_attach()
fails") reserves the bandwidth of an attach as one sum.
cpuset_can_attach() adds up the bandwidth of the deadline tasks it
moves. Once all tasks pass their checks, it reserves the sum in the
destination root domain. If another controller refuses the attach,
cpuset_cancel_attach() frees the sum. If the attach goes through, the
move of each task uses its part of the sum.
But a task can change its policy, or die, before the attach:
- A task that left SCHED_DEADLINE, or died, moves no bandwidth. Its
part of the reservation stays in the destination's total_bw, until
the next rebuild of the accounting.
- A task that became a deadline task has nothing reserved. The attach
still moves its bandwidth. The destination lacks it, and its
total_bw underflows when the task leaves.
In testing, a task switched between SCHED_DEADLINE and SCHED_OTHER
while it was moved 200 times into a one-CPU partition root and back.
On v7.4-rc1 with the patches before this one, the partition's total_bw
ended wrong in 5 of 6 VMs with 8 CPUs. It kept up to two reservations,
or wrapped below zero.
Fix this by keeping the tally of what the attach moves, in
sum_moved_dl_bw. Flag the move of a task only if the rest of the
reservation covers its bandwidth. Once the attach is through, give back
what it did not move.
set_cpus_allowed_dl() decides the move under the runqueue lock.
cpuset_attach_task() does not hold it. dl_task_offline_migration() can
change the task's record meanwhile, for a task throttled on a CPU gone
offline. So the tally counts what set_cpus_allowed_dl() reports in the
affinity context.
With this patch, total_bw ended right in 6 VMs of 6. The random_mix case
of deadline_bw_test, added later in this series, found the bug. It could
no longer lower a fair server in a partition root with no room left.
Fixes: 2ef269ef1ac0 ("cgroup/cpuset: Free DL BW in case can_attach() fails")
Cc: stable@xxxxxxxxxxxxxxx
Signed-off-by: Mathieu Desnoyers <mathieu.desnoyers@xxxxxxxxxxxx>
---
include/linux/sched.h | 3 ++-
kernel/cgroup/cpuset-internal.h | 2 ++
kernel/cgroup/cpuset.c | 37 +++++++++++++++++++++++++++------
kernel/sched/core.c | 12 +++++++----
kernel/sched/deadline.c | 4 ++++
kernel/sched/sched.h | 2 ++
6 files changed, 49 insertions(+), 11 deletions(-)
diff --git a/include/linux/sched.h b/include/linux/sched.h
index 5f14005d737e..2afa50aa3e54 100644
--- a/include/linux/sched.h
+++ b/include/linux/sched.h
@@ -1980,7 +1980,8 @@ extern void set_cpus_allowed_force(struct task_struct *p, const struct cpumask *
*/
extern int set_cpus_allowed_ptr(struct task_struct *p, const struct cpumask *new_mask);
extern int set_cpus_allowed_bw_reserved(struct task_struct *p,
- const struct cpumask *new_mask);
+ const struct cpumask *new_mask,
+ u64 *moved);
extern int dup_user_cpus_ptr(struct task_struct *dst, struct task_struct *src, int node);
extern void release_user_cpus_ptr(struct task_struct *p);
extern int dl_task_check_affinity(struct task_struct *p, const struct cpumask *mask);
diff --git a/kernel/cgroup/cpuset-internal.h b/kernel/cgroup/cpuset-internal.h
index bf847953c78d..93aa46d76b04 100644
--- a/kernel/cgroup/cpuset-internal.h
+++ b/kernel/cgroup/cpuset-internal.h
@@ -170,6 +170,8 @@ struct cpuset {
int nr_migrate_dl_tasks;
/* DL bandwidth that needs destination reservation for this attach. */
u64 sum_migrate_dl_bw;
+ /* The part of sum_migrate_dl_bw moved. The rest is given back. */
+ u64 sum_moved_dl_bw;
/*
* CPU used for temporary DL bandwidth allocation during attach;
* -1 if no DL bandwidth was allocated in the current attach.
diff --git a/kernel/cgroup/cpuset.c b/kernel/cgroup/cpuset.c
index 51b6be460794..ed77fc915261 100644
--- a/kernel/cgroup/cpuset.c
+++ b/kernel/cgroup/cpuset.c
@@ -3129,10 +3129,24 @@ static void clear_attach_data(struct llist_head *head, bool cancel)
llist_for_each_entry_safe(cs, next, lnode, attach_node) {
init_llist_node(&cs->attach_node);
if (cs->nr_migrate_dl_tasks) {
- if (cancel && cs->dl_bw_cpu >= 0)
- dl_bw_free(cs->dl_bw_cpu, cs->sum_migrate_dl_bw);
+ /*
+ * Give back the bandwidth the attach did not move.
+ * If the attach is cancelled, that is all of the
+ * reservation. Otherwise, it is what no move used,
+ * such as the part for a task that left
+ * SCHED_DEADLINE. The class it left releases its own
+ * bandwidth, from the root domain that counts it.
+ */
+ if (cs->dl_bw_cpu >= 0) {
+ u64 bw = cancel ? cs->sum_migrate_dl_bw :
+ cs->sum_migrate_dl_bw - cs->sum_moved_dl_bw;
+
+ if (bw)
+ dl_bw_free(cs->dl_bw_cpu, bw);
+ }
cs->nr_migrate_dl_tasks = 0;
cs->sum_migrate_dl_bw = 0;
+ cs->sum_moved_dl_bw = 0;
cs->dl_bw_cpu = -1;
}
}
@@ -3285,17 +3299,28 @@ static void cpuset_attach_task(struct cpuset *cs, struct task_struct *task)
* cpuset_reserve_dl_bw() made room in @cs for the bandwidth of the
* deadline tasks that cpuset_can_attach() counted. Flag the move, so
* that the bandwidth of this task moves with it.
+ *
+ * Nothing was reserved for a task that became a deadline task since.
+ * Flag its move only if the rest of the reservation covers its
+ * bandwidth.
*/
- if (dl_task(task) && dl_task_needs_bw_move(task, cs->effective_cpus))
+ if (dl_task(task) && dl_task_needs_bw_move(task, cs->effective_cpus) &&
+ cs->sum_moved_dl_bw + task->dl.dl_bw <= cs->sum_migrate_dl_bw)
reserved = true;
/*
* can_attach beforehand should guarantee that this doesn't
* fail. TODO: have a better way to handle failure here
*/
- if (reserved)
- WARN_ON_ONCE(set_cpus_allowed_bw_reserved(task, cpus_attach));
- else
+ if (reserved) {
+ u64 moved;
+
+ /* Count what the move reports it moved. */
+ WARN_ON_ONCE(set_cpus_allowed_bw_reserved(task, cpus_attach,
+ &moved));
+ cs->sum_moved_dl_bw += moved;
+ } else {
WARN_ON_ONCE(set_cpus_allowed_ptr(task, cpus_attach));
+ }
if (cpuset_v2() && !attach_ctx.mems_updated)
return;
diff --git a/kernel/sched/core.c b/kernel/sched/core.c
index 030106b3d97f..3a538b42dcdd 100644
--- a/kernel/sched/core.c
+++ b/kernel/sched/core.c
@@ -3275,17 +3275,21 @@ EXPORT_SYMBOL_GPL(set_cpus_allowed_ptr);
/*
* Like set_cpus_allowed_ptr(), for a caller that has made room for the
* deadline bandwidth of @p in the root domain of @new_mask. Move that
- * bandwidth there with @p. Return 0 on success, or a negative error
- * code.
+ * bandwidth there with @p, and set @moved to the bandwidth moved, 0
+ * if none. Return 0 on success, or a negative error code.
*/
-int set_cpus_allowed_bw_reserved(struct task_struct *p, const struct cpumask *new_mask)
+int set_cpus_allowed_bw_reserved(struct task_struct *p,
+ const struct cpumask *new_mask, u64 *moved)
{
struct affinity_context ac = {
.new_mask = new_mask,
.flags = SCA_DL_BW_RESERVED,
};
+ int ret;
- return __set_cpus_allowed_ptr(p, &ac);
+ ret = __set_cpus_allowed_ptr(p, &ac);
+ *moved = ac.dl_bw_moved;
+ return ret;
}
/*
diff --git a/kernel/sched/deadline.c b/kernel/sched/deadline.c
index b365401e7148..0326527dd2d1 100644
--- a/kernel/sched/deadline.c
+++ b/kernel/sched/deadline.c
@@ -3571,6 +3571,9 @@ static void set_cpus_allowed_dl(struct task_struct *p,
* Otherwise, the bandwidth stays where it is counted, until the next
* rebuild of the accounting. Moving it without room would leave no
* root domain counting it.
+ *
+ * Report in @ctx the bandwidth moved. The caller read the record
+ * without the runqueue lock, so it must count only what this reports.
*/
if ((ctx->flags & SCA_DL_BW_RESERVED) &&
dl_task_needs_bw_move(p, ctx->new_mask)) {
@@ -3597,6 +3600,7 @@ static void set_cpus_allowed_dl(struct task_struct *p,
cpu = cpumask_any_and(ctx->new_mask, cpu_active_mask);
p->dl.dl_bw_cpu = cpu < nr_cpu_ids ? (int)cpu : -1;
p->dl.dl_bw_gen = dl_bw_gen;
+ ctx->dl_bw_moved = p->dl.dl_bw;
}
set_cpus_allowed_common(p, ctx);
diff --git a/kernel/sched/sched.h b/kernel/sched/sched.h
index d7b4cad28262..ed84b7b7161f 100644
--- a/kernel/sched/sched.h
+++ b/kernel/sched/sched.h
@@ -2663,6 +2663,8 @@ extern const u32 sched_prio_to_wmult[40];
struct affinity_context {
const struct cpumask *new_mask;
struct cpumask *user_mask;
+ /* Out: the deadline bandwidth moved to the new root domain. */
+ u64 dl_bw_moved;
unsigned int flags;
};
--
2.47.3