Skip to contentSkip to conversation

Fix deadlock in __create_workqueue

PATCH In review Srivatsa Vaddagiri 3 participants 11 messages Apr 30, 2004 – May 5, 2004 Workqueue

lore
Share & tools
lore
#0 lore raw

PATCH 1/1 Fix deadlock in __create_workqueue

Noticed a possible deadlock in __create_workqueue when CONFIG_HOTPLUG_CPU is set. This can happen when create_workqueue_thread fails to create a worker thread. In that case, we call destroy_workqueue with cpu hotplug lock held. destroy_workqueue however also attempts to take the same lock.

Patch below address this deadlock as well as a kthread_stop race. Its tested against 2.6.6-rc2-mm2 on a 4-way Intel SMP box.

---

 linux-2.6.6-rc2-mm2-root/kernel/workqueue.c |   58 +++++++++++++++++-----------
 1 files changed, 37 insertions(+), 21 deletions(-)
kernel/workqueue.c renamed from kernel/workqueue.c~__create_workqueue_fix +37 -21
diff -puN kernel/workqueue.c~__create_workqueue_fix kernel/workqueue.c
@@ -201,8 +201,9 @@ static int worker_thread(void *__cwq)
 	siginitset(&sa.sa.sa_mask, sigmask(SIGCHLD));
 	do_sigaction(SIGCHLD, &sa, (struct k_sigaction *)0);
 
+	set_task_state(current, TASK_INTERRUPTIBLE);
+
 	while (!kthread_should_stop()) {
-		set_task_state(current, TASK_INTERRUPTIBLE);
 
 		add_wait_queue(&cwq->more_work, &wait);
 		if (list_empty(&cwq->worklist))
@@ -213,7 +214,11 @@ static int worker_thread(void *__cwq)
 
 		if (!list_empty(&cwq->worklist))
 			run_workqueue(cwq);
+
+		set_task_state(current, TASK_INTERRUPTIBLE);
 	}
+
+	__set_current_state(TASK_RUNNING);
 	return 0;
 }
 
@@ -297,6 +302,21 @@ static struct task_struct *create_workqu
 	return p;
 }
 
+static void cleanup_workqueue_thread(struct workqueue_struct *wq, int cpu)
+{
+	struct cpu_workqueue_struct *cwq;
+	unsigned long flags;
+	struct task_struct *p;
+
+	cwq = wq->cpu_wq + cpu;
+	spin_lock_irqsave(&cwq->lock, flags);
+	p = cwq->thread;
+	cwq->thread = NULL;
+	spin_unlock_irqrestore(&cwq->lock, flags);
+	if (p)
+		kthread_stop(p);
+}
+
 struct workqueue_struct *__create_workqueue(const char *name,
 					    int singlethread)
 {
@@ -322,16 +342,27 @@ struct workqueue_struct *__create_workqu
 		else
 			wake_up_process(p);
 	} else {
-		spin_lock(&workqueue_lock);
-		list_add(&wq->list, &workqueues);
-		spin_unlock_irq(&workqueue_lock);
 		for_each_online_cpu(cpu) {
 			p = create_workqueue_thread(wq, cpu);
 			if (p) {
 				kthread_bind(p, cpu);
 				wake_up_process(p);
-			} else
+			} else {
+				int i;
+
+				for (i=cpu-1; i>=0; --i)
+					if (cpu_online(i))
+						cleanup_workqueue_thread(wq, i);
+
 				destroy = 1;
+				break;
+			}
+		}
+
+		if (!destroy) {
+			spin_lock(&workqueue_lock);
+			list_add(&wq->list, &workqueues);
+			spin_unlock_irq(&workqueue_lock);
 		}
 	}
 
@@ -339,28 +370,13 @@ struct workqueue_struct *__create_workqu
 	 * Was there any error during startup? If yes then clean up:
 	 */
 	if (destroy) {
-		destroy_workqueue(wq);
+		kfree(wq);
 		wq = NULL;
 	}
 	unlock_cpu_hotplug();
 	return wq;
 }
 
