Commit 46c94ace authored by Andrew Morton's avatar Andrew Morton Committed by Greg Kroah-Hartman

[PATCH] Fix deadlocks on dpm_sem

From: Paul Mackerras <paulus@samba.org>

Currently the device_pm_foo() functions are rather prone to deadlocks
during suspend/resume.  This is because the dpm_sem is held for the
duration of device_suspend() and device_resume() as well as device_pm_add()
and device_pm_remove().  If for any reason you get a device addition or
removal triggered by a device's suspend or resume code, you get a deadlock.
 (The classic example is a USB host adaptor resuming and discovering that
the mouse you used to have plugged in has gone away.)

This patch fixes the problem by using a separate semaphore, called
dpm_list_sem, to cover the places where we need the device pm lists to be
stable, and by being careful about how we traverse the lists on suspend and
resume.  I have analysed the various cases that can occur and I am
confident that I have handled them all correctly.  I posted this patch
together with a detailed analysis 10 days ago.

Signed-off-by Paul Mackerras <paulus@samba.org>
Signed-off-by: default avatarAndrew Morton <akpm@osdl.org>
Signed-off-by: default avatarGreg Kroah-Hartman <greg@kroah.com>
parent c185e5b0
...@@ -28,6 +28,7 @@ LIST_HEAD(dpm_off); ...@@ -28,6 +28,7 @@ LIST_HEAD(dpm_off);
LIST_HEAD(dpm_off_irq); LIST_HEAD(dpm_off_irq);
DECLARE_MUTEX(dpm_sem); DECLARE_MUTEX(dpm_sem);
DECLARE_MUTEX(dpm_list_sem);
/* /*
* PM Reference Counting. * PM Reference Counting.
...@@ -75,12 +76,12 @@ int device_pm_add(struct device * dev) ...@@ -75,12 +76,12 @@ int device_pm_add(struct device * dev)
pr_debug("PM: Adding info for %s:%s\n", pr_debug("PM: Adding info for %s:%s\n",
dev->bus ? dev->bus->name : "No Bus", dev->kobj.name); dev->bus ? dev->bus->name : "No Bus", dev->kobj.name);
atomic_set(&dev->power.pm_users, 0); atomic_set(&dev->power.pm_users, 0);
down(&dpm_sem); down(&dpm_list_sem);
list_add_tail(&dev->power.entry, &dpm_active); list_add_tail(&dev->power.entry, &dpm_active);
device_pm_set_parent(dev, dev->parent); device_pm_set_parent(dev, dev->parent);
if ((error = dpm_sysfs_add(dev))) if ((error = dpm_sysfs_add(dev)))
list_del(&dev->power.entry); list_del(&dev->power.entry);
up(&dpm_sem); up(&dpm_list_sem);
return error; return error;
} }
...@@ -88,11 +89,11 @@ void device_pm_remove(struct device * dev) ...@@ -88,11 +89,11 @@ void device_pm_remove(struct device * dev)
{ {
pr_debug("PM: Removing info for %s:%s\n", pr_debug("PM: Removing info for %s:%s\n",
dev->bus ? dev->bus->name : "No Bus", dev->kobj.name); dev->bus ? dev->bus->name : "No Bus", dev->kobj.name);
down(&dpm_sem); down(&dpm_list_sem);
dpm_sysfs_remove(dev); dpm_sysfs_remove(dev);
device_pm_release(dev->power.pm_parent); device_pm_release(dev->power.pm_parent);
list_del(&dev->power.entry); list_del_init(&dev->power.entry);
up(&dpm_sem); up(&dpm_list_sem);
} }
...@@ -27,6 +27,11 @@ extern void device_shutdown(void); ...@@ -27,6 +27,11 @@ extern void device_shutdown(void);
*/ */
extern struct semaphore dpm_sem; extern struct semaphore dpm_sem;
/*
* Used to serialize changes to the dpm_* lists.
*/
extern struct semaphore dpm_list_sem;
/* /*
* The PM lists. * The PM lists.
*/ */
......
...@@ -31,16 +31,22 @@ int resume_device(struct device * dev) ...@@ -31,16 +31,22 @@ int resume_device(struct device * dev)
void dpm_resume(void) void dpm_resume(void)
{ {
down(&dpm_list_sem);
while(!list_empty(&dpm_off)) { while(!list_empty(&dpm_off)) {
struct list_head * entry = dpm_off.next; struct list_head * entry = dpm_off.next;
struct device * dev = to_device(entry); struct device * dev = to_device(entry);
get_device(dev);
list_del_init(entry); list_del_init(entry);
list_add_tail(entry, &dpm_active);
up(&dpm_list_sem);
if (!dev->power.prev_state) if (!dev->power.prev_state)
resume_device(dev); resume_device(dev);
down(&dpm_list_sem);
list_add_tail(entry, &dpm_active); put_device(dev);
} }
up(&dpm_list_sem);
} }
...@@ -76,9 +82,13 @@ void dpm_power_up(void) ...@@ -76,9 +82,13 @@ void dpm_power_up(void)
{ {
while(!list_empty(&dpm_off_irq)) { while(!list_empty(&dpm_off_irq)) {
struct list_head * entry = dpm_off_irq.next; struct list_head * entry = dpm_off_irq.next;
struct device * dev = to_device(entry);
get_device(dev);
list_del_init(entry); list_del_init(entry);
resume_device(to_device(entry));
list_add_tail(entry, &dpm_active); list_add_tail(entry, &dpm_active);
resume_device(dev);
put_device(dev);
} }
} }
......
...@@ -63,11 +63,6 @@ int suspend_device(struct device * dev, u32 state) ...@@ -63,11 +63,6 @@ int suspend_device(struct device * dev, u32 state)
* If we hit a failure with any of the devices, call device_resume() * If we hit a failure with any of the devices, call device_resume()
* above to bring the suspended devices back to life. * above to bring the suspended devices back to life.
* *
* Note this function leaves dpm_sem held to
* a) block other devices from registering.
* b) prevent other PM operations from happening after we've begun.
* c) make sure we're exclusive when we disable interrupts.
*
*/ */
int device_suspend(u32 state) int device_suspend(u32 state)
...@@ -75,29 +70,40 @@ int device_suspend(u32 state) ...@@ -75,29 +70,40 @@ int device_suspend(u32 state)
int error = 0; int error = 0;
down(&dpm_sem); down(&dpm_sem);
while(!list_empty(&dpm_active)) { down(&dpm_list_sem);
while (!list_empty(&dpm_active) && error == 0) {
struct list_head * entry = dpm_active.prev; struct list_head * entry = dpm_active.prev;
struct device * dev = to_device(entry); struct device * dev = to_device(entry);
get_device(dev);
up(&dpm_list_sem);
error = suspend_device(dev, state); error = suspend_device(dev, state);
if (!error) { down(&dpm_list_sem);
list_del(&dev->power.entry);
list_add(&dev->power.entry, &dpm_off); /* Check if the device got removed */
} else if (error == -EAGAIN) { if (!list_empty(&dev->power.entry)) {
list_del(&dev->power.entry); /* Move it to the dpm_off or dpm_off_irq list */
list_add(&dev->power.entry, &dpm_off_irq); if (!error) {
} else { list_del(&dev->power.entry);
list_add(&dev->power.entry, &dpm_off);
} else if (error == -EAGAIN) {
list_del(&dev->power.entry);
list_add(&dev->power.entry, &dpm_off_irq);
error = 0;
}
}
if (error)
printk(KERN_ERR "Could not suspend device %s: " printk(KERN_ERR "Could not suspend device %s: "
"error %d\n", kobject_name(&dev->kobj), error); "error %d\n", kobject_name(&dev->kobj), error);
goto Error; put_device(dev);
}
} }
Done: up(&dpm_list_sem);
if (error)
dpm_resume();
up(&dpm_sem); up(&dpm_sem);
return error; return error;
Error:
dpm_resume();
goto Done;
} }
EXPORT_SYMBOL_GPL(device_suspend); EXPORT_SYMBOL_GPL(device_suspend);
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment