[<prev] [next>] [<thread-prev] [thread-next>] [day] [month] [year] [list]
Message-ID: <cd44676f-ab39-4b59-8215-7986b5a3e21b@redhat.com>
Date: Tue, 3 Feb 2026 23:48:57 -0500
From: Waiman Long <llong@...hat.com>
To: Chen Ridong <chenridong@...weicloud.com>, Tejun Heo <tj@...nel.org>,
Johannes Weiner <hannes@...xchg.org>, Michal Koutný
<mkoutny@...e.com>, Ingo Molnar <mingo@...hat.com>,
Peter Zijlstra <peterz@...radead.org>, Juri Lelli <juri.lelli@...hat.com>,
Vincent Guittot <vincent.guittot@...aro.org>,
Steven Rostedt <rostedt@...dmis.org>, Ben Segall <bsegall@...gle.com>,
Mel Gorman <mgorman@...e.de>, Valentin Schneider <vschneid@...hat.com>,
Anna-Maria Behnsen <anna-maria@...utronix.de>,
Frederic Weisbecker <frederic@...nel.org>,
Thomas Gleixner <tglx@...utronix.de>, Shuah Khan <shuah@...nel.org>
Cc: cgroups@...r.kernel.org, linux-kernel@...r.kernel.org,
linux-kselftest@...r.kernel.org
Subject: Re: [PATCH/for-next v3 3/3] cgroup/cpuset: Call housekeeping_update()
without holding cpus_read_lock
On 2/3/26 9:44 PM, Chen Ridong wrote:
>
> On 2026/2/3 4:11, Waiman Long wrote:
>> The current cpuset partition code is able to dynamically update
>> the sched domains of a running system and the corresponding
>> HK_TYPE_DOMAIN housekeeping cpumask to perform what is essentally the
>> "isolcpus=domain,..." boot command line feature at run time.
>>
>> The housekeeping cpumask update requires flushing a number of different
>> workqueues which may not be safe with cpus_read_lock() held as the
>> workqueue flushing code may acquire cpus_read_lock() or acquiring locks
>> which have locking dependency with cpus_read_lock() down the chain. Below
>> is an example of such circular locking problem.
>>
>> ======================================================
>> WARNING: possible circular locking dependency detected
>> 6.18.0-test+ #2 Tainted: G S
>> ------------------------------------------------------
>> test_cpuset_prs/10971 is trying to acquire lock:
>> ffff888112ba4958 ((wq_completion)sync_wq){+.+.}-{0:0}, at: touch_wq_lockdep_map+0x7a/0x180
>>
>> but task is already holding lock:
>> ffffffffae47f450 (cpuset_mutex){+.+.}-{4:4}, at: cpuset_partition_write+0x85/0x130
>>
>> which lock already depends on the new lock.
>>
>> the existing dependency chain (in reverse order) is:
>> -> #4 (cpuset_mutex){+.+.}-{4:4}:
>> -> #3 (cpu_hotplug_lock){++++}-{0:0}:
>> -> #2 (rtnl_mutex){+.+.}-{4:4}:
>> -> #1 ((work_completion)(&arg.work)){+.+.}-{0:0}:
>> -> #0 ((wq_completion)sync_wq){+.+.}-{0:0}:
>>
>> Chain exists of:
>> (wq_completion)sync_wq --> cpu_hotplug_lock --> cpuset_mutex
>>
>> 5 locks held by test_cpuset_prs/10971:
>> #0: ffff88816810e440 (sb_writers#7){.+.+}-{0:0}, at: ksys_write+0xf9/0x1d0
>> #1: ffff8891ab620890 (&of->mutex#2){+.+.}-{4:4}, at: kernfs_fop_write_iter+0x260/0x5f0
>> #2: ffff8890a78b83e8 (kn->active#187){.+.+}-{0:0}, at: kernfs_fop_write_iter+0x2b6/0x5f0
>> #3: ffffffffadf32900 (cpu_hotplug_lock){++++}-{0:0}, at: cpuset_partition_write+0x77/0x130
>> #4: ffffffffae47f450 (cpuset_mutex){+.+.}-{4:4}, at: cpuset_partition_write+0x85/0x130
>>
>> Call Trace:
>> <TASK>
>> :
>> touch_wq_lockdep_map+0x93/0x180
>> __flush_workqueue+0x111/0x10b0
>> housekeeping_update+0x12d/0x2d0
>> update_parent_effective_cpumask+0x595/0x2440
>> update_prstate+0x89d/0xce0
>> cpuset_partition_write+0xc5/0x130
>> cgroup_file_write+0x1a5/0x680
>> kernfs_fop_write_iter+0x3df/0x5f0
>> vfs_write+0x525/0xfd0
>> ksys_write+0xf9/0x1d0
>> do_syscall_64+0x95/0x520
>> entry_SYSCALL_64_after_hwframe+0x76/0x7e
>>
>> To avoid such a circular locking dependency problem, we have to
>> call housekeeping_update() without holding the cpus_read_lock() and
>> cpuset_mutex. The current set of wq's flushed by housekeeping_update()
>> may not have work functions that call cpus_read_lock() directly,
>> but we are likely to extend the list of wq's that are flushed in the
>> future. Moreover, the current set of work functions may hold locks that
>> may have cpu_hotplug_lock down the dependency chain.
>>
>> One way to do that is to defer the housekeeping_update() call after
>> the current cpuset critical section has finished without holding
>> cpus_read_lock. For cpuset control file write, this can be done by
>> deferring it using task_work right before returning to userspace.
>>
>> To enable mutual exclusion between the housekeeping_update() call and
>> other cpuset control file write actions, a new top level cpuset_top_mutex
>> is introduced. This new mutex will be acquired first to allow sharing
>> variables used by both code paths. However, cpuset update from CPU
>> hotplug can still happen in parallel with the housekeeping_update()
>> call, though that should be rare in production environment.
>>
>> As cpus_read_lock() is now no longer held when
>> tmigr_isolated_exclude_cpumask() is called, it needs to acquire it
>> directly.
>>
>> The lockdep_is_cpuset_held() is also updated to check the new
>> cpuset_top_mutex.
>>
>> Signed-off-by: Waiman Long <longman@...hat.com>
>> ---
>> kernel/cgroup/cpuset.c | 103 +++++++++++++++++++++++++++-------
>> kernel/sched/isolation.c | 4 +-
>> kernel/time/timer_migration.c | 3 +-
>> 3 files changed, 86 insertions(+), 24 deletions(-)
>>
>> diff --git a/kernel/cgroup/cpuset.c b/kernel/cgroup/cpuset.c
>> index e98a2e953392..d2f51f40f87e 100644
>> --- a/kernel/cgroup/cpuset.c
>> +++ b/kernel/cgroup/cpuset.c
>> @@ -65,14 +65,28 @@ static const char * const perr_strings[] = {
>> * CPUSET Locking Convention
>> * -------------------------
>> *
>> - * Below are the three global locks guarding cpuset structures in lock
>> + * Below are the four global/local locks guarding cpuset structures in lock
>> * acquisition order:
>> + * - cpuset_top_mutex
>> * - cpu_hotplug_lock (cpus_read_lock/cpus_write_lock)
>> * - cpuset_mutex
>> * - callback_lock (raw spinlock)
>> *
>> - * A task must hold all the three locks to modify externally visible or
>> - * used fields of cpusets, though some of the internally used cpuset fields
>> + * As cpuset will now indirectly flush a number of different workqueues in
>> + * housekeeping_update() to update housekeeping cpumasks when the set of
>> + * isolated CPUs is going to be changed, it may be vulnerable to deadlock
>> + * if we hold cpus_read_lock while calling into housekeeping_update().
>> + *
>> + * The first cpuset_top_mutex will be held except when calling into
>> + * cpuset_handle_hotplug() from the CPU hotplug code where cpus_write_lock
>> + * and cpuset_mutex will be held instead. The main purpose of this mutex
>> + * is to prevent regular cpuset control file write actions from interfering
>> + * with the call to housekeeping_update(), though CPU hotplug operation can
>> + * still happen in parallel. This mutex also provides protection for some
>> + * internal variables.
>> + *
>> + * A task must hold all the remaining three locks to modify externally visible
>> + * or used fields of cpusets, though some of the internally used cpuset fields
>> * and internal variables can be modified without holding callback_lock. If only
>> * reliable read access of the externally used fields are needed, a task can
>> * hold either cpuset_mutex or callback_lock which are exposed to other
>> @@ -100,6 +114,7 @@ static const char * const perr_strings[] = {
>> * cpumasks and nodemasks.
>> */
>>
>> +static DEFINE_MUTEX(cpuset_top_mutex);
>> static DEFINE_MUTEX(cpuset_mutex);
>>
>> /*
>> @@ -111,6 +126,8 @@ static DEFINE_MUTEX(cpuset_mutex);
>> *
>> * CSCB: Readable by holding either cpuset_mutex or callback_lock. Writable
>> * by holding both cpuset_mutex and callback_lock.
>> + *
>> + * T: Read/write-able by holding the cpuset_top_mutex.
>> */
>>
>> /*
>> @@ -135,6 +152,13 @@ static cpumask_var_t isolated_cpus; /* CSCB */
>> */
>> static bool isolated_cpus_updating; /* RWCS */
>>
>> +/*
>> + * Copy of isolated_cpus to be processed by housekeeping_update()
>> + */
>> +static cpumask_var_t isolated_hk_cpus; /* T */
>> +static bool isolcpus_twork_queued; /* T */
>> +
>> +
>> /*
>> * A flag to force sched domain rebuild at the end of an operation.
>> * It can be set in
>> @@ -298,6 +322,7 @@ void lockdep_assert_cpuset_lock_held(void)
>> */
>> void cpuset_full_lock(void)
>> {
>> + mutex_lock(&cpuset_top_mutex);
>> cpus_read_lock();
>> mutex_lock(&cpuset_mutex);
>> }
>> @@ -306,12 +331,13 @@ void cpuset_full_unlock(void)
>> {
>> mutex_unlock(&cpuset_mutex);
>> cpus_read_unlock();
>> + mutex_unlock(&cpuset_top_mutex);
>> }
>>
>> #ifdef CONFIG_LOCKDEP
>> bool lockdep_is_cpuset_held(void)
>> {
>> - return lockdep_is_held(&cpuset_mutex);
>> + return lockdep_is_held(&cpuset_top_mutex);
>> }
>> #endif
>>
> void cpuset_lock(void)
> {
> mutex_lock(&cpuset_mutex);
> }
>
> void cpuset_unlock(void)
> {
> mutex_unlock(&cpuset_mutex);
> }
>
> void lockdep_assert_cpuset_lock_held(void)
> {
> lockdep_assert_held(&cpuset_mutex);
> }
>
> A potential issue is that lockdep_is_cpuset_held() only checks cpuset_top_mutex.
> In the call chain below, only cpuset_mutex is acquired:
>
> rebuild_sched_domains_cpuslocked ---only cpuset_mutex is acquired
> rebuild_sched_domains_locked
> partition_sched_domains
> dl_rebuild_rd_accounting
> dl_rebuild_rd_accounting
> dl_update_tasks_root_domain
> dl_add_task_root_domain
> dl_get_task_effective_cpus
> housekeeping_cpumask
> housekeeping_dereference_check
> if (IS_ENABLED(CONFIG_CPUSETS) && lockdep_is_cpuset_held())
>
> Since lockdep_is_cpuset_held() validates cpuset_top_mutex rather than
> cpuset_mutex, could this lead to false lockdep warnings?
Right, it should check for either cpuset_mutex or cpuset_top_mutex.
Thanks,
Longman
Powered by blists - more mailing lists