-static void cleanup_workqueue_thread(struct workqueue_struct *wq, int cpu)
-{
-	struct cpu_workqueue_struct *cwq;
-	unsigned long flags;
-	struct task_struct *p;
-
-	cwq = wq->cpu_wq + cpu;
-	spin_lock_irqsave(&cwq->lock, flags);
-	p = cwq->thread;
-	cwq->thread = NULL;
-	spin_unlock_irqrestore(&cwq->lock, flags);
-	if (p)
-		kthread_stop(p);
-}
-
 void destroy_workqueue(struct workqueue_struct *wq)
 {
 	int cpu;

_

Signature · 5 lines
Thanks and Regards,
Srivatsa Vaddagiri,
Linux Technology Center,
IBM Software Labs,
Bangalore, INDIA - 560017
#1 lore raw
Quoted 7 lines · Srivatsa Vaddagiri

Noticed a possible deadlock in __create_workqueue when CONFIG_HOTPLUG_CPU is set. This can happen when create_workqueue_thread fails to create a worker thread. In that case, we call destroy_workqueue with cpu hotplug lock held. destroy_workqueue however also attempts to take the same lock.

Patch below address this deadlock as well as a kthread_stop race.

Fixing a kthread_stop() race is a quite different thing from fixing a create-workqueue() error-path deadlock and hence should be a separate patch.

And the description of that separate patch should explain the race which it is fixing! Yes, the logic in worker_thread() is a bit dorky, but I don't believe that there is a race in there.

I dropped that part of your patch. Please resend, with justification, if you disagree.

Thanks.

#2 lore raw
Quoted 2 lines · Andrew Morton

Yes, the logic in worker_thread() is a bit dorky, but I don't believe that there is a race in there.

worker_thread examines kthread_should_stop() while its state is TASK_RUNNING, after which it sets its state to TASK_INTERRUPTIBLE. If kthread_stop were to come after kthread_should_stop and before worker_thread has set its state to TASK_INTERRUPTIBLE (which is possible because of a CPU going dead), wouldn't kthread_stop block forever? Note that in case of CPU going dead, it is possible that a worker thread bound to the dead cpu continues executing on a different cpu before it is killed in CPU_DEAD processing.

Am I missing something here?

Signature · 5 lines
Thanks and Regards,
Srivatsa Vaddagiri,
Linux Technology Center,
IBM Software Labs,
Bangalore, INDIA - 560017
#3 lore raw
Quoted 5 lines · Srivatsa Vaddagiri
Quoted 2 lines · Andrew Morton

Yes, the logic in worker_thread() is a bit dorky, but I don't believe that there is a race in there.

Quoted 8 lines

worker_thread examines kthread_should_stop() while its state is TASK_RUNNING, after which it sets its state to TASK_INTERRUPTIBLE. If kthread_stop were to come after kthread_should_stop and before worker_thread has set its state to TASK_INTERRUPTIBLE (which is possible because of a CPU going dead), wouldn't kthread_stop block forever? Note that in case of CPU going dead, it is possible that a worker thread bound to the dead cpu continues executing on a different cpu before it is killed in CPU_DEAD processing.

yup, sorry, I misread the code. Like this?

kernel/workqueue.c renamed from kernel/workqueue.c~worker_thread-race-fix +4 -3
@@ -201,19 +201,20 @@ static int worker_thread(void *__cwq)
 	siginitset(&sa.sa.sa_mask, sigmask(SIGCHLD));
 	do_sigaction(SIGCHLD, &sa, (struct k_sigaction *)0);
 
+	set_current_state(TASK_INTERRUPTIBLE);
 	while (!kthread_should_stop()) {
-		set_task_state(current, TASK_INTERRUPTIBLE);
-
 		add_wait_queue(&cwq->more_work, &wait);
 		if (list_empty(&cwq->worklist))
 			schedule();
 		else
-			set_task_state(current, TASK_RUNNING);
+			__set_current_state(TASK_RUNNING);
 		remove_wait_queue(&cwq->more_work, &wait);
 
 		if (!list_empty(&cwq->worklist))
 			run_workqueue(cwq);
+		set_current_state(TASK_INTERRUPTIBLE);
 	}
+	__set_current_state(TASK_RUNNING);
 	return 0;
 }
 

_

