diff options
Diffstat (limited to 'drivers/base/devres.c')
| -rw-r--r-- | drivers/base/devres.c | 644 | 
1 files changed, 644 insertions, 0 deletions
diff --git a/drivers/base/devres.c b/drivers/base/devres.c new file mode 100644 index 00000000000..e177c9533b6 --- /dev/null +++ b/drivers/base/devres.c @@ -0,0 +1,644 @@ +/* + * drivers/base/devres.c - device resource management + * + * Copyright (c) 2006  SUSE Linux Products GmbH + * Copyright (c) 2006  Tejun Heo <teheo@suse.de> + * + * This file is released under the GPLv2. + */ + +#include <linux/device.h> +#include <linux/module.h> + +struct devres_node { +	struct list_head		entry; +	dr_release_t			release; +#ifdef CONFIG_DEBUG_DEVRES +	const char			*name; +	size_t				size; +#endif +}; + +struct devres { +	struct devres_node		node; +	/* -- 3 pointers */ +	unsigned long long		data[];	/* guarantee ull alignment */ +}; + +struct devres_group { +	struct devres_node		node[2]; +	void				*id; +	int				color; +	/* -- 8 pointers */ +}; + +#ifdef CONFIG_DEBUG_DEVRES +static int log_devres = 0; +module_param_named(log, log_devres, int, S_IRUGO | S_IWUSR); + +static void set_node_dbginfo(struct devres_node *node, const char *name, +			     size_t size) +{ +	node->name = name; +	node->size = size; +} + +static void devres_log(struct device *dev, struct devres_node *node, +		       const char *op) +{ +	if (unlikely(log_devres)) +		dev_printk(KERN_ERR, dev, "DEVRES %3s %p %s (%lu bytes)\n", +			   op, node, node->name, (unsigned long)node->size); +} +#else /* CONFIG_DEBUG_DEVRES */ +#define set_node_dbginfo(node, n, s)	do {} while (0) +#define devres_log(dev, node, op)	do {} while (0) +#endif /* CONFIG_DEBUG_DEVRES */ + +/* + * Release functions for devres group.  These callbacks are used only + * for identification. + */ +static void group_open_release(struct device *dev, void *res) +{ +	/* noop */ +} + +static void group_close_release(struct device *dev, void *res) +{ +	/* noop */ +} + +static struct devres_group * node_to_group(struct devres_node *node) +{ +	if (node->release == &group_open_release) +		return container_of(node, struct devres_group, node[0]); +	if (node->release == &group_close_release) +		return container_of(node, struct devres_group, node[1]); +	return NULL; +} + +static __always_inline struct devres * alloc_dr(dr_release_t release, +						size_t size, gfp_t gfp) +{ +	size_t tot_size = sizeof(struct devres) + size; +	struct devres *dr; + +	dr = kmalloc_track_caller(tot_size, gfp); +	if (unlikely(!dr)) +		return NULL; + +	memset(dr, 0, tot_size); +	INIT_LIST_HEAD(&dr->node.entry); +	dr->node.release = release; +	return dr; +} + +static void add_dr(struct device *dev, struct devres_node *node) +{ +	devres_log(dev, node, "ADD"); +	BUG_ON(!list_empty(&node->entry)); +	list_add_tail(&node->entry, &dev->devres_head); +} + +/** + * devres_alloc - Allocate device resource data + * @release: Release function devres will be associated with + * @size: Allocation size + * @gfp: Allocation flags + * + * allocate devres of @size bytes.  The allocated area is zeroed, then + * associated with @release.  The returned pointer can be passed to + * other devres_*() functions. + * + * RETURNS: + * Pointer to allocated devres on success, NULL on failure. + */ +#ifdef CONFIG_DEBUG_DEVRES +void * __devres_alloc(dr_release_t release, size_t size, gfp_t gfp, +		      const char *name) +{ +	struct devres *dr; + +	dr = alloc_dr(release, size, gfp); +	if (unlikely(!dr)) +		return NULL; +	set_node_dbginfo(&dr->node, name, size); +	return dr->data; +} +EXPORT_SYMBOL_GPL(__devres_alloc); +#else +void * devres_alloc(dr_release_t release, size_t size, gfp_t gfp) +{ +	struct devres *dr; + +	dr = alloc_dr(release, size, gfp); +	if (unlikely(!dr)) +		return NULL; +	return dr->data; +} +EXPORT_SYMBOL_GPL(devres_alloc); +#endif + +/** + * devres_free - Free device resource data + * @res: Pointer to devres data to free + * + * Free devres created with devres_alloc(). + */ +void devres_free(void *res) +{ +	if (res) { +		struct devres *dr = container_of(res, struct devres, data); + +		BUG_ON(!list_empty(&dr->node.entry)); +		kfree(dr); +	} +} +EXPORT_SYMBOL_GPL(devres_free); + +/** + * devres_add - Register device resource + * @dev: Device to add resource to + * @res: Resource to register + * + * Register devres @res to @dev.  @res should have been allocated + * using devres_alloc().  On driver detach, the associated release + * function will be invoked and devres will be freed automatically. + */ +void devres_add(struct device *dev, void *res) +{ +	struct devres *dr = container_of(res, struct devres, data); +	unsigned long flags; + +	spin_lock_irqsave(&dev->devres_lock, flags); +	add_dr(dev, &dr->node); +	spin_unlock_irqrestore(&dev->devres_lock, flags); +} +EXPORT_SYMBOL_GPL(devres_add); + +static struct devres *find_dr(struct device *dev, dr_release_t release, +			      dr_match_t match, void *match_data) +{ +	struct devres_node *node; + +	list_for_each_entry_reverse(node, &dev->devres_head, entry) { +		struct devres *dr = container_of(node, struct devres, node); + +		if (node->release != release) +			continue; +		if (match && !match(dev, dr->data, match_data)) +			continue; +		return dr; +	} + +	return NULL; +} + +/** + * devres_find - Find device resource + * @dev: Device to lookup resource from + * @release: Look for resources associated with this release function + * @match: Match function (optional) + * @match_data: Data for the match function + * + * Find the latest devres of @dev which is associated with @release + * and for which @match returns 1.  If @match is NULL, it's considered + * to match all. + * + * RETURNS: + * Pointer to found devres, NULL if not found. + */ +void * devres_find(struct device *dev, dr_release_t release, +		   dr_match_t match, void *match_data) +{ +	struct devres *dr; +	unsigned long flags; + +	spin_lock_irqsave(&dev->devres_lock, flags); +	dr = find_dr(dev, release, match, match_data); +	spin_unlock_irqrestore(&dev->devres_lock, flags); + +	if (dr) +		return dr->data; +	return NULL; +} +EXPORT_SYMBOL_GPL(devres_find); + +/** + * devres_get - Find devres, if non-existent, add one atomically + * @dev: Device to lookup or add devres for + * @new_res: Pointer to new initialized devres to add if not found + * @match: Match function (optional) + * @match_data: Data for the match function + * + * Find the latest devres of @dev which has the same release function + * as @new_res and for which @match return 1.  If found, @new_res is + * freed; otherwise, @new_res is added atomically. + * + * RETURNS: + * Pointer to found or added devres. + */ +void * devres_get(struct device *dev, void *new_res, +		  dr_match_t match, void *match_data) +{ +	struct devres *new_dr = container_of(new_res, struct devres, data); +	struct devres *dr; +	unsigned long flags; + +	spin_lock_irqsave(&dev->devres_lock, flags); +	dr = find_dr(dev, new_dr->node.release, match, match_data); +	if (!dr) { +		add_dr(dev, &new_dr->node); +		dr = new_dr; +		new_dr = NULL; +	} +	spin_unlock_irqrestore(&dev->devres_lock, flags); +	devres_free(new_dr); + +	return dr->data; +} +EXPORT_SYMBOL_GPL(devres_get); + +/** + * devres_remove - Find a device resource and remove it + * @dev: Device to find resource from + * @release: Look for resources associated with this release function + * @match: Match function (optional) + * @match_data: Data for the match function + * + * Find the latest devres of @dev associated with @release and for + * which @match returns 1.  If @match is NULL, it's considered to + * match all.  If found, the resource is removed atomically and + * returned. + * + * RETURNS: + * Pointer to removed devres on success, NULL if not found. + */ +void * devres_remove(struct device *dev, dr_release_t release, +		     dr_match_t match, void *match_data) +{ +	struct devres *dr; +	unsigned long flags; + +	spin_lock_irqsave(&dev->devres_lock, flags); +	dr = find_dr(dev, release, match, match_data); +	if (dr) { +		list_del_init(&dr->node.entry); +		devres_log(dev, &dr->node, "REM"); +	} +	spin_unlock_irqrestore(&dev->devres_lock, flags); + +	if (dr) +		return dr->data; +	return NULL; +} +EXPORT_SYMBOL_GPL(devres_remove); + +/** + * devres_destroy - Find a device resource and destroy it + * @dev: Device to find resource from + * @release: Look for resources associated with this release function + * @match: Match function (optional) + * @match_data: Data for the match function + * + * Find the latest devres of @dev associated with @release and for + * which @match returns 1.  If @match is NULL, it's considered to + * match all.  If found, the resource is removed atomically and freed. + * + * RETURNS: + * 0 if devres is found and freed, -ENOENT if not found. + */ +int devres_destroy(struct device *dev, dr_release_t release, +		   dr_match_t match, void *match_data) +{ +	void *res; + +	res = devres_remove(dev, release, match, match_data); +	if (unlikely(!res)) +		return -ENOENT; + +	devres_free(res); +	return 0; +} +EXPORT_SYMBOL_GPL(devres_destroy); + +static int remove_nodes(struct device *dev, +			struct list_head *first, struct list_head *end, +			struct list_head *todo) +{ +	int cnt = 0, nr_groups = 0; +	struct list_head *cur; + +	/* First pass - move normal devres entries to @todo and clear +	 * devres_group colors. +	 */ +	cur = first; +	while (cur != end) { +		struct devres_node *node; +		struct devres_group *grp; + +		node = list_entry(cur, struct devres_node, entry); +		cur = cur->next; + +		grp = node_to_group(node); +		if (grp) { +			/* clear color of group markers in the first pass */ +			grp->color = 0; +			nr_groups++; +		} else { +			/* regular devres entry */ +			if (&node->entry == first) +				first = first->next; +			list_move_tail(&node->entry, todo); +			cnt++; +		} +	} + +	if (!nr_groups) +		return cnt; + +	/* Second pass - Scan groups and color them.  A group gets +	 * color value of two iff the group is wholly contained in +	 * [cur, end).  That is, for a closed group, both opening and +	 * closing markers should be in the range, while just the +	 * opening marker is enough for an open group. +	 */ +	cur = first; +	while (cur != end) { +		struct devres_node *node; +		struct devres_group *grp; + +		node = list_entry(cur, struct devres_node, entry); +		cur = cur->next; + +		grp = node_to_group(node); +		BUG_ON(!grp || list_empty(&grp->node[0].entry)); + +		grp->color++; +		if (list_empty(&grp->node[1].entry)) +			grp->color++; + +		BUG_ON(grp->color <= 0 || grp->color > 2); +		if (grp->color == 2) { +			/* No need to update cur or end.  The removed +			 * nodes are always before both. +			 */ +			list_move_tail(&grp->node[0].entry, todo); +			list_del_init(&grp->node[1].entry); +		} +	} + +	return cnt; +} + +static int release_nodes(struct device *dev, struct list_head *first, +			 struct list_head *end, unsigned long flags) +{ +	LIST_HEAD(todo); +	int cnt; +	struct devres *dr, *tmp; + +	cnt = remove_nodes(dev, first, end, &todo); + +	spin_unlock_irqrestore(&dev->devres_lock, flags); + +	/* Release.  Note that both devres and devres_group are +	 * handled as devres in the following loop.  This is safe. +	 */ +	list_for_each_entry_safe_reverse(dr, tmp, &todo, node.entry) { +		devres_log(dev, &dr->node, "REL"); +		dr->node.release(dev, dr->data); +		kfree(dr); +	} + +	return cnt; +} + +/** + * devres_release_all - Release all resources + * @dev: Device to release resources for + * + * Release all resources associated with @dev.  This function is + * called on driver detach. + */ +int devres_release_all(struct device *dev) +{ +	unsigned long flags; + +	spin_lock_irqsave(&dev->devres_lock, flags); +	return release_nodes(dev, dev->devres_head.next, &dev->devres_head, +			     flags); +} + +/** + * devres_open_group - Open a new devres group + * @dev: Device to open devres group for + * @id: Separator ID + * @gfp: Allocation flags + * + * Open a new devres group for @dev with @id.  For @id, using a + * pointer to an object which won't be used for another group is + * recommended.  If @id is NULL, address-wise unique ID is created. + * + * RETURNS: + * ID of the new group, NULL on failure. + */ +void * devres_open_group(struct device *dev, void *id, gfp_t gfp) +{ +	struct devres_group *grp; +	unsigned long flags; + +	grp = kmalloc(sizeof(*grp), gfp); +	if (unlikely(!grp)) +		return NULL; + +	grp->node[0].release = &group_open_release; +	grp->node[1].release = &group_close_release; +	INIT_LIST_HEAD(&grp->node[0].entry); +	INIT_LIST_HEAD(&grp->node[1].entry); +	set_node_dbginfo(&grp->node[0], "grp<", 0); +	set_node_dbginfo(&grp->node[1], "grp>", 0); +	grp->id = grp; +	if (id) +		grp->id = id; + +	spin_lock_irqsave(&dev->devres_lock, flags); +	add_dr(dev, &grp->node[0]); +	spin_unlock_irqrestore(&dev->devres_lock, flags); +	return grp->id; +} +EXPORT_SYMBOL_GPL(devres_open_group); + +/* Find devres group with ID @id.  If @id is NULL, look for the latest. */ +static struct devres_group * find_group(struct device *dev, void *id) +{ +	struct devres_node *node; + +	list_for_each_entry_reverse(node, &dev->devres_head, entry) { +		struct devres_group *grp; + +		if (node->release != &group_open_release) +			continue; + +		grp = container_of(node, struct devres_group, node[0]); + +		if (id) { +			if (grp->id == id) +				return grp; +		} else if (list_empty(&grp->node[1].entry)) +			return grp; +	} + +	return NULL; +} + +/** + * devres_close_group - Close a devres group + * @dev: Device to close devres group for + * @id: ID of target group, can be NULL + * + * Close the group identified by @id.  If @id is NULL, the latest open + * group is selected. + */ +void devres_close_group(struct device *dev, void *id) +{ +	struct devres_group *grp; +	unsigned long flags; + +	spin_lock_irqsave(&dev->devres_lock, flags); + +	grp = find_group(dev, id); +	if (grp) +		add_dr(dev, &grp->node[1]); +	else +		WARN_ON(1); + +	spin_unlock_irqrestore(&dev->devres_lock, flags); +} +EXPORT_SYMBOL_GPL(devres_close_group); + +/** + * devres_remove_group - Remove a devres group + * @dev: Device to remove group for + * @id: ID of target group, can be NULL + * + * Remove the group identified by @id.  If @id is NULL, the latest + * open group is selected.  Note that removing a group doesn't affect + * any other resources. + */ +void devres_remove_group(struct device *dev, void *id) +{ +	struct devres_group *grp; +	unsigned long flags; + +	spin_lock_irqsave(&dev->devres_lock, flags); + +	grp = find_group(dev, id); +	if (grp) { +		list_del_init(&grp->node[0].entry); +		list_del_init(&grp->node[1].entry); +		devres_log(dev, &grp->node[0], "REM"); +	} else +		WARN_ON(1); + +	spin_unlock_irqrestore(&dev->devres_lock, flags); + +	kfree(grp); +} +EXPORT_SYMBOL_GPL(devres_remove_group); + +/** + * devres_release_group - Release resources in a devres group + * @dev: Device to release group for + * @id: ID of target group, can be NULL + * + * Release all resources in the group identified by @id.  If @id is + * NULL, the latest open group is selected.  The selected group and + * groups properly nested inside the selected group are removed. + * + * RETURNS: + * The number of released non-group resources. + */ +int devres_release_group(struct device *dev, void *id) +{ +	struct devres_group *grp; +	unsigned long flags; +	int cnt = 0; + +	spin_lock_irqsave(&dev->devres_lock, flags); + +	grp = find_group(dev, id); +	if (grp) { +		struct list_head *first = &grp->node[0].entry; +		struct list_head *end = &dev->devres_head; + +		if (!list_empty(&grp->node[1].entry)) +			end = grp->node[1].entry.next; + +		cnt = release_nodes(dev, first, end, flags); +	} else { +		WARN_ON(1); +		spin_unlock_irqrestore(&dev->devres_lock, flags); +	} + +	return cnt; +} +EXPORT_SYMBOL_GPL(devres_release_group); + +/* + * Managed kzalloc/kfree + */ +static void devm_kzalloc_release(struct device *dev, void *res) +{ +	/* noop */ +} + +static int devm_kzalloc_match(struct device *dev, void *res, void *data) +{ +	return res == data; +} + +/** + * devm_kzalloc - Managed kzalloc + * @dev: Device to allocate memory for + * @size: Allocation size + * @gfp: Allocation gfp flags + * + * Managed kzalloc.  Memory allocated with this function is + * automatically freed on driver detach.  Like all other devres + * resources, guaranteed alignment is unsigned long long. + * + * RETURNS: + * Pointer to allocated memory on success, NULL on failure. + */ +void * devm_kzalloc(struct device *dev, size_t size, gfp_t gfp) +{ +	struct devres *dr; + +	/* use raw alloc_dr for kmalloc caller tracing */ +	dr = alloc_dr(devm_kzalloc_release, size, gfp); +	if (unlikely(!dr)) +		return NULL; + +	set_node_dbginfo(&dr->node, "devm_kzalloc_release", size); +	devres_add(dev, dr->data); +	return dr->data; +} +EXPORT_SYMBOL_GPL(devm_kzalloc); + +/** + * devm_kfree - Managed kfree + * @dev: Device this memory belongs to + * @p: Memory to free + * + * Free memory allocated with dev_kzalloc(). + */ +void devm_kfree(struct device *dev, void *p) +{ +	int rc; + +	rc = devres_destroy(dev, devm_kzalloc_release, devm_kzalloc_match, p); +	WARN_ON(rc); +} +EXPORT_SYMBOL_GPL(devm_kfree);  |