From 0458d5b4c9cc4ca0f62625d0144ddc4b4bc97a3c Mon Sep 17 00:00:00 2001 From: Alan Stern Date: Fri, 4 May 2007 11:52:20 -0400 Subject: USB: add USB-Persist facility This patch (as886) adds the controversial USB-persist facility, allowing USB devices to persist across a power loss during system suspend. The facility is controlled by a new Kconfig option (with appropriate warnings about the potential dangers); when the option is off the behavior will remain the same as it is now. But when the option is on, people will be able to use suspend-to-disk and keep their USB filesystems intact -- something particularly valuable for small machines where the root filesystem is on a USB device! Signed-off-by: Alan Stern Signed-off-by: Greg Kroah-Hartman --- drivers/usb/core/Kconfig | 22 +++++ drivers/usb/core/driver.c | 39 +++++---- drivers/usb/core/generic.c | 5 +- drivers/usb/core/hub.c | 196 ++++++++++++++++++++++++++++++++------------- drivers/usb/core/usb.h | 1 + 5 files changed, 191 insertions(+), 72 deletions(-) (limited to 'drivers/usb/core') diff --git a/drivers/usb/core/Kconfig b/drivers/usb/core/Kconfig index 346fc030c92..5113ef4cb7f 100644 --- a/drivers/usb/core/Kconfig +++ b/drivers/usb/core/Kconfig @@ -86,6 +86,28 @@ config USB_SUSPEND If you are unsure about this, say N here. +config USB_PERSIST + bool "USB device persistence during system suspend (DANGEROUS)" + depends on USB && PM && EXPERIMENTAL + default n + help + If you say Y here, USB device data structures will remain + persistent across system suspend, even if the USB bus loses + power. (This includes software-suspend, also known as swsusp, + or suspend-to-disk.) The devices will reappear as if by magic + when the system wakes up, with no need to unmount USB filesystems, + rmmod host-controller drivers, or do anything else. + + WARNING: This option can be dangerous! + + If a USB device is replaced by another of the same type while + the system is asleep, there's a good chance the kernel won't + detect the change. Likewise if the media in a USB storage + device is replaced. When this happens it's almost certain to + cause data corruption and maybe even crash your system. + + If you are unsure, say N here. + config USB_OTG bool depends on USB && EXPERIMENTAL diff --git a/drivers/usb/core/driver.c b/drivers/usb/core/driver.c index e8b447e06c5..12dd986bdff 100644 --- a/drivers/usb/core/driver.c +++ b/drivers/usb/core/driver.c @@ -824,8 +824,9 @@ static int usb_resume_device(struct usb_device *udev) struct usb_device_driver *udriver; int status = 0; - if (udev->state == USB_STATE_NOTATTACHED || - udev->state != USB_STATE_SUSPENDED) + if (udev->state == USB_STATE_NOTATTACHED) + goto done; + if (udev->state != USB_STATE_SUSPENDED && !udev->reset_resume) goto done; /* Can't resume it if it doesn't have a driver. */ @@ -882,7 +883,7 @@ done: } /* Caller has locked intf's usb_device's pm_mutex */ -static int usb_resume_interface(struct usb_interface *intf) +static int usb_resume_interface(struct usb_interface *intf, int reset_resume) { struct usb_driver *driver; int status = 0; @@ -902,21 +903,21 @@ static int usb_resume_interface(struct usb_interface *intf) } driver = to_usb_driver(intf->dev.driver); - if (driver->resume) { + if (reset_resume && driver->post_reset) + driver->post_reset(intf, reset_resume); + else if (driver->resume) { status = driver->resume(intf); if (status) dev_err(&intf->dev, "%s error %d\n", "resume", status); - else - mark_active(intf); - } else { + } else dev_warn(&intf->dev, "no resume for driver %s?\n", driver->name); - mark_active(intf); - } done: // dev_dbg(&intf->dev, "%s: status %d\n", __FUNCTION__, status); + if (status == 0) + mark_active(intf); return status; } @@ -1063,7 +1064,7 @@ static int usb_suspend_both(struct usb_device *udev, pm_message_t msg) if (status != 0) { while (--i >= 0) { intf = udev->actconfig->interface[i]; - usb_resume_interface(intf); + usb_resume_interface(intf, 0); } /* Try another autosuspend when the interfaces aren't busy */ @@ -1162,20 +1163,21 @@ static int usb_resume_both(struct usb_device *udev) } } else { - /* Needed only for setting udev->dev.power.power_state.event - * and for possible debugging message. */ + /* Needed for setting udev->dev.power.power_state.event, + * for possible debugging message, and for reset_resume. */ status = usb_resume_device(udev); } if (status == 0 && udev->actconfig) { for (i = 0; i < udev->actconfig->desc.bNumInterfaces; i++) { intf = udev->actconfig->interface[i]; - usb_resume_interface(intf); + usb_resume_interface(intf, udev->reset_resume); } } done: // dev_dbg(&udev->dev, "%s: status %d\n", __FUNCTION__, status); + udev->reset_resume = 0; return status; } @@ -1510,8 +1512,15 @@ static int usb_resume(struct device *dev) if (!is_usb_device(dev)) /* Ignore PM for interfaces */ return 0; udev = to_usb_device(dev); - if (udev->autoresume_disabled) - return -EPERM; + + /* If autoresume is disabled then we also want to prevent resume + * during system wakeup. However, a "persistent-device" reset-resume + * after power loss counts as a wakeup event. So allow a + * reset-resume to occur if remote wakeup is enabled. */ + if (udev->autoresume_disabled) { + if (!(udev->reset_resume && udev->do_remote_wakeup)) + return -EPERM; + } return usb_external_resume_device(udev); } diff --git a/drivers/usb/core/generic.c b/drivers/usb/core/generic.c index 7cbf992adcc..d363b0ea734 100644 --- a/drivers/usb/core/generic.c +++ b/drivers/usb/core/generic.c @@ -217,7 +217,10 @@ static int generic_resume(struct usb_device *udev) { int rc; - rc = usb_port_resume(udev); + if (udev->reset_resume) + rc = usb_reset_suspended_device(udev); + else + rc = usb_port_resume(udev); /* Root hubs don't have upstream ports to resume or reset, * so the line above won't do much for them. We have to diff --git a/drivers/usb/core/hub.c b/drivers/usb/core/hub.c index 77a6627b18d..51d2d304568 100644 --- a/drivers/usb/core/hub.c +++ b/drivers/usb/core/hub.c @@ -553,45 +553,121 @@ static int hub_hub_status(struct usb_hub *hub, static int hub_port_disable(struct usb_hub *hub, int port1, int set_state) { struct usb_device *hdev = hub->hdev; - int ret; + int ret = 0; - if (hdev->children[port1-1] && set_state) { + if (hdev->children[port1-1] && set_state) usb_set_device_state(hdev->children[port1-1], USB_STATE_NOTATTACHED); - } - ret = clear_port_feature(hdev, port1, USB_PORT_FEAT_ENABLE); + if (!hub->error) + ret = clear_port_feature(hdev, port1, USB_PORT_FEAT_ENABLE); if (ret) dev_err(hub->intfdev, "cannot disable port %d (err = %d)\n", - port1, ret); - + port1, ret); return ret; } +/* + * Disable a port and mark a logical connnect-change event, so that some + * time later khubd will disconnect() any existing usb_device on the port + * and will re-enumerate if there actually is a device attached. + */ +static void hub_port_logical_disconnect(struct usb_hub *hub, int port1) +{ + dev_dbg(hub->intfdev, "logical disconnect on port %d\n", port1); + hub_port_disable(hub, port1, 1); -/* caller has locked the hub device */ -static void hub_pre_reset(struct usb_interface *intf) + /* FIXME let caller ask to power down the port: + * - some devices won't enumerate without a VBUS power cycle + * - SRP saves power that way + * - ... new call, TBD ... + * That's easy if this hub can switch power per-port, and + * khubd reactivates the port later (timer, SRP, etc). + * Powerdown must be optional, because of reset/DFU. + */ + + set_bit(port1, hub->change_bits); + kick_khubd(hub); +} + +static void disconnect_all_children(struct usb_hub *hub, int logical) { - struct usb_hub *hub = usb_get_intfdata(intf); struct usb_device *hdev = hub->hdev; int port1; for (port1 = 1; port1 <= hdev->maxchild; ++port1) { - if (hdev->children[port1 - 1]) { - usb_disconnect(&hdev->children[port1 - 1]); - if (hub->error == 0) - hub_port_disable(hub, port1, 0); + if (hdev->children[port1-1]) { + if (logical) + hub_port_logical_disconnect(hub, port1); + else + usb_disconnect(&hdev->children[port1-1]); + } + } +} + +#ifdef CONFIG_USB_PERSIST + +#define USB_PERSIST 1 + +/* For "persistent-device" resets we must mark the child devices for reset + * and turn off a possible connect-change status (so khubd won't disconnect + * them later). + */ +static void mark_children_for_reset_resume(struct usb_hub *hub) +{ + struct usb_device *hdev = hub->hdev; + int port1; + + for (port1 = 1; port1 <= hdev->maxchild; ++port1) { + struct usb_device *child = hdev->children[port1-1]; + + if (child) { + child->reset_resume = 1; + clear_port_feature(hdev, port1, + USB_PORT_FEAT_C_CONNECTION); } } +} + +#else + +#define USB_PERSIST 0 + +static inline void mark_children_for_reset_resume(struct usb_hub *hub) +{ } + +#endif /* CONFIG_USB_PERSIST */ + +/* caller has locked the hub device */ +static void hub_pre_reset(struct usb_interface *intf) +{ + struct usb_hub *hub = usb_get_intfdata(intf); + + /* This routine doesn't run as part of a reset-resume, so it's safe + * to disconnect all the drivers below the hub. + */ + disconnect_all_children(hub, 0); hub_quiesce(hub); } /* caller has locked the hub device */ -static void hub_post_reset(struct usb_interface *intf) +static void hub_post_reset(struct usb_interface *intf, int reset_resume) { struct usb_hub *hub = usb_get_intfdata(intf); - hub_activate(hub); hub_power_on(hub); + if (reset_resume) { + if (USB_PERSIST) + mark_children_for_reset_resume(hub); + else { + /* Reset-resume doesn't call pre_reset, so we have to + * disconnect the children here. But we may not lock + * the child devices, so we have to do a "logical" + * disconnect. + */ + disconnect_all_children(hub, 1); + } + } + hub_activate(hub); } @@ -1053,33 +1129,64 @@ void usb_set_device_state(struct usb_device *udev, #ifdef CONFIG_PM +/** + * usb_reset_suspended_device - reset a suspended device instead of resuming it + * @udev: device to be reset instead of resumed + * + * If a host controller doesn't maintain VBUS suspend current during a + * system sleep or is reset when the system wakes up, all the USB + * power sessions below it will be broken. This is especially troublesome + * for mass-storage devices containing mounted filesystems, since the + * device will appear to have disconnected and all the memory mappings + * to it will be lost. + * + * As an alternative, this routine attempts to recover power sessions for + * devices that are still present by resetting them instead of resuming + * them. If all goes well, the devices will appear to persist across the + * the interruption of the power sessions. + * + * This facility is inherently dangerous. Although usb_reset_device() + * makes every effort to insure that the same device is present after the + * reset as before, it cannot provide a 100% guarantee. Furthermore it's + * quite possible for a device to remain unaltered but its media to be + * changed. If the user replaces a flash memory card while the system is + * asleep, he will have only himself to blame when the filesystem on the + * new card is corrupted and the system crashes. + */ +int usb_reset_suspended_device(struct usb_device *udev) +{ + int rc = 0; + + dev_dbg(&udev->dev, "usb %sresume\n", "reset-"); + + /* After we're done the device won't be suspended any more. + * In addition, the reset won't work if udev->state is SUSPENDED. + */ + usb_set_device_state(udev, udev->actconfig + ? USB_STATE_CONFIGURED + : USB_STATE_ADDRESS); + + /* Root hubs don't need to be (and can't be) reset */ + if (udev->parent) + rc = usb_reset_device(udev); + return rc; +} + /** * usb_root_hub_lost_power - called by HCD if the root hub lost Vbus power * @rhdev: struct usb_device for the root hub * * The USB host controller driver calls this function when its root hub * is resumed and Vbus power has been interrupted or the controller - * has been reset. The routine marks all the children of the root hub - * as NOTATTACHED and marks logical connect-change events on their ports. + * has been reset. The routine marks @rhdev as having lost power. When + * the hub driver is resumed it will take notice; if CONFIG_USB_PERSIST + * is enabled then it will carry out power-session recovery, otherwise + * it will disconnect all the child devices. */ void usb_root_hub_lost_power(struct usb_device *rhdev) { - struct usb_hub *hub; - int port1; - unsigned long flags; - dev_warn(&rhdev->dev, "root hub lost power or was reset\n"); - - spin_lock_irqsave(&device_state_lock, flags); - hub = hdev_to_hub(rhdev); - for (port1 = 1; port1 <= rhdev->maxchild; ++port1) { - if (rhdev->children[port1 - 1]) { - recursively_mark_NOTATTACHED( - rhdev->children[port1 - 1]); - set_bit(port1, hub->change_bits); - } - } - spin_unlock_irqrestore(&device_state_lock, flags); + rhdev->reset_resume = 1; } EXPORT_SYMBOL_GPL(usb_root_hub_lost_power); @@ -1513,29 +1620,6 @@ static int hub_port_reset(struct usb_hub *hub, int port1, return status; } -/* - * Disable a port and mark a logical connnect-change event, so that some - * time later khubd will disconnect() any existing usb_device on the port - * and will re-enumerate if there actually is a device attached. - */ -static void hub_port_logical_disconnect(struct usb_hub *hub, int port1) -{ - dev_dbg(hub->intfdev, "logical disconnect on port %d\n", port1); - hub_port_disable(hub, port1, 1); - - /* FIXME let caller ask to power down the port: - * - some devices won't enumerate without a VBUS power cycle - * - SRP saves power that way - * - ... new call, TBD ... - * That's easy if this hub can switch power per-port, and - * khubd reactivates the port later (timer, SRP, etc). - * Powerdown must be optional, because of reset/DFU. - */ - - set_bit(port1, hub->change_bits); - kick_khubd(hub); -} - #ifdef CONFIG_PM #ifdef CONFIG_USB_SUSPEND @@ -3018,7 +3102,7 @@ int usb_reset_composite_device(struct usb_device *udev, cintf->dev.driver) { drv = to_usb_driver(cintf->dev.driver); if (drv->post_reset) - (drv->post_reset)(cintf); + (drv->post_reset)(cintf, 0); } if (cintf != iface) up(&cintf->dev.sem); diff --git a/drivers/usb/core/usb.h b/drivers/usb/core/usb.h index 6f361df374f..1a486288673 100644 --- a/drivers/usb/core/usb.h +++ b/drivers/usb/core/usb.h @@ -36,6 +36,7 @@ extern void usb_host_cleanup(void); extern void usb_autosuspend_work(struct work_struct *work); extern int usb_port_suspend(struct usb_device *dev); extern int usb_port_resume(struct usb_device *dev); +extern int usb_reset_suspended_device(struct usb_device *udev); extern int usb_external_suspend_device(struct usb_device *udev, pm_message_t msg); extern int usb_external_resume_device(struct usb_device *udev); -- cgit v1.2.3