#4 lore raw
Quoted 1 line · Andrew Morton

yup, sorry, I misread the code. Like this?

Yes, looks fine to me!

Quoted 26 lines
kernel/workqueue.c renamed from kernel/workqueue.c~worker_thread-race-fix +4 -3
@@ -201,19 +201,20 @@ static int worker_thread(void *__cwq)
 	siginitset(&sa.sa.sa_mask, sigmask(SIGCHLD));
 	do_sigaction(SIGCHLD, &sa, (struct k_sigaction *)0);
 
+	set_current_state(TASK_INTERRUPTIBLE);
 	while (!kthread_should_stop()) {
-		set_task_state(current, TASK_INTERRUPTIBLE);
-
 		add_wait_queue(&cwq->more_work, &wait);
 		if (list_empty(&cwq->worklist))
 			schedule();
 		else
-			set_task_state(current, TASK_RUNNING);
+			__set_current_state(TASK_RUNNING);
 		remove_wait_queue(&cwq->more_work, &wait);
 
 		if (!list_empty(&cwq->worklist))
 			run_workqueue(cwq);
+		set_current_state(TASK_INTERRUPTIBLE);
 	}
+	__set_current_state(TASK_RUNNING);
 	return 0;
 }
Signature · 5 lines
Thanks and Regards,
Srivatsa Vaddagiri,
Linux Technology Center,
IBM Software Labs,
Bangalore, INDIA - 560017
#5 lore raw
In reply to Srivatsa Vaddagiri
Quoted 5 lines · Srivatsa Vaddagiri

Noticed a possible deadlock in __create_workqueue when CONFIG_HOTPLUG_CPU is
set. This can happen when create_workqueue_thread fails to create a worker thread. In that case, we call destroy_workqueue with cpu hotplug lock held. destroy_workqueue however also attempts to take the same lock.

Can we not simply do:

kernel/workqueue.c renamed from kernel/workqueue.c~a +1 -1
diff -puN kernel/workqueue.c~a kernel/workqueue.c
@@ -334,6 +334,7 @@ struct workqueue_struct *__create_workqu
 				destroy = 1;
 		}
 	}
