udlfb: fix semaphore value leak

I observed that the performance of the udl fb driver degrades over time.
On a freshly booted machine, it takes 6 seconds to do "ls -la /usr/bin";
after some time of use, the same operation takes 14 seconds.

The reason is that the value of "limit_sem" decays over time.

The udl driver uses a semaphore "limit_set" to specify how many free urbs
are there on dlfb->urbs.list. If the count is zero, the "down" operation
will sleep until some urbs are added to the freelist.

In order to avoid some hypothetical deadlock, the driver will not call
"up" immediately, but it will offload it to a workqueue. The problem is
that if we call "schedule_delayed_work" on the same work item multiple
times, the work item may only be executed once.

This is happening:
* some urb completes
* dlfb_urb_completion adds it to the free list
* dlfb_urb_completion calls schedule_delayed_work to schedule the function
  dlfb_release_urb_work to increase the semaphore count
* as the urb is on the free list, some other task grabs it and submits it
* the submitted urb completes, dlfb_urb_completion is called again
* dlfb_urb_completion calls schedule_delayed_work, but the work is already
  scheduled, so it does nothing
* finally, dlfb_release_urb_work is called, it increases the semaphore
  count by 1, although it should increase it by 2

So, the semaphore count is decreasing over time, and this causes gradual
performance degradation.

Note that in the current kernel, the "up" function may be called from
interrupt and it may race with the "down" function called by another
thread, so we don't have to offload the call of "up" to a workqueue at
all. This patch removes the workqueue code. The patch also changes
"down_interruptible" to "down" in dlfb_free_urb_list, so that we will
clean up the driver properly even if a signal arrives.

With this patch, the performance of udlfb no longer degrades.

Signed-off-by: Mikulas Patocka <mpatocka@redhat.com>
Cc: stable@vger.kernel.org
[b.zolnierkie: fix immediatelly -> immediately typo]
Signed-off-by: Bartlomiej Zolnierkiewicz <b.zolnierkie@samsung.com>
This commit is contained in:
Mikulas Patocka 2018-07-25 15:41:54 +02:00 committed by Bartlomiej Zolnierkiewicz
parent 8c5b044299
commit 9d0aa601e4
2 changed files with 2 additions and 26 deletions

View File

@ -922,14 +922,6 @@ static void dlfb_free(struct kref *kref)
kfree(dlfb); kfree(dlfb);
} }
static void dlfb_release_urb_work(struct work_struct *work)
{
struct urb_node *unode = container_of(work, struct urb_node,
release_urb_work.work);
up(&unode->dlfb->urbs.limit_sem);
}
static void dlfb_free_framebuffer(struct dlfb_data *dlfb) static void dlfb_free_framebuffer(struct dlfb_data *dlfb)
{ {
struct fb_info *info = dlfb->info; struct fb_info *info = dlfb->info;
@ -1789,14 +1781,7 @@ static void dlfb_urb_completion(struct urb *urb)
dlfb->urbs.available++; dlfb->urbs.available++;
spin_unlock_irqrestore(&dlfb->urbs.lock, flags); spin_unlock_irqrestore(&dlfb->urbs.lock, flags);
/* up(&dlfb->urbs.limit_sem);
* When using fb_defio, we deadlock if up() is called
* while another is waiting. So queue to another process.
*/
if (fb_defio)
schedule_delayed_work(&unode->release_urb_work, 0);
else
up(&dlfb->urbs.limit_sem);
} }
static void dlfb_free_urb_list(struct dlfb_data *dlfb) static void dlfb_free_urb_list(struct dlfb_data *dlfb)
@ -1805,16 +1790,11 @@ static void dlfb_free_urb_list(struct dlfb_data *dlfb)
struct list_head *node; struct list_head *node;
struct urb_node *unode; struct urb_node *unode;
struct urb *urb; struct urb *urb;
int ret;
unsigned long flags; unsigned long flags;
/* keep waiting and freeing, until we've got 'em all */ /* keep waiting and freeing, until we've got 'em all */
while (count--) { while (count--) {
down(&dlfb->urbs.limit_sem);
/* Getting interrupted means a leak, but ok at disconnect */
ret = down_interruptible(&dlfb->urbs.limit_sem);
if (ret)
break;
spin_lock_irqsave(&dlfb->urbs.lock, flags); spin_lock_irqsave(&dlfb->urbs.lock, flags);
@ -1854,9 +1834,6 @@ static int dlfb_alloc_urb_list(struct dlfb_data *dlfb, int count, size_t size)
break; break;
unode->dlfb = dlfb; unode->dlfb = dlfb;
INIT_DELAYED_WORK(&unode->release_urb_work,
dlfb_release_urb_work);
urb = usb_alloc_urb(0, GFP_KERNEL); urb = usb_alloc_urb(0, GFP_KERNEL);
if (!urb) { if (!urb) {
kfree(unode); kfree(unode);

View File

@ -20,7 +20,6 @@ struct dloarea {
struct urb_node { struct urb_node {
struct list_head entry; struct list_head entry;
struct dlfb_data *dlfb; struct dlfb_data *dlfb;
struct delayed_work release_urb_work;
struct urb *urb; struct urb *urb;
}; };