Re: [PATCH v3 03/10] usb: find internal hub tier mismatch via acpi

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



On Fri, 2014-01-17 at 10:53 -0500, Alan Stern wrote:
> On Thu, 16 Jan 2014, Dan Williams wrote:
> 
> > Maybe I need to consider it a bit longer, but introducing a per-port
> > mutex adds at least 2 new lock ordering constraints.  A replacement of
> > hub->busy_bits with port_dev->lock now introduces a constraint with not
> > only the child lock, but also synchronous invocations of rpm_{suspend|
> > resume} for the port.
> 
> That second constraint is very simple: Since the resume operation will 
> acquire the port lock, you have to make sure you don't hold the port 
> lock when performing a resume.
> 
> > Patch 7 would look something like this:
> > 
> > ---
> > diff --git a/drivers/usb/core/hub.c b/drivers/usb/core/hub.c
> > index d86548edcc36..da12d07da127 100644
> > --- a/drivers/usb/core/hub.c
> > +++ b/drivers/usb/core/hub.c
> > @@ -4740,6 +4740,13 @@ static void hub_events(void)
> >  
> >                 /* deal with port status changes */
> >                 for (i = 1; i <= hdev->maxchild; i++) {
> > +                       mutex_lock(&port_dev->lock);
> > +                       if (port_dev->power_is_on)
> > +                               do_hub_event(...);
> > +                       else
> > +                               hub_clear_misc_change(..);
> > +                       mutex_unlock(&port_dev->lock);
> > +
> > [..]
> 
> I wouldn't split out hub_clear_misc_change in quite that way, but never
> mind.
> 
> Also, you left out the places in hub_events where the port lock needs
> to be dropped: around the calls to usb_reset_device and 
> hub_port_connect_change.  And you left out the places in the resume and 
> reset routines where the port lock needs to be acquired.
> 
> > diff --git a/drivers/usb/core/port.c b/drivers/usb/core/port.c
> > index 97e4939fee1a..a7f32f200d90 100644
> > --- a/drivers/usb/core/port.c
> > +++ b/drivers/usb/core/port.c
> > @@ -88,7 +88,7 @@ static int usb_port_runtime_resume(struct device *dev)
> >                 pm_runtime_get_sync(&peer->dev);
> >  
> >         usb_autopm_get_interface(intf);
> > -       set_bit(port1, hub->busy_bits);
> > +       mutex_lock(&port_dev->lock);
> >  
> >         retval = usb_hub_set_port_power(hdev, hub, port1, true);
> >         if (port_dev->child && !retval) {
> > @@ -105,7 +105,7 @@ static int usb_port_runtime_resume(struct device *dev)
> >                 retval = 0;
> >         }
> >  
> > -       clear_bit(port1, hub->busy_bits);
> > +       mutex_unlock(&port_dev->lock);
> >         usb_autopm_put_interface(intf);
> >  
> >         if (!hub_is_superspeed(hdev) && peer)
> > 
> > @@ -138,12 +138,12 @@ static int usb_port_runtime_suspend(struct device *dev)
> >                 return -EBUSY;
> >  
> >         usb_autopm_get_interface(intf);
> > -       set_bit(port1, hub->busy_bits);
> > +       mutex_lock(&port_dev->lock);
> >         retval = usb_hub_set_port_power(hdev, hub, port1, false);
> >         usb_clear_port_feature(hdev, port1, USB_PORT_FEAT_C_CONNECTION);
> >         if (!hub_is_superspeed(hdev))
> >                 usb_clear_port_feature(hdev, port1, USB_PORT_FEAT_C_ENABLE);
> > -       clear_bit(port1, hub->busy_bits);
> > +       mutex_unlock(&port_dev->lock);
> >         usb_autopm_put_interface(intf);
> >  
> >         /* bounce our peer now that we are down */
> > ---
> 
> Yes, something like that.
> 
> > ...not too bad, although I don't really like ->power_is_on compared to
> > just reading the pm runtime state.  If we use pm_runtime_active() then
> > this begins to look a lot like patch 7 again.
> > 
> > However, patch 9 wants khubd held off until both the port and any child
> > devices have had a chance to resume.  It would expand the scope of the
> > lock from protecting concurrent port access to ordering port vs child
> > device resume.  Patch 9 as coded does so without adding a locking
> > constraint (beyond flushing resume work).  We certainly can't wrap a
> > port mutex around usb_autoresume_device(port_dev->child) given the child
> > synchronously resumes the port.  What is the alternative I am missing?
> 
> For one thing, like I mentioned above, usb_port_resume needs to hold
> the port lock.  (And so does usb_reset_device, after it calls 
> usb_autoresume_device.)
> 
> But maybe I'm not getting your point.
> 
> > Some nice benefits fall out from forcing a port+child resume cycle on
> > port resume:
> > 
> > 1/ prevents usb_port_runtime_resume() from growing recovery logic that
> > usb_port_resume() implements, like reset and disconnect.
> > 
> > 2/ similarly, if usb_port_resume() grows warm reset recovery logic (as
> > it seems to need) that is applicable to port power recovery as well.
> > 
> > Leaning on pm_runtime synchronization to implement new hub_events()
> > requirements of "port pm active" and "flushes pending usb_device
> > recovery" is a tighter implementation.  Specifically, it is tighter than
> > adding a new lock and its ordering constraints between suspend, resume,
> > child and potentially peer port events.
> 
> If you mean that we should avoid duplicating code between 
> usb_port_resume/finish_port_resume and the resume routines in port.c, I 
> agree.
> 
> The basic idea of using runtime PM synchronization to prevent khubd and
> port power operations from interfering sounds good, and it is simpler
> than adding a new mutex.  And I think we can also use it to prevent
> port power operations and port resets from interfering.  But that
> leaves open the question of how to prevent usb_device_reset from
> interfering with khubd.
> 

Yes, patch 9 narrowly addresses a problem that needs a wider solution.

We want to:

1/ prevent khubd from running while reset is in progress

2/ prevent khubd from seeing an intermediate device state during
usb_port_resume

3/ prevent khubd from running in the interval between completion of
ubs_port_runtime_resume() completing and usb_port_resume() running.

All these scenarios to point to problems with ->device_state collisions
not necessarily the port state.  How about a new lock that synchronizes
device state changes with state checks?  This replaces patch 9, only
compile tested:

diff --git a/drivers/usb/core/driver.c b/drivers/usb/core/driver.c
index 8d989b1d3dc5..edf30888e282 100644
--- a/drivers/usb/core/driver.c
+++ b/drivers/usb/core/driver.c
@@ -1477,6 +1477,9 @@ void usb_autosuspend_device(struct usb_device *udev)
 {
 	int	status;
 
+	if (!udev)
+		return;
+
 	usb_mark_last_busy(udev);
 	status = pm_runtime_put_sync_autosuspend(&udev->dev);
 	dev_vdbg(&udev->dev, "%s: cnt %d -> %d\n",
@@ -1508,6 +1511,9 @@ int usb_autoresume_device(struct usb_device *udev)
 {
 	int	status;
 
+	if (!udev)
+		return 0;
+
 	status = pm_runtime_get_sync(&udev->dev);
 	if (status < 0)
 		pm_runtime_put_sync(&udev->dev);
@@ -1746,7 +1752,10 @@ int usb_runtime_suspend(struct device *dev)
 	if (autosuspend_check(udev) != 0)
 		return -EAGAIN;
 
+	/* lock the device to synchronize its state with khubd */
+	mutex_lock(&udev->state_lock);
 	status = usb_suspend_both(udev, PMSG_AUTO_SUSPEND);
+	mutex_unlock(&udev->state_lock);
 
 	/* Allow a retry if autosuspend failed temporarily */
 	if (status == -EAGAIN || status == -EBUSY)
@@ -1765,10 +1774,15 @@ int usb_runtime_resume(struct device *dev)
 	struct usb_device	*udev = to_usb_device(dev);
 	int			status;
 
-	/* Runtime resume for a USB device means resuming both the device
-	 * and all its interfaces.
+	WARN_ON_ONCE(!mutex_is_locked(&dev->mutex));
+	/* Runtime resume for a USB device means resuming both the
+	 * device and all its interfaces.  We lock to synchronize the
+	 * device state with khubd
 	 */
+	mutex_lock(&udev->state_lock);
 	status = usb_resume_both(udev, PMSG_AUTO_RESUME);
+	mutex_unlock(&udev->state_lock);
+
 	return status;
 }
 
diff --git a/drivers/usb/core/hub.c b/drivers/usb/core/hub.c
index 80661e20de9e..63d789558b1d 100644
--- a/drivers/usb/core/hub.c
+++ b/drivers/usb/core/hub.c
@@ -4382,7 +4382,7 @@ static void hub_port_connect_change(struct usb_hub *hub, int port1,
 	unsigned wHubCharacteristics =
 			le16_to_cpu(hub->descriptor->wHubCharacteristics);
 	struct usb_device *udev;
-	int status, i;
+	int status = -ENODEV, i;
 	unsigned unit_load;
 
 	dev_dbg (hub_dev,
@@ -4403,9 +4403,14 @@ static void hub_port_connect_change(struct usb_hub *hub, int port1,
 
 	/* Try to resuscitate an existing device */
 	udev = hub->ports[port1 - 1]->child;
+
+	/* lock the device is case we need to wakeup, otherwise flush
+	 * any pending state changes
+	 */
+	usb_lock_device(udev);
+	usb_lock_device_state(udev);
 	if ((portstatus & USB_PORT_STAT_CONNECTION) && udev &&
 			udev->state != USB_STATE_NOTATTACHED) {
-		usb_lock_device(udev);
 		if (portstatus & USB_PORT_STAT_ENABLE) {
 			status = 0;		/* Nothing to do */
 
@@ -4417,17 +4422,16 @@ static void hub_port_connect_change(struct usb_hub *hub, int port1,
 			 */
 			status = usb_remote_wakeup(udev);
 #endif
-
 		} else {
-			status = -ENODEV;	/* Don't resuscitate */
-		}
-		usb_unlock_device(udev);
-
-		if (status == 0) {
-			clear_bit(port1, hub->change_bits);
-			return;
+			/* Don't resuscitate */;
 		}
 	}
+	usb_unlock_device_state(udev);
+	usb_unlock_device(udev);
+
+	clear_bit(port1, hub->change_bits);
+	if (status == 0)
+		return;
 
 	/* Disconnect any existing devices under this port */
 	if (udev) {
@@ -4436,7 +4440,6 @@ static void hub_port_connect_change(struct usb_hub *hub, int port1,
 			usb_phy_notify_disconnect(hcd->phy, udev->speed);
 		usb_disconnect(&hub->ports[port1 - 1]->child);
 	}
-	clear_bit(port1, hub->change_bits);
 
 	/* We can forget about a "removed" device when there's a physical
 	 * disconnect or the connect status changes.
@@ -4625,32 +4628,41 @@ static int hub_handle_remote_wakeup(struct usb_hub *hub, unsigned int port,
 
 	hdev = hub->hdev;
 	udev = hub->ports[port - 1]->child;
+
+	/* lock the device since we may issue resume here, lock the
+	 * device_state to synchronize with an in-flight pm operation
+	 */
+	usb_lock_device(udev);
+	usb_lock_device_state(udev);
 	if (!hub_is_superspeed(hdev)) {
 		if (!(portchange & USB_PORT_STAT_C_SUSPEND))
-			return 0;
+			goto out;
 		usb_clear_port_feature(hdev, port, USB_PORT_FEAT_C_SUSPEND);
 	} else {
 		if (!udev || udev->state != USB_STATE_SUSPENDED ||
 				 (portstatus & USB_PORT_STAT_LINK_STATE) !=
 				 USB_SS_PORT_LS_U0)
-			return 0;
+			goto out;
 	}
 
 	if (udev) {
 		/* TRSMRCY = 10 msec */
 		msleep(10);
 
-		usb_lock_device(udev);
 		ret = usb_remote_wakeup(udev);
-		usb_unlock_device(udev);
 		if (ret < 0)
 			connect_change = 1;
 	} else {
 		ret = -ENODEV;
 		hub_port_disable(hub, port, 1);
 	}
+
 	dev_dbg(hub->intfdev, "resume on port %d, status %d\n",
 			port, ret);
+ out:
+	usb_unlock_device_state(udev);
+	usb_unlock_device(udev);
+
 	return connect_change;
 }
 
@@ -4811,6 +4823,19 @@ static void hub_events(void)
 
 		/* deal with port status changes */
 		for_each_pm_active_port(i, port_dev, hub) {
+			struct usb_device *udev = port_dev->child;
+
+			/* usb_port_runtime_resume may have requested resume make
+			 * sure it has completed
+			 */
+			if (port_dev->resume_child) {
+				port_dev->resume_child = 0;
+				usb_lock_device(udev);
+				usb_autoresume_device(udev);
+				usb_autosuspend_device(udev);
+				usb_unlock_device(udev);
+			}
+
 			if (test_bit(i, hub->busy_bits))
 				continue;
 			connect_change = test_bit(i, hub->change_bits);
@@ -4883,8 +4908,9 @@ static void hub_events(void)
 			 */
 			if (hub_port_warm_reset_required(hub, portstatus)) {
 				int status;
-				struct usb_device *udev = port_dev->child;
 
+				usb_lock_device(udev);
+				usb_lock_device_state(udev);
 				dev_dbg(hub_dev, "warm reset port %d\n", i);
 				if (!udev ||
 				    !(portstatus & USB_PORT_STAT_CONNECTION) ||
@@ -4895,11 +4921,11 @@ static void hub_events(void)
 					if (status < 0)
 						hub_port_disable(hub, i, 1);
 				} else {
-					usb_lock_device(udev);
 					status = usb_reset_device(udev);
-					usb_unlock_device(udev);
 					connect_change = 0;
 				}
+				usb_unlock_device_state(udev);
+				usb_unlock_device(udev);
 			}
 
 			if (connect_change)
diff --git a/drivers/usb/core/hub.h b/drivers/usb/core/hub.h
index 2ba10798c943..3513548a0f06 100644
--- a/drivers/usb/core/hub.h
+++ b/drivers/usb/core/hub.h
@@ -90,6 +90,7 @@ struct usb_port_location {
  * @location: opaque representation of platform connector location
  * @portnum: port index num based one
  * @power_is_on: port's power state
+ * @resume_child: set at resume to sync khubd with child recovery
  * @did_runtime_put: port has done pm_runtime_put().
  */
 struct usb_port {
@@ -101,6 +102,7 @@ struct usb_port {
 	struct usb_port_location location;
 	u8 portnum;
 	unsigned power_is_on:1;
+	unsigned resume_child:1;
 	unsigned did_runtime_put:1;
 };
 
diff --git a/drivers/usb/core/port.c b/drivers/usb/core/port.c
index 97e4939fee1a..602653015980 100644
--- a/drivers/usb/core/port.c
+++ b/drivers/usb/core/port.c
@@ -102,6 +102,7 @@ static int usb_port_runtime_resume(struct device *dev)
 		if (retval < 0)
 			dev_dbg(&port_dev->dev, "can't get reconnection after setting port  power on, status %d\n",
 					retval);
+		port_dev->resume_child = 1;
 		retval = 0;
 	}
 
diff --git a/drivers/usb/core/usb.c b/drivers/usb/core/usb.c
index 4d1144990d4c..7e8e5591e852 100644
--- a/drivers/usb/core/usb.c
+++ b/drivers/usb/core/usb.c
@@ -426,6 +426,7 @@ struct usb_device *usb_alloc_dev(struct usb_device *parent,
 	}
 
 	device_initialize(&dev->dev);
+	mutex_init(&dev->state_lock);
 	dev->dev.bus = &usb_bus_type;
 	dev->dev.type = &usb_device_type;
 	dev->dev.groups = usb_device_groups;
diff --git a/include/linux/usb.h b/include/linux/usb.h
index 512ab162832c..d4e3b9b92085 100644
--- a/include/linux/usb.h
+++ b/include/linux/usb.h
@@ -445,6 +445,7 @@ struct usb3_lpm_parameters {
  * @route: tree topology hex string for use with xHCI
  * @state: device state: configured, not attached, etc.
  * @speed: device speed: high/full/low (or error)
+ * @state_lock: sync khubd vs reset and resume
  * @tt: Transaction Translator info; used with low/full speed dev, highspeed hub
  * @ttport: device port on that tt hub
  * @toggle: one bit for each endpoint, with ([0] = IN, [1] = OUT) endpoints
@@ -513,6 +514,7 @@ struct usb_device {
 	u32		route;
 	enum usb_device_state	state;
 	enum usb_device_speed	speed;
+	struct mutex	state_lock;
 
 	struct usb_tt	*tt;
 	int		ttport;
@@ -607,9 +609,37 @@ extern struct usb_device *usb_hub_find_child(struct usb_device *hdev,
 		if (!child) continue; else
 
 /* USB device locking */
-#define usb_lock_device(udev)		device_lock(&(udev)->dev)
-#define usb_unlock_device(udev)		device_unlock(&(udev)->dev)
-#define usb_trylock_device(udev)	device_trylock(&(udev)->dev)
+static inline void usb_lock_device(struct usb_device *udev)
+{
+	if (udev)
+		device_lock(&udev->dev);
+}
+
+static inline void usb_unlock_device(struct usb_device *udev)
+{
+	if (udev)
+		device_unlock(&udev->dev);
+}
+
+static inline int usb_trylock_device(struct usb_device *udev)
+{
+	if (udev)
+		return device_trylock(&udev->dev);
+	return 0;
+}
+
+static inline void usb_lock_device_state(struct usb_device *udev)
+{
+	if (udev)
+		mutex_lock(&udev->state_lock);
+}
+
+static inline void usb_unlock_device_state(struct usb_device *udev)
+{
+	if (udev)
+		mutex_unlock(&udev->state_lock);
+}
+
 extern int usb_lock_device_for_reset(struct usb_device *udev,
 				     const struct usb_interface *iface);
 



--
To unsubscribe from this list: send the line "unsubscribe linux-usb" in
the body of a message to majordomo@xxxxxxxxxxxxxxx
More majordomo info at  http://vger.kernel.org/majordomo-info.html




[Index of Archives]     [Linux Media]     [Linux Input]     [Linux Audio Users]     [Yosemite News]     [Linux Kernel]     [Linux SCSI]     [Old Linux USB Devel Archive]

  Powered by Linux