+	unlock_cpu_hotplug();
 
 	/*
 	 * Was there any error during startup? If yes then clean up:
@@ -342,7 +343,6 @@ struct workqueue_struct *__create_workqu
 		destroy_workqueue(wq);
 		wq = NULL;
 	}
-	unlock_cpu_hotplug();
 	return wq;
 }
 

_

#6 lore raw
Quoted 15 lines · Andrew Morton

Can we not simply do:

diff -puN kernel/workqueue.c~a kernel/workqueue.c

kernel/workqueue.c renamed from kernel/workqueue.c~a +1 -0
@@ -334,6 +334,7 @@ struct workqueue_struct *__create_workqu
 				destroy = 1;
 		}
 	}
+	unlock_cpu_hotplug();
 
 	/*
 	 * Was there any error during startup? If yes then clean up:

kernel/workqueue.c struct workqueue_struct *__create_workqu · line 347

Quoted 6 lines
kernel/workqueue.c renamed from kernel/workqueue.c~a +0 -1
@@ -342,7 +343,6 @@ struct workqueue_struct *__create_workqu
 		destroy_workqueue(wq);
 		wq = NULL;
 	}
-	unlock_cpu_hotplug();
 	return wq;
 }

I didn't do this because I introduced a break at the first instance when create_workqueue_thread failed. Breaking out of the loop like that appeared to be more efficient rather than going back and trying to create threads for rest of the online cpus, because most likely thread creation will fail for other cpus also and anyway the workqueue will be destroyed down the line.

If the break is introduced, I dont think we can rely on destroy_workqueue because some of the per-cpu structures (spinlocks for one) will not be initialized for all online cpus.

Signature · 5 lines
Thanks and Regards,
Srivatsa Vaddagiri,
Linux Technology Center,
IBM Software Labs,
Bangalore, INDIA - 560017
#7 lore raw
Quoted 14 lines · Srivatsa Vaddagiri
Quoted 12 lines · Andrew Morton

Can we not simply do:

diff -puN kernel/workqueue.c~a kernel/workqueue.c

kernel/workqueue.c renamed from kernel/workqueue.c~a +1 -0
@@ -334,6 +334,7 @@ struct workqueue_struct *__create_workqu
 				destroy = 1;
 		}
 	}
+	unlock_cpu_hotplug();
 
Quoted 16 lines

kernel/workqueue.c struct workqueue_struct *__create_workqu · line 347

Quoted 9 lines
kernel/workqueue.c renamed from kernel/workqueue.c~a +0 -1
 	/*
 	 * Was there any error during startup? If yes then clean up:
@@ -342,7 +343,6 @@ struct workqueue_struct *__create_workqu
 		destroy_workqueue(wq);
 		wq = NULL;
 	}
-	unlock_cpu_hotplug();
 	return wq;
 }

I didn't do this because I introduced a break at the first instance when create_workqueue_thread failed. Breaking out of the loop like that appeared to be more efficient rather than going back and trying to create threads for rest of the online cpus, because most likely thread creation will fail for other cpus also and anyway the workqueue will be destroyed down the line.

Well that create_workqueue_thread() will basically never fail - it's not a path we need to be optimising.

And from inspection, cleanup_workqueue_thread() will handle the non-existent thread quite happily.

#8 lore raw
Quoted 2 lines · Andrew Morton

Well that create_workqueue_thread() will basically never fail - it's not a path we need to be optimising.

Even if thread creation normally never fails, we still check for its return code and have some error recovery code! In that case, I dont understand the point behind continuing the loop once thread destruction fails for some CPU. Lets say on a 128 CPU machine, if thread creation fails for the 1st CPU (because of say ENOMEM?), then why continue trying to create threads for the rest of 126 CPUs and _then_ destroy? Why not just break at the first occurence of failure and cleanup then and there?

Signature · 5 lines
Thanks and Regards,
Srivatsa Vaddagiri,
Linux Technology Center,
IBM Software Labs,
Bangalore, INDIA - 560017
#9 lore raw
Quoted 5 lines · Srivatsa Vaddagiri
Quoted 2 lines · Andrew Morton

Well that create_workqueue_thread() will basically never fail - it's not a path we need to be optimising.

Quoted 9 lines

Even if thread creation normally never fails, we still check for its return code and have some error recovery code! In that case, I dont understand the point behind continuing the loop once thread destruction fails for some CPU. Lets say on a 128 CPU machine, if thread creation fails for the 1st CPU (because of say ENOMEM?), then why continue trying to create threads for the rest of 126 CPUs and _then_ destroy? Why not just break at the first occurence of failure and cleanup then and there?

Because there's less code to get wrong.

In this situation, correctness, clarity and code size are the only things we care about.

#10 lore raw
In reply to Srivatsa Vaddagiri
Quoted 13 lines · Srivatsa Vaddagiri
Quoted 12 lines · Andrew Morton

Can we not simply do:

diff -puN kernel/workqueue.c~a kernel/workqueue.c

kernel/workqueue.c renamed from kernel/workqueue.c~a +1 -0
@@ -334,6 +334,7 @@ struct workqueue_struct *__create_workqu
 				destroy = 1;
 		}
 	}
+	unlock_cpu_hotplug();
 
Quoted 16 lines

kernel/workqueue.c struct workqueue_struct *__create_workqu · line 347

Quoted 9 lines
kernel/workqueue.c renamed from kernel/workqueue.c~a +0 -1
 	/*
 	 * Was there any error during startup? If yes then clean up:
@@ -342,7 +343,6 @@ struct workqueue_struct *__create_workqu
 		destroy_workqueue(wq);
 		wq = NULL;
 	}
-	unlock_cpu_hotplug();
 	return wq;
 }

I didn't do this because I introduced a break at the first instance when create_workqueue_thread failed. Breaking out of the loop like that appeared to be more efficient rather than going back and trying to create threads for rest of the online cpus, because most likely thread creation will fail for other cpus also and anyway the workqueue will be destroyed down the line.

The logic was not the way I would have done it, but it *is* neater. I prefer the akpm fix I think.

Rusty.

Signature · 1 line
Anyone who quotes me in their signature is an idiot -- Rusty Russell