@CWills: yes, this does look like a bug.

Let's look at __lru_add_drain_all():

 770 /*
 771  * Doesn't need any cpu hotplug locking because we do rely on per-cpu
 772  * kworkers being shut down before our page_alloc_cpu_dead callback is
 773  * executed on the offlined cpu.
 774  * Calling this function with cpu hotplug locks held can actually lead
 775  * to obscure indirect dependencies via WQ context.
 776  */
 777 static inline void __lru_add_drain_all(bool force_all_cpus)
 778 {
...
 855     cpumask_clear(&has_work);
 856     for_each_online_cpu(cpu) {
 857         struct work_struct *work = &per_cpu(lru_add_drain_work, cpu);
 858 
 859         if (cpu_needs_drain(cpu)) {
 860             INIT_WORK(work, lru_add_drain_per_cpu);
 861             queue_work_on(cpu, mm_percpu_wq, work);
 862             __cpumask_set_cpu(cpu, &has_work);
 863         }
 864     }
 865 
 866     for_each_cpu(cpu, &has_work)
 867         flush_work(&per_cpu(lru_add_drain_work, cpu));
 868 
 869 done:
 870     mutex_unlock(&lock);
 871 }
 
queue_work_on() adds lru_add_drain_per_cpu() to a kworker thread running on each
individual CPU, and as we already know, these kworker threads never get an
opportunity to run until each core has re-entered the kernel / does a syscall /
or sleep. 

2491 bool queue_work_on(int cpu, struct workqueue_struct *wq,
2492            struct work_struct *work)
2493 {
2494     bool ret = false;
2495     unsigned long irq_flags;
2496 
2497     local_irq_save(irq_flags);
2498 
2499     if (!test_and_set_bit(WORK_STRUCT_PENDING_BIT, work_data_bits(work)) &&
2500         !clear_pending_if_disabled(work)) {
2501         __queue_work(cpu, wq, work);
2502         ret = true;
2503     }
2504 
2505     local_irq_restore(irq_flags);
2506     return ret;
2507 }

This pretty much just calls __queue_work():

2321 static void __queue_work(int cpu, struct workqueue_struct *wq,
2322              struct work_struct *work)
2323 {
...
2437 
2438     /*
2439      * Limit the number of concurrently active work items to max_active.
2440      * @work must also queue behind existing inactive work items to 
maintain
2441      * ordering when max_active changes. See wq_adjust_max_active().
2442      */
2443     if (list_empty(&pwq->inactive_works) && pwq_tryinc_nr_active(pwq, 
false)) {
2444         if (list_empty(&pool->worklist))
2445             pool->last_progress_ts = jiffies;
2446 
2447         trace_workqueue_activate_work(work);
2448         insert_work(pwq, work, &pool->worklist, work_flags);
2449         kick_pool_pick(pool, &wake_task);
2450     } else {
2451         work_flags |= WORK_STRUCT_INACTIVE;
2452         insert_work(pwq, work, &pwq->inactive_works, work_flags);
2453     }
2454 
2455 out:
2456     raw_spin_unlock(&pool->lock);
2457     if (wake_task)
2458         wake_up_process(wake_task);
2459     rcu_read_unlock();
2460 }

Okay, there is some interesting things going on in __queue_work(). We either
take the top path, that selects a task, insert_work() adds it to the runqueue,
kick_pool_pick() sets the task to RUNNING and then tries to wake it up.

Which doesn't work, as it won't preempt the SCHED_FIFO userspace task thats
spinning.

The bottom path, just calls insert_work() and places it on the workqeueue for
later processing, a later that never really comes.

I have been trying a couple of ideas.

The first was to see if we are needing to drain the LRU cache on a cpu core
that is isolated, by checking if its not a housekeeping core, and if it isn't
then just send a Inter Processor Interrupt to force the drain to run.

--- a/mm/folio.c
+++ b/mm/folio.c
@@ -33,6 +33,8 @@
 #include <linux/page_idle.h>
 #include <linux/local_lock.h>
 #include <linux/buffer_head.h>
+#include <linux/sched/isolation.h>
+#include <linux/smp.h>
 
 #include "internal.h"
 #include "page_alloc.h"
@@ -752,6 +754,11 @@ static void lru_add_drain_per_cpu(struct work_struct 
*dummy)
        lru_add_and_bh_lrus_drain();
 }
 
+static void lru_add_drain_ipi(void *info)
+{
+       lru_add_and_bh_lrus_drain();
+}
+
 static bool cpu_needs_drain(unsigned int cpu)
 {
        struct cpu_fbatches *fbatches = &per_cpu(cpu_fbatches, cpu);
@@ -857,9 +864,13 @@ static inline void __lru_add_drain_all(bool force_all_cpus)
                struct work_struct *work = &per_cpu(lru_add_drain_work, cpu);
 
                if (cpu_needs_drain(cpu)) {
-                       INIT_WORK(work, lru_add_drain_per_cpu);
-                       queue_work_on(cpu, mm_percpu_wq, work);
-                       __cpumask_set_cpu(cpu, &has_work);
+                       if (!housekeeping_cpu(cpu, HK_TYPE_WQ)) {
+                               smp_call_function_single(cpu, 
lru_add_drain_ipi, NULL, 1);
+                       } else {
+                               INIT_WORK(work, lru_add_drain_per_cpu);
+                               queue_work_on(cpu, mm_percpu_wq, work);
+                               __cpumask_set_cpu(cpu, &has_work);
+                       }
                }
        }
 
I tested this, but it doesn't quite have the performance that I was expecting.
It still gets stuck in D state for too long, and doesn't really improve 
anything.

What I am going to try next is to see if I can patch workqueues themselves to
see if I can get it to detect if the work is queued on a CPU that has a higher
priority task, then to send an IPI instead.

I'll let you know how my experiments turn out.

Thanks,
Matthew

-- 
You received this bug notification because you are a member of Ubuntu
Bugs, which is subscribed to Ubuntu.
https://bugs.launchpad.net/bugs/2165410

Title:
  Ubuntu LTS 26.04 linux-aws: systemd enters D state and blocks SSH
  during cpuset migration on nohz_full CPUs

To manage notifications about this bug go to:
https://bugs.launchpad.net/ubuntu/+source/linux-aws/+bug/2165410/+subscriptions


-- 
ubuntu-bugs mailing list
[email protected]
https://lists.ubuntu.com/mailman/listinfo/ubuntu-bugs

Reply via email to