2021-07-08 08:45:19

by Ikjoon Jang

[permalink] [raw]
Subject: [PATCH] xhci: fix unmatched num_trbs_free

When unlinked urbs are queued to the cancelled td list, many tds
might be located after hw dequeue pointer and just marked as no-op
but not reclaimed to num_trbs_free. This bias can leads to unnecessary
ring expansions and leaks in atomic pool.

To prevent this bias, this patch counts free TRBs every time xhci moves
dequeue pointer. This patch utilizes existing
update_ring_for_set_deq_completion() function, renamed it to move_deq().

When it walks through to the new dequeue pointer, it also counts
free TRBs manually. This patch adds a fast path for the most cases
where the new dequeue pointer is still in the current segment.

Signed-off-by: Ikjoon Jang <[email protected]>
---

drivers/usb/host/xhci-ring.c | 106 +++++++++++++++++------------------
1 file changed, 52 insertions(+), 54 deletions(-)

diff --git a/drivers/usb/host/xhci-ring.c b/drivers/usb/host/xhci-ring.c
index 3c12a6fc406b..6414ffe33581 100644
--- a/drivers/usb/host/xhci-ring.c
+++ b/drivers/usb/host/xhci-ring.c
@@ -152,6 +152,54 @@ static void next_trb(struct xhci_hcd *xhci,
}
}

+/* Forward dequeue pointer to the specific position,
+ * walk through the ring and reclaim free trb slots to num_trbs_free
+ */
+static int move_deq(struct xhci_hcd *xhci, struct xhci_ring *ep_ring,
+ struct xhci_segment *new_seg, union xhci_trb *new_deq)
+{
+ unsigned int steps;
+ union xhci_trb *deq;
+ struct xhci_segment *seg = ep_ring->deq_seg;
+
+ /* direct paths */
+ if (ep_ring->dequeue == new_deq) {
+ return 0;
+ } else if ((ep_ring->deq_seg == new_seg) &&
+ (ep_ring->dequeue <= new_deq)) {
+ steps = new_deq - ep_ring->dequeue;
+ deq = new_deq;
+ goto found;
+ }
+
+ /* fast walk to the next segment */
+ seg = seg->next;
+ steps = (TRBS_PER_SEGMENT - 1) -
+ (ep_ring->dequeue - ep_ring->deq_seg->trbs);
+ deq = &seg->trbs[0];
+
+ while (deq != new_deq) {
+ if (trb_is_link(deq)) {
+ seg = seg->next;
+ deq = seg->trbs;
+ } else {
+ steps++;
+ deq++;
+ }
+ if (deq == ep_ring->dequeue) {
+ xhci_warn(xhci, "Unable to find new dequeue pointer\n");
+ return -ENOENT;
+ }
+ }
+
+found:
+ ep_ring->deq_seg = seg;
+ ep_ring->dequeue = deq;
+ ep_ring->num_trbs_free += steps;
+
+ return 0;
+}
+
/*
* See Cycle bit rules. SW is the consumer for the event ring only.
*/
@@ -1245,52 +1293,6 @@ void xhci_stop_endpoint_command_watchdog(struct timer_list *t)
"xHCI host controller is dead.");
}

