[Email can sometimes be a very frustrating medium. It's fair to say that you and I are both very intelligent people, and yet we've been talking past each other in this thread to an almost comical extent. I bet we could have settled the whole thing very simply in less than an hour or two of face-to-face contact.]
On Sat, 14 Aug 2004, David Brownell wrote: > > The > > problem you outlined still can't happen, though. khubd won't see the > > remote wakeup status until it can lock the hub sending the status report; > > so long as suspend() is using that hub, khubd won't interfere. > > But locktree() is all that would prevent khubd from interfering, > unless you're moving away from the "no-parent-locking" rule. > > Your "no parent-locking" rule (or the "lock only the immediate > parent" variant) would certainly let khubd wake up a hub before > its subtree finished suspending. > > Consider a task suspending from a root hub: > > SUSPEND( root{ a{v,w}, b{x,y}, c } ) > > When V issues a wakeup after A's been suspended, and > while B is being suspended, the no-locktree behavior is > to wake A up ... whoa, khubd just interfered with the task > that was suspending!! Likewise with other operations. I don't know what you mean by my "no-parent-locking rule". Maybe it stems from a misunderstanding of something I said earlier, leading to this current misconception? Regardless, in the situation you describe (and after replacing locktree() with down(serialize)) the suspend task would still own the root hub's lock, because it had not yet finished suspending all the children of the root. The remote wakeup request from V, relayed through A, is presented to khubd as the USB_PORT_STAT_C_SUSPEND bit in the root hub's port status. So khubd won't see the wakeup request until the suspend task is complete, because khubd won't read the root hub's port status until it can lock the root hub. There is a potentially more dangerous variant of this scenario. Suppose instead of V issuing a remote wakeup request that some task decides to resume A. Under the current implementation (and after replacing locktree() with down(serialize)) the suspend task would have released A's lock already, so the resume would proceed and would wake A up. In the algorithm I presented A remains locked until its parent (the root) is suspended; the resume would be forced to wait and then would fail because the clear_port_feature(USB_PORT_FEAT_SUSPEND) message to the root hub would be rejected. The basic message of this analysis is that I'm saying tasks like suspend should keep devices locked for as long as they want to prevent other tasks from accessing them. Your implementation relies on resume() using locktree() to prevent interference such as resuming a device after it is suspended and unlocked but before its parent is suspended. My algorithm relies on keeping the device locked until after its parent is suspended to prevent this; it seems more straightforward. After all, this is the same mechanism we rely on to prevent tasks from resuming devices below a suspended hub when usb_suspend_device() isn't running! > > Well, there is a slight difference in meaning. Debouncing, for instance, > > could qualify as non-data signalling but not as a subtree operation. > > Debouncing should be covered by such a lock, certainly; have > you got something against singleton degenerate trees? :) Not a bit. But during debounce processing for new connections or disconnections there _is_ no child device structure to lock. Of course, that could be changed... > You've not yet shown me a "... than necessary" case though; > your examples always allow tasks to interfere with each other. It only seems that way because you haven't understood them fully! > And I don't see complication at all, except when trying to get > potential performance optimizations by switching to fancier > algorithms. How is the "lock parent then child" policy at all > complicated? It's more complicated than "lock child" is. Imagine, for the sake of argument, that the __usb_suspend_device()/hub_suspend() code is reorganized so that children aren't unlocked until after their parent is suspended. (It wouldn't be such a bad idea to go ahead and actually make that change, regardless of anything else...) Now imagine that a year from now some programmer who hasn't read the email archives comes along and notices this strange locktree() routine sitting there in hub.c. He also notices that if every call to locktree(u) were replaced with usb_lock_device(u) then everything would work just as correctly as before. Who could blame him for making the replacements and removing locktree()? > > As for simple locking rules... Your rule seems to go like this: > > > > Every subtree operation starts by calling locktree() on its > > topmost node, then uses further locking as appropriate for > > the kind of subtree traversal it will perform. Proper > > Missing important adjective: "task" serialization. > > > serialization is guaranteed by the fact that _every_ subtree > > operation calls locktree() at the start and locks each device > > before touching it. > > > > The rule I'm proposing goes like this: > > > > Every subtree operation uses locking as appropriate for the kind > > of subtree traversal it will perform. Proper serialization is > > Analogous adjective: "device" serialization. > > > guaranteed by the fact that _every_ subtree operation locks each > > device before touching it. I don't understand the distinction you're making between "task" serialization and "device" serialization. > > Once you accept that my rule is correct, I think you have to agree that > > it's simpler than your rule. > > If it's correct, it's for a different problem though; that's why it's > simpler. It allows two tasks to interfere with each other, forcing > the use of non-deterministic and multipass algorithms to clean > up after such interference. (Rather than knowing that it can't > be un-done till later, so a single-pass algorithm will work.) _How_ does my approach allow two tasks to interfere with each other? Under what circumstances can such interference occur? I've already shown that the remote-wakeup case you mentioned avoids interference, as does resume-during-suspend. Can you find any other examples? The type of interference I've been concerned with is where a second task can alter a device's state when the first task doesn't want it to. This can't happen with my approach, because the first task will continue to hold the device's lock during the entire time it wants to prevent state alterations. Are you worried about some other sort of interference? > The way locktree() prevents them is by _consistently_ implementing the > parent-before-child locking hierarchy. If that policy is implemented > consistently, it's equivalent to locktree. Inconsistent locking > policy implementation ... let's not go there! My approach _never_ involves locking a parent while holding a child's lock, so at least it's opposed to parent-after-child. Is that enough to make my approach is equivalent to locktree? :-) More likely you meant "always lock a device's parent before locking the device". I don't see any point to this. In general, you lock a device to prevent other tasks from interfering while you use the device (or conversely, because you're going to change the device and you don't want to interfere with anyone else using it). But if you're going to suspend A and everything below A then you're not doing anything at all to A's parent, so why should you lock it? > > > - prefix (parent first then children) ... task can drop parent's lock > > > as soon as it has (a) visited the parent, (b) locked the children > > > > So we can add: (c) unlock the parent, (d) recurse on the children. > > Yes. Ideally one just appends to list end, and takes the next item > off its front. That's a classic algorithm, one I suspect the PM core suspend > code might be better off using. > > Now here's another problem. The fact is, subtree operations in general > > can't safely use your prefix-traversal locking pattern. You would run > > into problems in step (d), because once the parent is unlocked new > > children can be added. > > Which by definition is perfectly OK, otherwise you couldn't have > been safe using the postfix locking pattern ... :) This is a more complicated issue than your description would indicate. If the set of nodes in a subtree can change (because children are added) during a traversal, you have to specify very carefully which nodes you want to visit. The only thing that makes sense is to say that the traversal should visit all of a parent's children _as of the time the parent was visited_. That's good for both suspend and resume. Given such a requirement then yes, the prefix-type locking algorithm you described will work -- provided you take care to insure no harm will come from trying to visit a child that didn't exist at the time you visited the parent. > If unlock-early is incorrect, then prefix traversal would also be > incorrect ... likely it's a postfix algorithm, not a prefix one. > Otherwise there'd be no need to keep those other locks, > or care about things like siblings added after the algorithm > had "finished" with the parent node. Well, that's the thing. If there were an operation which (like resume) required visiting parents before children and also (_unlike_ resume) required visiting all the nodes in a subtree as of the time the operation completed -- even nodes that weren't present when their parent was visited -- then your unlock-early approach wouldn't work. Nevertheless, wouldn't you say that this operation required a prefix-order traversal (by definition: parents before children)? Fortunately there aren't any such operations we have to worry about. > > Simple locking can do this with no help from locktree. > > But locktree() doesn't hurt either, to get that first lock; > T2 just needs a multi-pass algorithm, and the locking pass > grabs a whole bunch of locks after locktree() returns. So you're not totally against the idea of multi-pass algorithms! :-) It sounded like you were, from some other things you said. > You're the one who suggested talking about "locking device > nodes" rather than saying exactly how it was done ... very > specifically to move away from saying it means down(serialize). > Have you changed your mind on that? No. When I wrote the preceding message I was speaking in the context of the current kernel code, which doesn't include usb_lock_device(). When that routine gets merged back in, I would rather say that locking device nodes means calling usb_lock_device() for the first lock a task acquires and doing down(serialize) for subsequent nested locks. At any rate, it definitely does not mean calling locktree() -- in fact it refers to the individual locking operations that comprise locktree(). > > > something like it is needed > > > to prevent two tasks from interfering with each other, just > > > grabbing locks from the middle of the tree can NEVER provide > > > task serialization. > > > > You keep saying that, and I don't understand why. > > Again, see the example at the top of this message. I've shown that the example does not give rise to interference. As for task serialization... I can't speak to that until I know what you mean by it. > Consider when more than two tasks are active too; > when tasks touch a node, they should always do it in > the same order. I agree this is generally desireable, although it's not always necessary. For instance, if usb_disconnect() and usb_suspend_device() were working on overlapping sets of nodes, it wouldn't really matter if the orders didn't always match. Anyway, all of the algorithms I've proposed do have this property. (Your prefix-order locking optimization does _not_.) > > Since suspend() and > > resume() are the two most problematic subtree operations, here are some > > explicit algorithms to provide a counterexample to your statement: > > Ones that don't use the simple down() scheme you've been > talking about, note -- they added a "must own parent lock" > rule. Except (!!) when getting the first lock; ergo, you've got > those "tasks interfere with each other" problems, which comes > from thosee exceptions-to-rule. :) Hang on there! They _do_ use the simple down() scheme -- at least, they don't call locktree() anywhere! As for "must own parent lock" rules: (a) The algorithms have no such rule; it's not clear why you say they do. And quite apart from any transparent internal aspects of the implementation, there's no such requirement placed on any code calling suspend() or resume(). (b) What's wrong with requiring callers to hold a parent's lock? usb_new_device() does so. (c) After some more thought, I decided it would be better to require the caller to own the lock on the topmost device being suspended or resumed. Doing this would permit the subroutine in my example to become the main (the only!) routine for suspend. It also permits tasks to suspend or resume devices that are already locked, which is a Good Thing (suppose a driver wants to suspend a device during probe?). In fact, I suggest making such a change to the existing code. It would bring usb_suspend_device() and usb_resume_device() in line with other routines like usb_set_configuration() and usb_reset_device(). (Whoops -- usb_reset_device() doesn't require this in the current code. Well, it will once my patches get merged back in.) Finally, I don't see what you mean about having "tasks interfere with each other" problems. I'm pretty confident that you can't find any examples of situations where another task can interfere with my suspend or resume algorithms. > So long as there's a device node in hdev->children[], the prefix algorithm > lets that node's lock be used instead of holding the hub's lock. The node > could be stored there earlier than we do now by someone willing to spend > time on a performance patch. Yes, that would work. It would be a little awkward, because hub_port_connect_change() sometimes deletes the child device and creates another one, but it could be done. It would cause synchronization problems between khubd and the hub disconnect() routine, however. I _like_ the idea of making sure that a driver doesn't try to access its device after disconnect() returns. If hdev->serialize can't be used for that purpose then something else would have to be introduced, like that old private khubd semaphore. Alan Stern P.S.: Ironically, even though I object to locktree in part because of the unnecessary delays it can cause, I wouldn't mind so much getting rid of usbdev->serialize entirely and just using a single semaphore to lock an entire bus. Although it would be even less efficient in terms of concurrent operation, it would be much simpler, more elegant, and quite obviously correct. And in fact, so long as khubd remains single-threaded the loss in concurrency wouldn't even be all that high. Probably a number of people won't think this is a good idea, though. ------------------------------------------------------- SF.Net email is sponsored by Shop4tech.com-Lowest price on Blank Media 100pk Sonic DVD-R 4x for only $29 -100pk Sonic DVD+R for only $33 Save 50% off Retail on Ink & Toner - Free Shipping and Free Gift. http://www.shop4tech.com/z/Inkjet_Cartridges/9_108_r285 _______________________________________________ [EMAIL PROTECTED] To unsubscribe, use the last form field at: https://lists.sourceforge.net/lists/listinfo/linux-usb-devel