usbfs: Add ioctls for runtime power management
authorAlan Stern <stern@rowland.harvard.edu>
Wed, 7 Aug 2019 14:29:50 +0000 (10:29 -0400)
committerGreg Kroah-Hartman <gregkh@linuxfoundation.org>
Fri, 9 Aug 2019 05:55:45 +0000 (07:55 +0200)
It has been requested that usbfs should implement runtime power
management, instead of forcing the device to remain at full power as
long as the device file is open.  This patch introduces that new
feature.

It does so by adding three new usbfs ioctls:

USBDEVFS_FORBID_SUSPEND: Prevents the device from going into
runtime suspend (and causes a resume if the device is already
suspended).

USBDEVFS_ALLOW_SUSPEND: Allows the device to go into runtime
suspend.  Some time may elapse before the device actually is
suspended, depending on things like the autosuspend delay.

USBDEVFS_WAIT_FOR_RESUME: Blocks until the call is interrupted
by a signal or at least one runtime resume has occurred since
the most recent ALLOW_SUSPEND ioctl call (which may mean
immediately, even if the device is currently suspended).  In
the latter case, the device is prevented from suspending again
just as if FORBID_SUSPEND was called before the ioctl returns.

For backward compatibility, when the device file is first opened
runtime suspends are forbidden.  The userspace program can then allow
suspends whenever it wants, and either resume the device directly (by
forbidding suspends again) or wait for a resume from some other source
(such as a remote wakeup).  URBs submitted to a suspended device will
fail or will complete with an appropriate error code.

This combination of ioctls is sufficient for user programs to have
nearly the same degree of control over a device's runtime power
behavior as kernel drivers do.

Still lacking is documentation for the new ioctls.  I intend to add it
later, after the existing documentation for the usbfs userspace API is
straightened out into a reasonable form.

Suggested-by: Mayuresh Kulkarni <mkulkarni@opensource.cirrus.com>
Signed-off-by: Alan Stern <stern@rowland.harvard.edu>
Link: https://lore.kernel.org/r/Pine.LNX.4.44L0.1908071013220.1514-100000@iolanthe.rowland.org
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
drivers/usb/core/devio.c
drivers/usb/core/generic.c
drivers/usb/core/usb.h
include/uapi/linux/usbdevice_fs.h

index b265ab5..cdd34dc 100644 (file)
@@ -48,6 +48,9 @@
 #define USB_DEVICE_MAX                 (USB_MAXBUS * 128)
 #define USB_SG_SIZE                    16384 /* split-size for large txs */
 
+/* Mutual exclusion for ps->list in resume vs. release and remove */
+static DEFINE_MUTEX(usbfs_mutex);
+
 struct usb_dev_state {
        struct list_head list;      /* state list */
        struct usb_device *dev;
@@ -57,14 +60,17 @@ struct usb_dev_state {
        struct list_head async_completed;
        struct list_head memory_list;
        wait_queue_head_t wait;     /* wake up if a request completed */
+       wait_queue_head_t wait_for_resume;   /* wake up upon runtime resume */
        unsigned int discsignr;
        struct pid *disc_pid;
        const struct cred *cred;
        sigval_t disccontext;
        unsigned long ifclaimed;
        u32 disabled_bulk_eps;
-       bool privileges_dropped;
        unsigned long interface_allowed_mask;
+       int not_yet_resumed;
+       bool suspend_allowed;
+       bool privileges_dropped;
 };
 
 struct usb_memory {
@@ -694,9 +700,7 @@ static void driver_disconnect(struct usb_interface *intf)
        destroy_async_on_interface(ps, ifnum);
 }
 
-/* The following routines are merely placeholders.  There is no way
- * to inform a user task about suspend or resumes.
- */
+/* We don't care about suspend/resume of claimed interfaces */
 static int driver_suspend(struct usb_interface *intf, pm_message_t msg)
 {
        return 0;
@@ -707,12 +711,32 @@ static int driver_resume(struct usb_interface *intf)
        return 0;
 }
 
+/* The following routines apply to the entire device, not interfaces */
+void usbfs_notify_suspend(struct usb_device *udev)
+{
+       /* We don't need to handle this */
+}
+
+void usbfs_notify_resume(struct usb_device *udev)
+{
+       struct usb_dev_state *ps;
+
+       /* Protect against simultaneous remove or release */
+       mutex_lock(&usbfs_mutex);
+       list_for_each_entry(ps, &udev->filelist, list) {
+               WRITE_ONCE(ps->not_yet_resumed, 0);
+               wake_up_all(&ps->wait_for_resume);
+       }
+       mutex_unlock(&usbfs_mutex);
+}
+
 struct usb_driver usbfs_driver = {
        .name =         "usbfs",
        .probe =        driver_probe,
        .disconnect =   driver_disconnect,
        .suspend =      driver_suspend,
        .resume =       driver_resume,
+       .supports_autosuspend = 1,
 };
 
 static int claimintf(struct usb_dev_state *ps, unsigned int ifnum)
@@ -997,9 +1021,12 @@ static int usbdev_open(struct inode *inode, struct file *file)
        INIT_LIST_HEAD(&ps->async_completed);
        INIT_LIST_HEAD(&ps->memory_list);
        init_waitqueue_head(&ps->wait);
+       init_waitqueue_head(&ps->wait_for_resume);
        ps->disc_pid = get_pid(task_pid(current));
        ps->cred = get_current_cred();
        smp_wmb();
+
+       /* Can't race with resume; the device is already active */
        list_add_tail(&ps->list, &dev->filelist);
        file->private_data = ps;
        usb_unlock_device(dev);
@@ -1025,7 +1052,10 @@ static int usbdev_release(struct inode *inode, struct file *file)
        usb_lock_device(dev);
        usb_hub_release_all_ports(dev, ps);
 
+       /* Protect against simultaneous resume */
+       mutex_lock(&usbfs_mutex);
        list_del_init(&ps->list);
+       mutex_unlock(&usbfs_mutex);
 
        for (ifnum = 0; ps->ifclaimed && ifnum < 8*sizeof(ps->ifclaimed);
                        ifnum++) {
@@ -1033,7 +1063,8 @@ static int usbdev_release(struct inode *inode, struct file *file)
                        releaseintf(ps, ifnum);
        }
        destroy_all_async(ps);
-       usb_autosuspend_device(dev);
+       if (!ps->suspend_allowed)
+               usb_autosuspend_device(dev);
        usb_unlock_device(dev);
        usb_put_dev(dev);
        put_pid(ps->disc_pid);
@@ -2384,6 +2415,47 @@ static int proc_drop_privileges(struct usb_dev_state *ps, void __user *arg)
        return 0;
 }
 
