/* * * (C) COPYRIGHT 2012-2015 ARM Limited. All rights reserved. * * This program is free software and is provided to you under the terms of the * GNU General Public License version 2 as published by the Free Software * Foundation, and any use by you of this program is subject to the terms * of such GNU licence. * * A copy of the licence is included with the program, and can also be obtained * from Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. * */ #include #include #include #include #include #include #include #include #include #include #include #define KDS_LINK_TRIGGERED (1u << 0) #define KDS_LINK_EXCLUSIVE (1u << 1) #define KDS_INVALID (void *)-2 #define KDS_RESOURCE (void *)-1 struct kds_resource_set { unsigned long num_resources; unsigned long pending; struct kds_callback *cb; void *callback_parameter; void *callback_extra_parameter; struct list_head callback_link; struct work_struct callback_work; atomic_t cb_queued; /* This resource set will be freed when there are no pending * callbacks */ struct kref refcount; /* This is only initted when kds_waitall() is called. */ wait_queue_head_t wake; struct kds_link resources[0]; }; static DEFINE_SPINLOCK(kds_lock); static void __resource_set_release(struct kref *ref) { struct kds_resource_set *rset = container_of(ref, struct kds_resource_set, refcount); kfree(rset); } int kds_callback_init(struct kds_callback *cb, int direct, kds_callback_fn user_cb) { int ret = 0; cb->direct = direct; cb->user_cb = user_cb; if (!direct) { cb->wq = alloc_workqueue("kds", WQ_UNBOUND | WQ_HIGHPRI, WQ_UNBOUND_MAX_ACTIVE); if (!cb->wq) ret = -ENOMEM; } else { cb->wq = NULL; } return ret; } EXPORT_SYMBOL(kds_callback_init); void kds_callback_term(struct kds_callback *cb) { if (!cb->direct) { BUG_ON(!cb->wq); destroy_workqueue(cb->wq); } else { BUG_ON(cb->wq); } } EXPORT_SYMBOL(kds_callback_term); static void kds_do_user_callback(struct kds_resource_set *rset) { rset->cb->user_cb(rset->callback_parameter, rset->callback_extra_parameter); } static void kds_queued_callback(struct work_struct *work) { struct kds_resource_set *rset; rset = container_of(work, struct kds_resource_set, callback_work); atomic_dec(&rset->cb_queued); kds_do_user_callback(rset); } static void kds_callback_perform(struct kds_resource_set *rset) { if (rset->cb->direct) kds_do_user_callback(rset); else { int result; atomic_inc(&rset->cb_queued); result = queue_work(rset->cb->wq, &rset->callback_work); /* if we got a 0 return it means we've triggered the same rset twice! */ WARN_ON(!result); } } void kds_resource_init(struct kds_resource * const res) { BUG_ON(!res); INIT_LIST_HEAD(&res->waiters.link); res->waiters.parent = KDS_RESOURCE; } EXPORT_SYMBOL(kds_resource_init); int kds_resource_term(struct kds_resource *res) { unsigned long lflags; BUG_ON(!res); spin_lock_irqsave(&kds_lock, lflags); if (!list_empty(&res->waiters.link)) { spin_unlock_irqrestore(&kds_lock, lflags); printk(KERN_ERR "ERROR: KDS resource is still in use\n"); return -EBUSY; } res->waiters.parent = KDS_INVALID; spin_unlock_irqrestore(&kds_lock, lflags); return 0; } EXPORT_SYMBOL(kds_resource_term); int kds_async_waitall( struct kds_resource_set ** const pprset, struct kds_callback *cb, void *callback_parameter, void *callback_extra_parameter, int number_resources, unsigned long *exclusive_access_bitmap, struct kds_resource **resource_list) { struct kds_resource_set *rset = NULL; unsigned long lflags; int i; int triggered; int err = -EFAULT; BUG_ON(!pprset); BUG_ON(!resource_list); BUG_ON(!cb); WARN_ONCE(number_resources > 10, "Waiting on a high numbers of resources may increase latency, see documentation."); rset = kmalloc(sizeof(*rset) + number_resources * sizeof(struct kds_link), GFP_KERNEL); if (!rset) { return -ENOMEM; } rset->num_resources = number_resources; rset->pending = number_resources; rset->cb = cb; rset->callback_parameter = callback_parameter; rset->callback_extra_parameter = callback_extra_parameter; INIT_LIST_HEAD(&rset->callback_link); INIT_WORK(&rset->callback_work, kds_queued_callback); atomic_set(&rset->cb_queued, 0); kref_init(&rset->refcount); for (i = 0; i < number_resources; i++) { INIT_LIST_HEAD(&rset->resources[i].link); rset->resources[i].parent = rset; } spin_lock_irqsave(&kds_lock, lflags); for (i = 0; i < number_resources; i++) { unsigned long link_state = 0; if (test_bit(i, exclusive_access_bitmap)) { link_state |= KDS_LINK_EXCLUSIVE; } /* no-one else waiting? */ if (list_empty(&resource_list[i]->waiters.link)) { link_state |= KDS_LINK_TRIGGERED; rset->pending--; } /* Adding a non-exclusive and the current tail is a triggered non-exclusive? */ else if (((link_state & KDS_LINK_EXCLUSIVE) == 0) && (((list_entry(resource_list[i]->waiters.link.prev, struct kds_link, link)->state & (KDS_LINK_EXCLUSIVE | KDS_LINK_TRIGGERED)) == KDS_LINK_TRIGGERED))) { link_state |= KDS_LINK_TRIGGERED; rset->pending--; } rset->resources[i].state = link_state; /* avoid double wait (hang) */ if (!list_empty(&resource_list[i]->waiters.link)) { /* adding same rset again? */ if (list_entry(resource_list[i]->waiters.link.prev, struct kds_link, link)->parent == rset) { goto roll_back; } } list_add_tail(&rset->resources[i].link, &resource_list[i]->waiters.link); } triggered = (rset->pending == 0); /* set the pointer before the callback is called so it sees it */ *pprset = rset; spin_unlock_irqrestore(&kds_lock, lflags); if (triggered) { /* all resources obtained, trigger callback */ kds_callback_perform(rset); } return 0; roll_back: /* roll back */ while (i-- > 0) { list_del(&rset->resources[i].link); } err = -EINVAL; spin_unlock_irqrestore(&kds_lock, lflags); kfree(rset); return err; } EXPORT_SYMBOL(kds_async_waitall); static void wake_up_sync_call(void *callback_parameter, void *callback_extra_parameter) { wait_queue_head_t *wait = (wait_queue_head_t *)callback_parameter; wake_up(wait); } static struct kds_callback sync_cb = { wake_up_sync_call, 1, NULL, }; struct kds_resource_set *kds_waitall( int number_resources, unsigned long *exclusive_access_bitmap, struct kds_resource **resource_list, unsigned long jiffies_timeout) { struct kds_resource_set *rset; unsigned long lflags; int i; int triggered = 0; DECLARE_WAIT_QUEUE_HEAD_ONSTACK(wake); rset = kmalloc(sizeof(*rset) + number_resources * sizeof(struct kds_link), GFP_KERNEL); if (!rset) return rset; rset->num_resources = number_resources; rset->pending = number_resources; init_waitqueue_head(&rset->wake); INIT_LIST_HEAD(&rset->callback_link); INIT_WORK(&rset->callback_work, kds_queued_callback); atomic_set(&rset->cb_queued, 0); kref_init(&rset->refcount); spin_lock_irqsave(&kds_lock, lflags); for (i = 0; i < number_resources; i++) { unsigned long link_state = 0; if (test_bit(i, exclusive_access_bitmap)) { link_state |= KDS_LINK_EXCLUSIVE; } if (list_empty(&resource_list[i]->waiters.link)) { link_state |= KDS_LINK_TRIGGERED; rset->pending--; } /* Adding a non-exclusive and the current tail is a triggered non-exclusive? */ else if (((link_state & KDS_LINK_EXCLUSIVE) == 0) && (((list_entry(resource_list[i]->waiters.link.prev, struct kds_link, link)->state & (KDS_LINK_EXCLUSIVE | KDS_LINK_TRIGGERED)) == KDS_LINK_TRIGGERED))) { link_state |= KDS_LINK_TRIGGERED; rset->pending--; } INIT_LIST_HEAD(&rset->resources[i].link); rset->resources[i].parent = rset; rset->resources[i].state = link_state; /* avoid double wait (hang) */ if (!list_empty(&resource_list[i]->waiters.link)) { /* adding same rset again? */ if (list_entry(resource_list[i]->waiters.link.prev, struct kds_link, link)->parent == rset) { goto roll_back; } } list_add_tail(&rset->resources[i].link, &resource_list[i]->waiters.link); } if (rset->pending == 0) triggered = 1; else { rset->cb = &sync_cb; rset->callback_parameter = &rset->wake; rset->callback_extra_parameter = NULL; } spin_unlock_irqrestore(&kds_lock, lflags); if (!triggered) { long wait_res = 0; long timeout = (jiffies_timeout == KDS_WAIT_BLOCKING) ? MAX_SCHEDULE_TIMEOUT : jiffies_timeout; if (timeout) { wait_res = wait_event_interruptible_timeout(rset->wake, rset->pending == 0, timeout); } if ((wait_res == -ERESTARTSYS) || (wait_res == 0)) { /* use \a kds_resource_set_release to roll back */ kds_resource_set_release(&rset); return ERR_PTR(wait_res); } } return rset; roll_back: /* roll back */ while (i-- > 0) { list_del(&rset->resources[i].link); } spin_unlock_irqrestore(&kds_lock, lflags); kfree(rset); return ERR_PTR(-EINVAL); } EXPORT_SYMBOL(kds_waitall); static void trigger_new_rset_owner(struct kds_resource_set *rset, struct list_head *triggered) { if (0 == --rset->pending) { /* new owner now triggered, track for callback later */ kref_get(&rset->refcount); list_add(&rset->callback_link, triggered); } } static void __kds_resource_set_release_common(struct kds_resource_set *rset) { struct list_head triggered = LIST_HEAD_INIT(triggered); struct kds_resource_set *it; unsigned long lflags; int i; spin_lock_irqsave(&kds_lock, lflags); for (i = 0; i < rset->num_resources; i++) { struct kds_resource *resource; struct kds_link *it = NULL; /* fetch the previous entry on the linked list */ it = list_entry(rset->resources[i].link.prev, struct kds_link, link); /* unlink ourself */ list_del(&rset->resources[i].link); /* any waiters? */ if (list_empty(&it->link)) continue; /* were we the head of the list? (head if prev is a resource) */ if (it->parent != KDS_RESOURCE) { if ((it->state & KDS_LINK_TRIGGERED) && !(it->state & KDS_LINK_EXCLUSIVE)) { /* * previous was triggered and not exclusive, so we * trigger non-exclusive until end-of-list or first * exclusive */ struct kds_link *it_waiting = it; list_for_each_entry(it, &it_waiting->link, link) { /* exclusive found, stop triggering */ if (it->state & KDS_LINK_EXCLUSIVE) break; it->state |= KDS_LINK_TRIGGERED; /* a parent to update? */ if (it->parent != KDS_RESOURCE) trigger_new_rset_owner( it->parent, &triggered); } } continue; } /* we were the head, find the kds_resource */ resource = container_of(it, struct kds_resource, waiters); /* we know there is someone waiting from the any-waiters test above */ /* find the head of the waiting list */ it = list_first_entry(&resource->waiters.link, struct kds_link, link); /* new exclusive owner? */ if (it->state & KDS_LINK_EXCLUSIVE) { /* link now triggered */ it->state |= KDS_LINK_TRIGGERED; /* a parent to update? */ trigger_new_rset_owner(it->parent, &triggered); } /* exclusive releasing ? */ else if (rset->resources[i].state & KDS_LINK_EXCLUSIVE) { /* trigger non-exclusive until end-of-list or first exclusive */ list_for_each_entry(it, &resource->waiters.link, link) { /* exclusive found, stop triggering */ if (it->state & KDS_LINK_EXCLUSIVE) break; it->state |= KDS_LINK_TRIGGERED; /* a parent to update? */ trigger_new_rset_owner(it->parent, &triggered); } } } spin_unlock_irqrestore(&kds_lock, lflags); while (!list_empty(&triggered)) { it = list_first_entry(&triggered, struct kds_resource_set, callback_link); list_del(&it->callback_link); kds_callback_perform(it); /* Free the resource set if no callbacks pending */ kref_put(&it->refcount, &__resource_set_release); } } void kds_resource_set_release(struct kds_resource_set **pprset) { struct kds_resource_set *rset; int queued; rset = cmpxchg(pprset, *pprset, NULL); if (!rset) { /* caught a race between a cancelation * and a completion, nothing to do */ return; } __kds_resource_set_release_common(rset); /* * Caller is responsible for guaranteeing that callback work is not * pending (i.e. its running or completed) prior to calling release. */ queued = atomic_read(&rset->cb_queued); BUG_ON(queued); kref_put(&rset->refcount, &__resource_set_release); } EXPORT_SYMBOL(kds_resource_set_release); void kds_resource_set_release_sync(struct kds_resource_set **pprset) { struct kds_resource_set *rset; rset = cmpxchg(pprset, *pprset, NULL); if (!rset) { /* caught a race between a cancelation * and a completion, nothing to do */ return; } __kds_resource_set_release_common(rset); /* * In the case of a kds async wait cancellation ensure the deferred * call back does not get scheduled if a trigger fired at the same time * to release the wait. */ cancel_work_sync(&rset->callback_work); kref_put(&rset->refcount, &__resource_set_release); } EXPORT_SYMBOL(kds_resource_set_release_sync); MODULE_LICENSE("GPL"); MODULE_AUTHOR("ARM Ltd."); MODULE_VERSION("1.0");