-static void update_ring_for_set_deq_completion(struct xhci_hcd *xhci,
- struct xhci_virt_device *dev,
- struct xhci_ring *ep_ring,
- unsigned int ep_index)
-{
- union xhci_trb *dequeue_temp;
- int num_trbs_free_temp;
- bool revert = false;
-
- num_trbs_free_temp = ep_ring->num_trbs_free;
- dequeue_temp = ep_ring->dequeue;
-
- /* If we get two back-to-back stalls, and the first stalled transfer
- * ends just before a link TRB, the dequeue pointer will be left on
- * the link TRB by the code in the while loop. So we have to update
- * the dequeue pointer one segment further, or we'll jump off
- * the segment into la-la-land.
- */
- if (trb_is_link(ep_ring->dequeue)) {
- ep_ring->deq_seg = ep_ring->deq_seg->next;
- ep_ring->dequeue = ep_ring->deq_seg->trbs;
- }
-
- while (ep_ring->dequeue != dev->eps[ep_index].queued_deq_ptr) {
- /* We have more usable TRBs */
- ep_ring->num_trbs_free++;
- ep_ring->dequeue++;
- if (trb_is_link(ep_ring->dequeue)) {
- if (ep_ring->dequeue ==
- dev->eps[ep_index].queued_deq_ptr)
- break;
- ep_ring->deq_seg = ep_ring->deq_seg->next;
- ep_ring->dequeue = ep_ring->deq_seg->trbs;
- }
- if (ep_ring->dequeue == dequeue_temp) {
- revert = true;
- break;
- }
- }
-
- if (revert) {
- xhci_dbg(xhci, "Unable to find new dequeue pointer\n");
- ep_ring->num_trbs_free = num_trbs_free_temp;
- }
-}
-
/*
* When we get a completion for a Set Transfer Ring Dequeue Pointer command,
* we need to clear the set deq pending flag in the endpoint ring state, so that
@@ -1377,8 +1379,8 @@ static void xhci_handle_cmd_set_deq(struct xhci_hcd *xhci, int slot_id,
/* Update the ring's dequeue segment and dequeue pointer
* to reflect the new position.
*/
- update_ring_for_set_deq_completion(xhci, ep->vdev,
- ep_ring, ep_index);
+ move_deq(xhci, ep_ring, ep->queued_deq_seg,
+ ep->queued_deq_ptr);
} else {
xhci_warn(xhci, "Mismatch between completed Set TR Deq Ptr command & xHCI internal state.\n");
xhci_warn(xhci, "ep deq seg = %p, deq ptr = %p\n",
@@ -2212,9 +2214,7 @@ static int finish_td(struct xhci_hcd *xhci, struct xhci_virt_ep *ep,
}

/* Update ring dequeue pointer */
- ep_ring->dequeue = td->last_trb;
- ep_ring->deq_seg = td->last_trb_seg;
- ep_ring->num_trbs_free += td->num_trbs - 1;
+ move_deq(xhci, ep_ring, td->last_trb_seg, td->last_trb);
inc_deq(xhci, ep_ring);

return xhci_td_cleanup(xhci, td, ep_ring, td->status);
@@ -2434,9 +2434,7 @@ static int skip_isoc_td(struct xhci_hcd *xhci, struct xhci_td *td,
frame->actual_length = 0;

/* Update ring dequeue pointer */
- ep->ring->dequeue = td->last_trb;
- ep->ring->deq_seg = td->last_trb_seg;
- ep->ring->num_trbs_free += td->num_trbs - 1;
+ move_deq(xhci, ep->ring, td->last_trb_seg, td->last_trb);
inc_deq(xhci, ep->ring);

return xhci_td_cleanup(xhci, td, ep->ring, status);
--
2.32.0.93.g670b81a890-goog


2021-07-16 12:56:46

by Mathias Nyman

[permalink] [raw]
Subject: Re: [PATCH] xhci: fix unmatched num_trbs_free

On 8.7.2021 11.43, Ikjoon Jang wrote:
> When unlinked urbs are queued to the cancelled td list, many tds
> might be located after hw dequeue pointer and just marked as no-op
> but not reclaimed to num_trbs_free. This bias can leads to unnecessary
> ring expansions and leaks in atomic pool.

Good point, in that case trbs turned no-op never get added to free trb count.

>
> To prevent this bias, this patch counts free TRBs every time xhci moves
> dequeue pointer. This patch utilizes existing
> update_ring_for_set_deq_completion() function, renamed it to move_deq().
>
> When it walks through to the new dequeue pointer, it also counts
> free TRBs manually. This patch adds a fast path for the most cases
> where the new dequeue pointer is still in the current segment.
>

This looks like an option.

Another approach would be to keep the normal case fast, and the special case code simple.
Something like:

finish_td()
...
/* Update ring dequeue pointer */
if (ep_ring->dequeue == td->first_trb) {
ep_ring->dequeue = td->last_trb;
ep_ring->deq_seg = td->last_trb_seg;
ep_ring->num_trbs_free += td->num_trbs - 1;
inc_deq(xhci, ep_ring);
} else {
move_deq(...);
}

move_deq(...)
{
while(ring->dequeue != new_dequeue)
inc_deq(ring);
inc_deq(ring);
}

inc_deq() increases the num_trbs_free count.

I haven't looked at the details of this yet, but I'm away for the next two weeks so
I wanted to share this first anyway.

-Mathias

2021-07-19 04:37:58

by Ikjoon Jang

[permalink] [raw]
Subject: Re: [PATCH] xhci: fix unmatched num_trbs_free

Hi Mathias,

On Fri, Jul 16, 2021 at 8:54 PM Mathias Nyman
<[email protected]> wrote:
>
> On 8.7.2021 11.43, Ikjoon Jang wrote:
> > When unlinked urbs are queued to the cancelled td list, many tds
> > might be located after hw dequeue pointer and just marked as no-op
> > but not reclaimed to num_trbs_free. This bias can leads to unnecessary
> > ring expansions and leaks in atomic pool.
>
> Good point, in that case trbs turned no-op never get added to free trb count.
>
> >
> > To prevent this bias, this patch counts free TRBs every time xhci moves
> > dequeue pointer. This patch utilizes existing
> > update_ring_for_set_deq_completion() function, renamed it to move_deq().
> >
> > When it walks through to the new dequeue pointer, it also counts
> > free TRBs manually. This patch adds a fast path for the most cases
> > where the new dequeue pointer is still in the current segment.
> >
>
> This looks like an option.
>
> Another approach would be to keep the normal case fast, and the special case code simple.
> Something like:
>
> finish_td()
> ...
> /* Update ring dequeue pointer */
> if (ep_ring->dequeue == td->first_trb) {
> ep_ring->dequeue = td->last_trb;
> ep_ring->deq_seg = td->last_trb_seg;
> ep_ring->num_trbs_free += td->num_trbs - 1;
> inc_deq(xhci, ep_ring);
> } else {
> move_deq(...);
> }
>
> move_deq(...)
> {
> while(ring->dequeue != new_dequeue)
> inc_deq(ring);
> inc_deq(ring);
> }

Yes, I think most cases would be in (ep_ring->dequeue == td->first_trb)
so I think just repeating inc_deq() will be okay like the above example
cancelling urbs is an expensive and unusual operation.

But as you can see, I changed update_ring_for_set_deq_completion() to
move_deq(),
Do you think it's okay for that substitution In xhci_handle_cmd_set_deq()?
I'm worrying about some weird situation where the new dequeue ptr is
not in the ring.

>
> inc_deq() increases the num_trbs_free count.
>
> I haven't looked at the details of this yet, but I'm away for the next two weeks so
> I wanted to share this first anyway.
>

Thanks for reviewing, I hope to get some feedback when you come back.

> -Mathias

On Fri, Jul 16, 2021 at 8:54 PM Mathias Nyman
<[email protected]> wrote:
>
> On 8.7.2021 11.43, Ikjoon Jang wrote:
> > When unlinked urbs are queued to the cancelled td list, many tds
> > might be located after hw dequeue pointer and just marked as no-op
> > but not reclaimed to num_trbs_free. This bias can leads to unnecessary
> > ring expansions and leaks in atomic pool.
>
> Good point, in that case trbs turned no-op never get added to free trb count.
>
> >
> > To prevent this bias, this patch counts free TRBs every time xhci moves
> > dequeue pointer. This patch utilizes existing
> > update_ring_for_set_deq_completion() function, renamed it to move_deq().
> >
> > When it walks through to the new dequeue pointer, it also counts
> > free TRBs manually. This patch adds a fast path for the most cases
> > where the new dequeue pointer is still in the current segment.
> >
>
> This looks like an option.
>
> Another approach would be to keep the normal case fast, and the special case code simple.
> Something like:
>
> finish_td()
> ...
> /* Update ring dequeue pointer */
> if (ep_ring->dequeue == td->first_trb) {
> ep_ring->dequeue = td->last_trb;
> ep_ring->deq_seg = td->last_trb_seg;
> ep_ring->num_trbs_free += td->num_trbs - 1;
> inc_deq(xhci, ep_ring);
> } else {
> move_deq(...);
> }
>
> move_deq(...)
> {
> while(ring->dequeue != new_dequeue)
> inc_deq(ring);
> inc_deq(ring);
> }
>
> inc_deq() increases the num_trbs_free count.
>
> I haven't looked at the details of this yet, but I'm away for the next two weeks so
> I wanted to share this first anyway.
>
> -Mathias