+static int proc_forbid_suspend(struct usb_dev_state *ps)
+{
+       int ret = 0;
+
+       if (ps->suspend_allowed) {
+               ret = usb_autoresume_device(ps->dev);
+               if (ret == 0)
+                       ps->suspend_allowed = false;
+               else if (ret != -ENODEV)
+                       ret = -EIO;
+       }
+       return ret;
+}
+
+static int proc_allow_suspend(struct usb_dev_state *ps)
+{
+       if (!connected(ps))
+               return -ENODEV;
+
+       WRITE_ONCE(ps->not_yet_resumed, 1);
+       if (!ps->suspend_allowed) {
+               usb_autosuspend_device(ps->dev);
+               ps->suspend_allowed = true;
+       }
+       return 0;
+}
+
+static int proc_wait_for_resume(struct usb_dev_state *ps)
+{
+       int ret;
+
+       usb_unlock_device(ps->dev);
+       ret = wait_event_interruptible(ps->wait_for_resume,
+                       READ_ONCE(ps->not_yet_resumed) == 0);
+       usb_lock_device(ps->dev);
+
+       if (ret != 0)
+               return -EINTR;
+       return proc_forbid_suspend(ps);
+}
+
 /*
  * NOTE:  All requests here that have interface numbers as parameters
  * are assuming that somehow the configuration has been prevented from
@@ -2578,6 +2650,15 @@ static long usbdev_do_ioctl(struct file *file, unsigned int cmd,
        case USBDEVFS_GET_SPEED:
                ret = ps->dev->speed;
                break;
+       case USBDEVFS_FORBID_SUSPEND:
+               ret = proc_forbid_suspend(ps);
+               break;
+       case USBDEVFS_ALLOW_SUSPEND:
+               ret = proc_allow_suspend(ps);
+               break;
+       case USBDEVFS_WAIT_FOR_RESUME:
+               ret = proc_wait_for_resume(ps);
+               break;
        }
 
        /* Handle variable-length commands */
@@ -2651,15 +2732,20 @@ static void usbdev_remove(struct usb_device *udev)
 {
        struct usb_dev_state *ps;
 
+       /* Protect against simultaneous resume */
+       mutex_lock(&usbfs_mutex);
        while (!list_empty(&udev->filelist)) {
                ps = list_entry(udev->filelist.next, struct usb_dev_state, list);
                destroy_all_async(ps);
                wake_up_all(&ps->wait);
+               WRITE_ONCE(ps->not_yet_resumed, 0);
+               wake_up_all(&ps->wait_for_resume);
                list_del_init(&ps->list);
                if (ps->discsignr)
                        kill_pid_usb_asyncio(ps->discsignr, EPIPE, ps->disccontext,
                                             ps->disc_pid, ps->cred);
        }
+       mutex_unlock(&usbfs_mutex);
 }
 
 static int usbdev_notify(struct notifier_block *self,
index 1ac9c1e..38f8b3e 100644 (file)
@@ -257,6 +257,8 @@ static int generic_suspend(struct usb_device *udev, pm_message_t msg)
        else
                rc = usb_port_suspend(udev, msg);
 
+       if (rc == 0)
+               usbfs_notify_suspend(udev);
        return rc;
 }
 
@@ -273,6 +275,9 @@ static int generic_resume(struct usb_device *udev, pm_message_t msg)
                rc = hcd_bus_resume(udev, msg);
        else
                rc = usb_port_resume(udev, msg);
+
+       if (rc == 0)
+               usbfs_notify_resume(udev);
        return rc;
 }
 
index bd8d01f..2932d1e 100644 (file)
@@ -95,6 +95,9 @@ extern int usb_runtime_idle(struct device *dev);
 extern int usb_enable_usb2_hardware_lpm(struct usb_device *udev);
 extern int usb_disable_usb2_hardware_lpm(struct usb_device *udev);
 
+extern void usbfs_notify_suspend(struct usb_device *udev);
+extern void usbfs_notify_resume(struct usb_device *udev);
+
 #else
 
 static inline int usb_port_suspend(struct usb_device *udev, pm_message_t msg)
index 78efe87..d24bbb6 100644 (file)
@@ -223,5 +223,8 @@ struct usbdevfs_streams {
  * extending size of the data returned.
  */
 #define USBDEVFS_CONNINFO_EX(len)  _IOC(_IOC_READ, 'U', 32, len)
+#define USBDEVFS_FORBID_SUSPEND    _IO('U', 33)
+#define USBDEVFS_ALLOW_SUSPEND     _IO('U', 34)
+#define USBDEVFS_WAIT_FOR_RESUME   _IO('U', 35)
 
 #endif /* _UAPI_LINUX_USBDEVICE_FS_H */