int                     id;             /* I: pool ID */
        unsigned int            flags;          /* X: flags */
 
+       unsigned long           watchdog_ts;    /* L: watchdog timestamp */
+
        struct list_head        worklist;       /* L: list of pending works */
        int                     nr_workers;     /* L: total number of workers */
 
        struct pool_workqueue *pwq = get_work_pwq(work);
 
        trace_workqueue_activate_work(work);
+       if (list_empty(&pwq->pool->worklist))
+               pwq->pool->watchdog_ts = jiffies;
        move_linked_works(work, &pwq->pool->worklist, NULL);
        __clear_bit(WORK_STRUCT_DELAYED_BIT, work_data_bits(work));
        pwq->nr_active++;
                trace_workqueue_activate_work(work);
                pwq->nr_active++;
                worklist = &pwq->pool->worklist;
+               if (list_empty(worklist))
+                       pwq->pool->watchdog_ts = jiffies;
        } else {
                work_flags |= WORK_STRUCT_DELAYED;
                worklist = &pwq->delayed_works;
                        list_first_entry(&pool->worklist,
                                         struct work_struct, entry);
 
+               pool->watchdog_ts = jiffies;
+
                if (likely(!(*work_data_bits(work) & WORK_STRUCT_LINKED))) {
                        /* optimization path, not strictly necessary */
                        process_one_work(worker, work);
                                        struct pool_workqueue, mayday_node);
                struct worker_pool *pool = pwq->pool;
                struct work_struct *work, *n;
+               bool first = true;
 
                __set_current_state(TASK_RUNNING);
                list_del_init(&pwq->mayday_node);
                 * process'em.
                 */
                WARN_ON_ONCE(!list_empty(scheduled));
-               list_for_each_entry_safe(work, n, &pool->worklist, entry)
-                       if (get_work_pwq(work) == pwq)
+               list_for_each_entry_safe(work, n, &pool->worklist, entry) {
+                       if (get_work_pwq(work) == pwq) {
+                               if (first)
+                                       pool->watchdog_ts = jiffies;
                                move_linked_works(work, scheduled, &n);
+                       }
+                       first = false;
+               }
 
                if (!list_empty(scheduled)) {
                        process_scheduled_works(rescuer);
        pool->cpu = -1;
        pool->node = NUMA_NO_NODE;
        pool->flags |= POOL_DISASSOCIATED;
+       pool->watchdog_ts = jiffies;
        INIT_LIST_HEAD(&pool->worklist);
        INIT_LIST_HEAD(&pool->idle_list);
        hash_init(pool->busy_hash);
 
                pr_info("pool %d:", pool->id);
                pr_cont_pool_info(pool);
-               pr_cont(" workers=%d", pool->nr_workers);
+               pr_cont(" hung=%us workers=%d",
+                       jiffies_to_msecs(jiffies - pool->watchdog_ts) / 1000,
+                       pool->nr_workers);
                if (pool->manager)
                        pr_cont(" manager: %d",
                                task_pid_nr(pool->manager->task));
 static void workqueue_sysfs_unregister(struct workqueue_struct *wq)    { }
 #endif /* CONFIG_SYSFS */
 
+/*
+ * Workqueue watchdog.
+ *
+ * Stall may be caused by various bugs - missing WQ_MEM_RECLAIM, illegal
+ * flush dependency, a concurrency managed work item which stays RUNNING
+ * indefinitely.  Workqueue stalls can be very difficult to debug as the
+ * usual warning mechanisms don't trigger and internal workqueue state is
+ * largely opaque.
+ *
+ * Workqueue watchdog monitors all worker pools periodically and dumps
+ * state if some pools failed to make forward progress for a while where
+ * forward progress is defined as the first item on ->worklist changing.
+ *
+ * This mechanism is controlled through the kernel parameter
+ * "workqueue.watchdog_thresh" which can be updated at runtime through the
+ * corresponding sysfs parameter file.
+ */
+#ifdef CONFIG_WQ_WATCHDOG
+
+static void wq_watchdog_timer_fn(unsigned long data);
+
+static unsigned long wq_watchdog_thresh = 30;
+static struct timer_list wq_watchdog_timer =
+       TIMER_DEFERRED_INITIALIZER(wq_watchdog_timer_fn, 0, 0);
+
+static unsigned long wq_watchdog_touched = INITIAL_JIFFIES;
+static DEFINE_PER_CPU(unsigned long, wq_watchdog_touched_cpu) = INITIAL_JIFFIES;
+
+static void wq_watchdog_reset_touched(void)
+{
+       int cpu;
+
+       wq_watchdog_touched = jiffies;
+       for_each_possible_cpu(cpu)
+               per_cpu(wq_watchdog_touched_cpu, cpu) = jiffies;
+}
+
+static void wq_watchdog_timer_fn(unsigned long data)
+{
+       unsigned long thresh = READ_ONCE(wq_watchdog_thresh) * HZ;
+       bool lockup_detected = false;
+       struct worker_pool *pool;
+       int pi;
+
+       if (!thresh)
+               return;
+
+       rcu_read_lock();
+
+       for_each_pool(pool, pi) {
+               unsigned long pool_ts, touched, ts;
+
+               if (list_empty(&pool->worklist))
+                       continue;
+
+               /* get the latest of pool and touched timestamps */
+               pool_ts = READ_ONCE(pool->watchdog_ts);
+               touched = READ_ONCE(wq_watchdog_touched);
+
+               if (time_after(pool_ts, touched))
+                       ts = pool_ts;
+               else
+                       ts = touched;
+
+               if (pool->cpu >= 0) {
+                       unsigned long cpu_touched =
+                               READ_ONCE(per_cpu(wq_watchdog_touched_cpu,
+                                                 pool->cpu));
+                       if (time_after(cpu_touched, ts))
+                               ts = cpu_touched;
+               }
+
+               /* did we stall? */
+               if (time_after(jiffies, ts + thresh)) {
+                       lockup_detected = true;
+                       pr_emerg("BUG: workqueue lockup - pool");
+                       pr_cont_pool_info(pool);
+                       pr_cont(" stuck for %us!\n",
+                               jiffies_to_msecs(jiffies - pool_ts) / 1000);
+               }
+       }
+
+       rcu_read_unlock();
+
+       if (lockup_detected)
+               show_workqueue_state();
+
+       wq_watchdog_reset_touched();
+       mod_timer(&wq_watchdog_timer, jiffies + thresh);
+}
+
+void wq_watchdog_touch(int cpu)
+{
+       if (cpu >= 0)
+               per_cpu(wq_watchdog_touched_cpu, cpu) = jiffies;
+       else
+               wq_watchdog_touched = jiffies;
+}
+
+static void wq_watchdog_set_thresh(unsigned long thresh)
+{
+       wq_watchdog_thresh = 0;
+       del_timer_sync(&wq_watchdog_timer);
+
+       if (thresh) {
+               wq_watchdog_thresh = thresh;
+               wq_watchdog_reset_touched();
+               mod_timer(&wq_watchdog_timer, jiffies + thresh * HZ);
+       }
+}
+
+static int wq_watchdog_param_set_thresh(const char *val,
+                                       const struct kernel_param *kp)
+{
+       unsigned long thresh;
+       int ret;
+
+       ret = kstrtoul(val, 0, &thresh);
+       if (ret)
+               return ret;
+
+       if (system_wq)
+               wq_watchdog_set_thresh(thresh);
+       else
+               wq_watchdog_thresh = thresh;
+
+       return 0;
+}
+
+static const struct kernel_param_ops wq_watchdog_thresh_ops = {
+       .set    = wq_watchdog_param_set_thresh,
+       .get    = param_get_ulong,
+};
+
+module_param_cb(watchdog_thresh, &wq_watchdog_thresh_ops, &wq_watchdog_thresh,
+               0644);
+
+static void wq_watchdog_init(void)
+{
+       wq_watchdog_set_thresh(wq_watchdog_thresh);
+}
+
+#else  /* CONFIG_WQ_WATCHDOG */
+
+static inline void wq_watchdog_init(void) { }
+
+#endif /* CONFIG_WQ_WATCHDOG */
+
 static void __init wq_numa_init(void)
 {
        cpumask_var_t *tbl;
               !system_unbound_wq || !system_freezable_wq ||
               !system_power_efficient_wq ||
               !system_freezable_power_efficient_wq);
+
+       wq_watchdog_init();
+
        return 0;
 }
 early_initcall(init_workqueues);