|
@@ -22,9 +22,8 @@
|
|
|
#include <linux/usb/hcd.h>
|
|
|
#include <linux/usb/otg.h>
|
|
|
#include <linux/usb/quirks.h>
|
|
|
-#include <linux/kthread.h>
|
|
|
+#include <linux/workqueue.h>
|
|
|
#include <linux/mutex.h>
|
|
|
-#include <linux/freezer.h>
|
|
|
#include <linux/random.h>
|
|
|
#include <linux/pm_qos.h>
|
|
|
|
|
@@ -42,14 +41,9 @@
|
|
|
* change to USB_STATE_NOTATTACHED even when the semaphore isn't held. */
|
|
|
static DEFINE_SPINLOCK(device_state_lock);
|
|
|
|
|
|
-/* khubd's worklist and its lock */
|
|
|
-static DEFINE_SPINLOCK(hub_event_lock);
|
|
|
-static LIST_HEAD(hub_event_list); /* List of hubs needing servicing */
|
|
|
-
|
|
|
-/* Wakes up khubd */
|
|
|
-static DECLARE_WAIT_QUEUE_HEAD(khubd_wait);
|
|
|
-
|
|
|
-static struct task_struct *khubd_task;
|
|
|
+/* workqueue to process hub events */
|
|
|
+static struct workqueue_struct *hub_wq;
|
|
|
+static void hub_event(struct work_struct *work);
|
|
|
|
|
|
/* synchronize hub-port add/remove and peering operations */
|
|
|
DEFINE_MUTEX(usb_port_peer_mutex);
|
|
@@ -105,6 +99,7 @@ EXPORT_SYMBOL_GPL(ehci_cf_port_reset_rwsem);
|
|
|
#define HUB_DEBOUNCE_STEP 25
|
|
|
#define HUB_DEBOUNCE_STABLE 100
|
|
|
|
|
|
+static void hub_release(struct kref *kref);
|
|
|
static int usb_reset_and_verify_device(struct usb_device *udev);
|
|
|
|
|
|
static inline char *portspeed(struct usb_hub *hub, int portstatus)
|
|
@@ -576,20 +571,31 @@ static int hub_port_status(struct usb_hub *hub, int port1,
|
|
|
return ret;
|
|
|
}
|
|
|
|
|
|
-static void kick_khubd(struct usb_hub *hub)
|
|
|
+static void kick_hub_wq(struct usb_hub *hub)
|
|
|
{
|
|
|
- unsigned long flags;
|
|
|
+ struct usb_interface *intf;
|
|
|
|
|
|
- spin_lock_irqsave(&hub_event_lock, flags);
|
|
|
- if (!hub->disconnected && list_empty(&hub->event_list)) {
|
|
|
- list_add_tail(&hub->event_list, &hub_event_list);
|
|
|
+ if (hub->disconnected || work_pending(&hub->events))
|
|
|
+ return;
|
|
|
|
|
|
- /* Suppress autosuspend until khubd runs */
|
|
|
- usb_autopm_get_interface_no_resume(
|
|
|
- to_usb_interface(hub->intfdev));
|
|
|
- wake_up(&khubd_wait);
|
|
|
- }
|
|
|
- spin_unlock_irqrestore(&hub_event_lock, flags);
|
|
|
+ /*
|
|
|
+ * Suppress autosuspend until the event is proceed.
|
|
|
+ *
|
|
|
+ * Be careful and make sure that the symmetric operation is
|
|
|
+ * always called. We are here only when there is no pending
|
|
|
+ * work for this hub. Therefore put the interface either when
|
|
|
+ * the new work is called or when it is canceled.
|
|
|
+ */
|
|
|
+ intf = to_usb_interface(hub->intfdev);
|
|
|
+ usb_autopm_get_interface_no_resume(intf);
|
|
|
+ kref_get(&hub->kref);
|
|
|
+
|
|
|
+ if (queue_work(hub_wq, &hub->events))
|
|
|
+ return;
|
|
|
+
|
|
|
+ /* the work has already been scheduled */
|
|
|
+ usb_autopm_put_interface_async(intf);
|
|
|
+ kref_put(&hub->kref, hub_release);
|
|
|
}
|
|
|
|
|
|
void usb_kick_khubd(struct usb_device *hdev)
|
|
@@ -597,7 +603,7 @@ void usb_kick_khubd(struct usb_device *hdev)
|
|
|
struct usb_hub *hub = usb_hub_to_struct_hub(hdev);
|
|
|
|
|
|
if (hub)
|
|
|
- kick_khubd(hub);
|
|
|
+ kick_hub_wq(hub);
|
|
|
}
|
|
|
|
|
|
/*
|
|
@@ -619,7 +625,7 @@ void usb_wakeup_notification(struct usb_device *hdev,
|
|
|
hub = usb_hub_to_struct_hub(hdev);
|
|
|
if (hub) {
|
|
|
set_bit(portnum, hub->wakeup_bits);
|
|
|
- kick_khubd(hub);
|
|
|
+ kick_hub_wq(hub);
|
|
|
}
|
|
|
}
|
|
|
EXPORT_SYMBOL_GPL(usb_wakeup_notification);
|
|
@@ -659,7 +665,7 @@ static void hub_irq(struct urb *urb)
|
|
|
hub->nerrors = 0;
|
|
|
|
|
|
/* Something happened, let khubd figure it out */
|
|
|
- kick_khubd(hub);
|
|
|
+ kick_hub_wq(hub);
|
|
|
|
|
|
resubmit:
|
|
|
if (hub->quiescing)
|
|
@@ -973,7 +979,7 @@ static void hub_port_logical_disconnect(struct usb_hub *hub, int port1)
|
|
|
*/
|
|
|
|
|
|
set_bit(port1, hub->change_bits);
|
|
|
- kick_khubd(hub);
|
|
|
+ kick_hub_wq(hub);
|
|
|
}
|
|
|
|
|
|
/**
|
|
@@ -1241,7 +1247,7 @@ static void hub_activate(struct usb_hub *hub, enum hub_activation_type type)
|
|
|
&hub->leds, LED_CYCLE_PERIOD);
|
|
|
|
|
|
/* Scan all ports that need attention */
|
|
|
- kick_khubd(hub);
|
|
|
+ kick_hub_wq(hub);
|
|
|
|
|
|
/* Allow autosuspend if it was suppressed */
|
|
|
if (type <= HUB_INIT3)
|
|
@@ -1648,14 +1654,11 @@ static void hub_disconnect(struct usb_interface *intf)
|
|
|
struct usb_device *hdev = interface_to_usbdev(intf);
|
|
|
int port1;
|
|
|
|
|
|
- /* Take the hub off the event list and don't let it be added again */
|
|
|
- spin_lock_irq(&hub_event_lock);
|
|
|
- if (!list_empty(&hub->event_list)) {
|
|
|
- list_del_init(&hub->event_list);
|
|
|
- usb_autopm_put_interface_no_suspend(intf);
|
|
|
- }
|
|
|
+ /*
|
|
|
+ * Stop adding new hub events. We do not want to block here and thus
|
|
|
+ * will not try to remove any pending work item.
|
|
|
+ */
|
|
|
hub->disconnected = 1;
|
|
|
- spin_unlock_irq(&hub_event_lock);
|
|
|
|
|
|
/* Disconnect all children and quiesce the hub */
|
|
|
hub->error = 0;
|
|
@@ -1795,11 +1798,11 @@ descriptor_error:
|
|
|
}
|
|
|
|
|
|
kref_init(&hub->kref);
|
|
|
- INIT_LIST_HEAD(&hub->event_list);
|
|
|
hub->intfdev = &intf->dev;
|
|
|
hub->hdev = hdev;
|
|
|
INIT_DELAYED_WORK(&hub->leds, led_work);
|
|
|
INIT_DELAYED_WORK(&hub->init_work, NULL);
|
|
|
+ INIT_WORK(&hub->events, hub_event);
|
|
|
usb_get_intf(intf);
|
|
|
usb_get_dev(hdev);
|
|
|
|
|
@@ -4996,9 +4999,8 @@ static void port_event(struct usb_hub *hub, int port1)
|
|
|
hub_port_connect_change(hub, port1, portstatus, portchange);
|
|
|
}
|
|
|
|
|
|
-static void hub_event(void)
|
|
|
+static void hub_event(struct work_struct *work)
|
|
|
{
|
|
|
- struct list_head *tmp;
|
|
|
struct usb_device *hdev;
|
|
|
struct usb_interface *intf;
|
|
|
struct usb_hub *hub;
|
|
@@ -5007,23 +5009,11 @@ static void hub_event(void)
|
|
|
u16 hubchange;
|
|
|
int i, ret;
|
|
|
|
|
|
- /* Grab the first entry at the beginning of the list */
|
|
|
- spin_lock_irq(&hub_event_lock);
|
|
|
- if (list_empty(&hub_event_list)) {
|
|
|
- spin_unlock_irq(&hub_event_lock);
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- tmp = hub_event_list.next;
|
|
|
- list_del_init(tmp);
|
|
|
-
|
|
|
- hub = list_entry(tmp, struct usb_hub, event_list);
|
|
|
- kref_get(&hub->kref);
|
|
|
- spin_unlock_irq(&hub_event_lock);
|
|
|
-
|
|
|
+ hub = container_of(work, struct usb_hub, events);
|
|
|
hdev = hub->hdev;
|
|
|
hub_dev = hub->intfdev;
|
|
|
intf = to_usb_interface(hub_dev);
|
|
|
+
|
|
|
dev_dbg(hub_dev, "state %d ports %d chg %04x evt %04x\n",
|
|
|
hdev->state, hdev->maxchild,
|
|
|
/* NOTE: expects max 15 ports... */
|
|
@@ -5034,20 +5024,20 @@ static void hub_event(void)
|
|
|
* disconnected while waiting for the lock to succeed. */
|
|
|
usb_lock_device(hdev);
|
|
|
if (unlikely(hub->disconnected))
|
|
|
- goto out_disconnected;
|
|
|
+ goto out_hdev_lock;
|
|
|
|
|
|
/* If the hub has died, clean up after it */
|
|
|
if (hdev->state == USB_STATE_NOTATTACHED) {
|
|
|
hub->error = -ENODEV;
|
|
|
hub_quiesce(hub, HUB_DISCONNECT);
|
|
|
- goto out;
|
|
|
+ goto out_hdev_lock;
|
|
|
}
|
|
|
|
|
|
/* Autoresume */
|
|
|
ret = usb_autopm_get_interface(intf);
|
|
|
if (ret) {
|
|
|
dev_dbg(hub_dev, "Can't autoresume: %d\n", ret);
|
|
|
- goto out;
|
|
|
+ goto out_hdev_lock;
|
|
|
}
|
|
|
|
|
|
/* If this is an inactive hub, do nothing */
|
|
@@ -5124,34 +5114,12 @@ static void hub_event(void)
|
|
|
out_autopm:
|
|
|
/* Balance the usb_autopm_get_interface() above */
|
|
|
usb_autopm_put_interface_no_suspend(intf);
|
|
|
-out:
|
|
|
- /* Balance the usb_autopm_get_interface_no_resume() in
|
|
|
- * kick_khubd() and allow autosuspend.
|
|
|
- */
|
|
|
- usb_autopm_put_interface(intf);
|
|
|
-out_disconnected:
|
|
|
+out_hdev_lock:
|
|
|
usb_unlock_device(hdev);
|
|
|
- kref_put(&hub->kref, hub_release);
|
|
|
-}
|
|
|
|
|
|
-static int hub_thread(void *__unused)
|
|
|
-{
|
|
|
- /* khubd needs to be freezable to avoid interfering with USB-PERSIST
|
|
|
- * port handover. Otherwise it might see that a full-speed device
|
|
|
- * was gone before the EHCI controller had handed its port over to
|
|
|
- * the companion full-speed controller.
|
|
|
- */
|
|
|
- set_freezable();
|
|
|
-
|
|
|
- do {
|
|
|
- hub_event();
|
|
|
- wait_event_freezable(khubd_wait,
|
|
|
- !list_empty(&hub_event_list) ||
|
|
|
- kthread_should_stop());
|
|
|
- } while (!kthread_should_stop() || !list_empty(&hub_event_list));
|
|
|
-
|
|
|
- pr_debug("%s: khubd exiting\n", usbcore_name);
|
|
|
- return 0;
|
|
|
+ /* Balance the stuff in kick_hub_wq() and allow autosuspend */
|
|
|
+ usb_autopm_put_interface(intf);
|
|
|
+ kref_put(&hub->kref, hub_release);
|
|
|
}
|
|
|
|
|
|
static const struct usb_device_id hub_id_table[] = {
|
|
@@ -5191,20 +5159,29 @@ int usb_hub_init(void)
|
|
|
return -1;
|
|
|
}
|
|
|
|
|
|
- khubd_task = kthread_run(hub_thread, NULL, "khubd");
|
|
|
- if (!IS_ERR(khubd_task))
|
|
|
+ /*
|
|
|
+ * The workqueue needs to be freezable to avoid interfering with
|
|
|
+ * USB-PERSIST port handover. Otherwise it might see that a full-speed
|
|
|
+ * device was gone before the EHCI controller had handed its port
|
|
|
+ * over to the companion full-speed controller.
|
|
|
+ *
|
|
|
+ * Also we use ordered workqueue because the code is not ready
|
|
|
+ * for parallel execution of hub events, see choose_devnum().
|
|
|
+ */
|
|
|
+ hub_wq = alloc_ordered_workqueue("usb_hub_wq", WQ_FREEZABLE);
|
|
|
+ if (hub_wq)
|
|
|
return 0;
|
|
|
|
|
|
/* Fall through if kernel_thread failed */
|
|
|
usb_deregister(&hub_driver);
|
|
|
- printk(KERN_ERR "%s: can't start khubd\n", usbcore_name);
|
|
|
+ pr_err("%s: can't allocate workqueue for usb hub\n", usbcore_name);
|
|
|
|
|
|
return -1;
|
|
|
}
|
|
|
|
|
|
void usb_hub_cleanup(void)
|
|
|
{
|
|
|
- kthread_stop(khubd_task);
|
|
|
+ destroy_workqueue(hub_wq);
|
|
|
|
|
|
/*
|
|
|
* Hub resources are freed for us by usb_deregister. It calls
|