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 --- Documentation/power/swsusp.txt | 3 + Documentation/usb/persist.txt | 144 ++++++++++++++++++++++++++++++ drivers/hid/usbhid/hid-core.c | 2 +- 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 + drivers/usb/storage/usb.c | 8 +- include/linux/usb.h | 8 +- 10 files changed, 351 insertions(+), 77 deletions(-) create mode 100644 Documentation/usb/persist.txt diff --git a/Documentation/power/swsusp.txt b/Documentation/power/swsusp.txt index 5b8d6953f05..152b510d1bb 100644 --- a/Documentation/power/swsusp.txt +++ b/Documentation/power/swsusp.txt @@ -393,6 +393,9 @@ safest thing is to unmount all filesystems on removable media (such USB, Firewire, CompactFlash, MMC, external SATA, or even IDE hotplug bays) before suspending; then remount them after resuming. +There is a work-around for this problem. For more information, see +Documentation/usb/persist.txt. + Q: I upgraded the kernel from 2.6.15 to 2.6.16. Both kernels were compiled with the similar configuration files. Anyway I found that suspend to disk (and resume) is much slower on 2.6.16 compared to diff --git a/Documentation/usb/persist.txt b/Documentation/usb/persist.txt new file mode 100644 index 00000000000..6dcd5f88479 --- /dev/null +++ b/Documentation/usb/persist.txt @@ -0,0 +1,144 @@ + USB device persistence during system suspend + + Alan Stern + + September 2, 2006 (Updated March 27, 2007) + + + What is the problem? + +According to the USB specification, when a USB bus is suspended the +bus must continue to supply suspend current (around 1-5 mA). This +is so that devices can maintain their internal state and hubs can +detect connect-change events (devices being plugged in or unplugged). +The technical term is "power session". + +If a USB device's power session is interrupted then the system is +required to behave as though the device has been unplugged. It's a +conservative approach; in the absence of suspend current the computer +has no way to know what has actually happened. Perhaps the same +device is still attached or perhaps it was removed and a different +device plugged into the port. The system must assume the worst. + +By default, Linux behaves according to the spec. If a USB host +controller loses power during a system suspend, then when the system +wakes up all the devices attached to that controller are treated as +though they had disconnected. This is always safe and it is the +"officially correct" thing to do. + +For many sorts of devices this behavior doesn't matter in the least. +If the kernel wants to believe that your USB keyboard was unplugged +while the system was asleep and a new keyboard was plugged in when the +system woke up, who cares? It'll still work the same when you type on +it. + +Unfortunately problems _can_ arise, particularly with mass-storage +devices. The effect is exactly the same as if the device really had +been unplugged while the system was suspended. If you had a mounted +filesystem on the device, you're out of luck -- everything in that +filesystem is now inaccessible. This is especially annoying if your +root filesystem was located on the device, since your system will +instantly crash. + +Loss of power isn't the only mechanism to worry about. Anything that +interrupts a power session will have the same effect. For example, +even though suspend current may have been maintained while the system +was asleep, on many systems during the initial stages of wakeup the +firmware (i.e., the BIOS) resets the motherboard's USB host +controllers. Result: all the power sessions are destroyed and again +it's as though you had unplugged all the USB devices. Yes, it's +entirely the BIOS's fault, but that doesn't do _you_ any good unless +you can convince the BIOS supplier to fix the problem (lots of luck!). + +On many systems the USB host controllers will get reset after a +suspend-to-RAM. On almost all systems, no suspend current is +available during suspend-to-disk (also known as swsusp). You can +check the kernel log after resuming to see if either of these has +happened; look for lines saying "root hub lost power or was reset". + +In practice, people are forced to unmount any filesystems on a USB +device before suspending. If the root filesystem is on a USB device, +the system can't be suspended at all. (All right, it _can_ be +suspended -- but it will crash as soon as it wakes up, which isn't +much better.) + + + What is the solution? + +Setting CONFIG_USB_PERSIST will cause the kernel to work around these +issues. It enables a mode in which the core USB device data +structures are allowed to persist across a power-session disruption. +It works like this. If the kernel sees that a USB host controller is +not in the expected state during resume (i.e., if the controller was +reset or otherwise had lost power) then it applies a persistence check +to each of the USB devices below that controller. It doesn't try to +resume the device; that can't work once the power session is gone. +Instead it issues a USB port reset and then re-enumerates the device. +(This is exactly the same thing that happens whenever a USB device is +reset.) If the re-enumeration shows that the device now attached to +that port has the same descriptors as before, including the Vendor and +Product IDs, then the kernel continues to use the same device +structure. In effect, the kernel treats the device as though it had +merely been reset instead of unplugged. + +If no device is now attached to the port, or if the descriptors are +different from what the kernel remembers, then the treatment is what +you would expect. The kernel destroys the old device structure and +behaves as though the old device had been unplugged and a new device +plugged in, just as it would without the CONFIG_USB_PERSIST option. + +The end result is that the USB device remains available and usable. +Filesystem mounts and memory mappings are unaffected, and the world is +now a good and happy place. + + + Is this the best solution? + +Perhaps not. Arguably, keeping track of mounted filesystems and +memory mappings across device disconnects should be handled by a +centralized Logical Volume Manager. Such a solution would allow you +to plug in a USB flash device, create a persistent volume associated +with it, unplug the flash device, plug it back in later, and still +have the same persistent volume associated with the device. As such +it would be more far-reaching than CONFIG_USB_PERSIST. + +On the other hand, writing a persistent volume manager would be a big +job and using it would require significant input from the user. This +solution is much quicker and easier -- and it exists now, a giant +point in its favor! + +Furthermore, the USB_PERSIST option applies to _all_ USB devices, not +just mass-storage devices. It might turn out to be equally useful for +other device types, such as network interfaces. + + + WARNING: Using CONFIG_USB_PERSIST can be dangerous!! + +When recovering an interrupted power session the kernel does its best +to make sure the USB device hasn't been changed; that is, the same +device is still plugged into the port as before. But the checks +aren't guaranteed to be 100% accurate. + +If you replace one USB device with another of the same type (same +manufacturer, same IDs, and so on) there's an excellent chance the +kernel won't detect the change. Serial numbers and other strings are +not compared. In many cases it wouldn't help if they were, because +manufacturers frequently omit serial numbers entirely in their +devices. + +Furthermore it's quite possible to leave a USB device exactly the same +while changing its media. If you replace the flash memory card in a +USB card reader while the system is asleep, the kernel will have no +way to know you did it. The kernel will assume that nothing has +happened and will continue to use the partition tables, inodes, and +memory mappings for the old card. + +If the kernel gets fooled in this way, it's almost certain to cause +data corruption and to crash your system. You'll have no one to blame +but yourself. + +YOU HAVE BEEN WARNED! USE AT YOUR OWN RISK! + +That having been said, most of the time there shouldn't be any trouble +at all. The "persist" feature can be extremely useful. Make the most +of it. diff --git a/drivers/hid/usbhid/hid-core.c b/drivers/hid/usbhid/hid-core.c index 3afa4a5035b..e221b0d1f66 100644 --- a/drivers/hid/usbhid/hid-core.c +++ b/drivers/hid/usbhid/hid-core.c @@ -1015,7 +1015,7 @@ static void hid_pre_reset(struct usb_interface *intf) hid_suspend(intf, PMSG_ON); } -static void hid_post_reset(struct usb_interface *intf) +static void hid_post_reset(struct usb_interface *intf, int reset_resume) { struct usb_device *dev = interface_to_usbdev (intf); 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); diff --git a/drivers/usb/storage/usb.c b/drivers/usb/storage/usb.c index df5dc186aef..be4cd8fe4ce 100644 --- a/drivers/usb/storage/usb.c +++ b/drivers/usb/storage/usb.c @@ -236,7 +236,7 @@ static void storage_pre_reset(struct usb_interface *iface) mutex_lock(&us->dev_mutex); } -static void storage_post_reset(struct usb_interface *iface) +static void storage_post_reset(struct usb_interface *iface, int reset_resume) { struct us_data *us = usb_get_intfdata(iface); @@ -249,7 +249,11 @@ static void storage_post_reset(struct usb_interface *iface) /* FIXME: Notify the subdrivers that they need to reinitialize * the device */ - mutex_unlock(&us->dev_mutex); + + /* If this is a reset-resume then the pre_reset routine wasn't + * called, so we don't need to unlock the mutex. */ + if (!reset_resume) + mutex_unlock(&us->dev_mutex); } /* diff --git a/include/linux/usb.h b/include/linux/usb.h index 56aa2ee21f1..3d63e0c2dd7 100644 --- a/include/linux/usb.h +++ b/include/linux/usb.h @@ -403,6 +403,7 @@ struct usb_device { unsigned auto_pm:1; /* autosuspend/resume in progress */ unsigned do_remote_wakeup:1; /* remote wakeup should be enabled */ + unsigned reset_resume:1; /* needs reset instead of resume */ unsigned autosuspend_disabled:1; /* autosuspend and autoresume */ unsigned autoresume_disabled:1; /* disabled by the user */ #endif @@ -819,7 +820,10 @@ struct usbdrv_wrap { * @pre_reset: Called by usb_reset_composite_device() when the device * is about to be reset. * @post_reset: Called by usb_reset_composite_device() after the device - * has been reset. + * has been reset, or in lieu of @resume following a reset-resume + * (i.e., the device is reset instead of being resumed, as might + * happen if power was lost). The second argument tells which is + * the reason. * @id_table: USB drivers use ID table to support hotplugging. * Export this with MODULE_DEVICE_TABLE(usb,...). This must be set * or your driver's probe function will never get called. @@ -861,7 +865,7 @@ struct usb_driver { int (*resume) (struct usb_interface *intf); void (*pre_reset) (struct usb_interface *intf); - void (*post_reset) (struct usb_interface *intf); + void (*post_reset) (struct usb_interface *intf, int reset_resume); const struct usb_device_id *id_table; -- cgit v1.2.3