| // SPDX-License-Identifier: GPL-2.0-or-later | |
| /* FS-Cache cache handling | |
| * | |
| * Copyright (C) 2021 Red Hat, Inc. All Rights Reserved. | |
| * Written by David Howells (dhowells@redhat.com) | |
| */ | |
| #define FSCACHE_DEBUG_LEVEL CACHE | |
| #include <linux/export.h> | |
| #include <linux/slab.h> | |
| #include "internal.h" | |
| static LIST_HEAD(fscache_caches); | |
| DECLARE_RWSEM(fscache_addremove_sem); | |
| EXPORT_SYMBOL(fscache_addremove_sem); | |
| DECLARE_WAIT_QUEUE_HEAD(fscache_clearance_waiters); | |
| EXPORT_SYMBOL(fscache_clearance_waiters); | |
| static atomic_t fscache_cache_debug_id; | |
| /* | |
| * Allocate a cache cookie. | |
| */ | |
| static struct fscache_cache *fscache_alloc_cache(const char *name) | |
| { | |
| struct fscache_cache *cache; | |
| cache = kzalloc_obj(*cache); | |
| if (cache) { | |
| if (name) { | |
| cache->name = kstrdup(name, GFP_KERNEL); | |
| if (!cache->name) { | |
| kfree(cache); | |
| return NULL; | |
| } | |
| } | |
| refcount_set(&cache->ref, 1); | |
| INIT_LIST_HEAD(&cache->cache_link); | |
| cache->debug_id = atomic_inc_return(&fscache_cache_debug_id); | |
| } | |
| return cache; | |
| } | |
| static bool fscache_get_cache_maybe(struct fscache_cache *cache, | |
| enum fscache_cache_trace where) | |
| { | |
| bool success; | |
| int ref; | |
| success = __refcount_inc_not_zero(&cache->ref, &ref); | |
| if (success) | |
| trace_fscache_cache(cache->debug_id, ref + 1, where); | |
| return success; | |
| } | |
| /* | |
| * Look up a cache cookie. | |
| */ | |
| struct fscache_cache *fscache_lookup_cache(const char *name, bool is_cache) | |
| { | |
| struct fscache_cache *candidate, *cache, *unnamed = NULL; | |
| /* firstly check for the existence of the cache under read lock */ | |
| down_read(&fscache_addremove_sem); | |
| list_for_each_entry(cache, &fscache_caches, cache_link) { | |
| if (cache->name && name && strcmp(cache->name, name) == 0 && | |
| fscache_get_cache_maybe(cache, fscache_cache_get_acquire)) | |
| goto got_cache_r; | |
| if (!cache->name && !name && | |
| fscache_get_cache_maybe(cache, fscache_cache_get_acquire)) | |
| goto got_cache_r; | |
| } | |
| if (!name) { | |
| list_for_each_entry(cache, &fscache_caches, cache_link) { | |
| if (cache->name && | |
| fscache_get_cache_maybe(cache, fscache_cache_get_acquire)) | |
| goto got_cache_r; | |
| } | |
| } | |
| up_read(&fscache_addremove_sem); | |
| /* the cache does not exist - create a candidate */ | |
| candidate = fscache_alloc_cache(name); | |
| if (!candidate) | |
| return ERR_PTR(-ENOMEM); | |
| /* write lock, search again and add if still not present */ | |
| down_write(&fscache_addremove_sem); | |
| list_for_each_entry(cache, &fscache_caches, cache_link) { | |
| if (cache->name && name && strcmp(cache->name, name) == 0 && | |
| fscache_get_cache_maybe(cache, fscache_cache_get_acquire)) | |
| goto got_cache_w; | |
| if (!cache->name) { | |
| unnamed = cache; | |
| if (!name && | |
| fscache_get_cache_maybe(cache, fscache_cache_get_acquire)) | |
| goto got_cache_w; | |
| } | |
| } | |
| if (unnamed && is_cache && | |
| fscache_get_cache_maybe(unnamed, fscache_cache_get_acquire)) | |
| goto use_unnamed_cache; | |
| if (!name) { | |
| list_for_each_entry(cache, &fscache_caches, cache_link) { | |
| if (cache->name && | |
| fscache_get_cache_maybe(cache, fscache_cache_get_acquire)) | |
| goto got_cache_w; | |
| } | |
| } | |
| list_add_tail(&candidate->cache_link, &fscache_caches); | |
| trace_fscache_cache(candidate->debug_id, | |
| refcount_read(&candidate->ref), | |
| fscache_cache_new_acquire); | |
| up_write(&fscache_addremove_sem); | |
| return candidate; | |
| got_cache_r: | |
| up_read(&fscache_addremove_sem); | |
| return cache; | |
| use_unnamed_cache: | |
| cache = unnamed; | |
| cache->name = candidate->name; | |
| candidate->name = NULL; | |
| got_cache_w: | |
| up_write(&fscache_addremove_sem); | |
| kfree(candidate->name); | |
| kfree(candidate); | |
| return cache; | |
| } | |
| /** | |
| * fscache_acquire_cache - Acquire a cache-level cookie. | |
| * @name: The name of the cache. | |
| * | |
| * Get a cookie to represent an actual cache. If a name is given and there is | |
| * a nameless cache record available, this will acquire that and set its name, | |
| * directing all the volumes using it to this cache. | |
| * | |
| * The cache will be switched over to the preparing state if not currently in | |
| * use, otherwise -EBUSY will be returned. | |
| */ | |
| struct fscache_cache *fscache_acquire_cache(const char *name) | |
| { | |
| struct fscache_cache *cache; | |
| ASSERT(name); | |
| cache = fscache_lookup_cache(name, true); | |
| if (IS_ERR(cache)) | |
| return cache; | |
| if (!fscache_set_cache_state_maybe(cache, | |
| FSCACHE_CACHE_IS_NOT_PRESENT, | |
| FSCACHE_CACHE_IS_PREPARING)) { | |
| pr_warn("Cache tag %s in use\n", name); | |
| fscache_put_cache(cache, fscache_cache_put_cache); | |
| return ERR_PTR(-EBUSY); | |
| } | |
| return cache; | |
| } | |
| EXPORT_SYMBOL(fscache_acquire_cache); | |
| /** | |
| * fscache_put_cache - Release a cache-level cookie. | |
| * @cache: The cache cookie to be released | |
| * @where: An indication of where the release happened | |
| * | |
| * Release the caller's reference on a cache-level cookie. The @where | |
| * indication should give information about the circumstances in which the call | |
| * occurs and will be logged through a tracepoint. | |
| */ | |
| void fscache_put_cache(struct fscache_cache *cache, | |
| enum fscache_cache_trace where) | |
| { | |
| unsigned int debug_id; | |
| bool zero; | |
| int ref; | |
| if (IS_ERR_OR_NULL(cache)) | |
| return; | |
| debug_id = cache->debug_id; | |
| zero = __refcount_dec_and_test(&cache->ref, &ref); | |
| trace_fscache_cache(debug_id, ref - 1, where); | |
| if (zero) { | |
| down_write(&fscache_addremove_sem); | |
| list_del_init(&cache->cache_link); | |
| up_write(&fscache_addremove_sem); | |
| kfree(cache->name); | |
| kfree(cache); | |
| } | |
| } | |
| /** | |
| * fscache_relinquish_cache - Reset cache state and release cookie | |
| * @cache: The cache cookie to be released | |
| * | |
| * Reset the state of a cache and release the caller's reference on a cache | |
| * cookie. | |
| */ | |
| void fscache_relinquish_cache(struct fscache_cache *cache) | |
| { | |
| enum fscache_cache_trace where = | |
| (cache->state == FSCACHE_CACHE_IS_PREPARING) ? | |
| fscache_cache_put_prep_failed : | |
| fscache_cache_put_relinquish; | |
| cache->ops = NULL; | |
| cache->cache_priv = NULL; | |
| fscache_set_cache_state(cache, FSCACHE_CACHE_IS_NOT_PRESENT); | |
| fscache_put_cache(cache, where); | |
| } | |
| EXPORT_SYMBOL(fscache_relinquish_cache); | |
| /** | |
| * fscache_add_cache - Declare a cache as being open for business | |
| * @cache: The cache-level cookie representing the cache | |
| * @ops: Table of cache operations to use | |
| * @cache_priv: Private data for the cache record | |
| * | |
| * Add a cache to the system, making it available for netfs's to use. | |
| * | |
| * See Documentation/filesystems/caching/backend-api.rst for a complete | |
| * description. | |
| */ | |
| int fscache_add_cache(struct fscache_cache *cache, | |
| const struct fscache_cache_ops *ops, | |
| void *cache_priv) | |
| { | |
| int n_accesses; | |
| _enter("{%s,%s}", ops->name, cache->name); | |
| BUG_ON(fscache_cache_state(cache) != FSCACHE_CACHE_IS_PREPARING); | |
| /* Get a ref on the cache cookie and keep its n_accesses counter raised | |
| * by 1 to prevent wakeups from transitioning it to 0 until we're | |
| * withdrawing caching services from it. | |
| */ | |
| n_accesses = atomic_inc_return(&cache->n_accesses); | |
| trace_fscache_access_cache(cache->debug_id, refcount_read(&cache->ref), | |
| n_accesses, fscache_access_cache_pin); | |
| down_write(&fscache_addremove_sem); | |
| cache->ops = ops; | |
| cache->cache_priv = cache_priv; | |
| fscache_set_cache_state(cache, FSCACHE_CACHE_IS_ACTIVE); | |
| up_write(&fscache_addremove_sem); | |
| pr_notice("Cache \"%s\" added (type %s)\n", cache->name, ops->name); | |
| _leave(" = 0 [%s]", cache->name); | |
| return 0; | |
| } | |
| EXPORT_SYMBOL(fscache_add_cache); | |
| /** | |
| * fscache_begin_cache_access - Pin a cache so it can be accessed | |
| * @cache: The cache-level cookie | |
| * @why: An indication of the circumstances of the access for tracing | |
| * | |
| * Attempt to pin the cache to prevent it from going away whilst we're | |
| * accessing it and returns true if successful. This works as follows: | |
| * | |
| * (1) If the cache tests as not live (state is not FSCACHE_CACHE_IS_ACTIVE), | |
| * then we return false to indicate access was not permitted. | |
| * | |
| * (2) If the cache tests as live, then we increment the n_accesses count and | |
| * then recheck the liveness, ending the access if it ceased to be live. | |
| * | |
| * (3) When we end the access, we decrement n_accesses and wake up the any | |
| * waiters if it reaches 0. | |
| * | |
| * (4) Whilst the cache is caching, n_accesses is kept artificially | |
| * incremented to prevent wakeups from happening. | |
| * | |
| * (5) When the cache is taken offline, the state is changed to prevent new | |
| * accesses, n_accesses is decremented and we wait for n_accesses to | |
| * become 0. | |
| */ | |
| bool fscache_begin_cache_access(struct fscache_cache *cache, enum fscache_access_trace why) | |
| { | |
| int n_accesses; | |
| if (!fscache_cache_is_live(cache)) | |
| return false; | |
| n_accesses = atomic_inc_return(&cache->n_accesses); | |
| smp_mb__after_atomic(); /* Reread live flag after n_accesses */ | |
| trace_fscache_access_cache(cache->debug_id, refcount_read(&cache->ref), | |
| n_accesses, why); | |
| if (!fscache_cache_is_live(cache)) { | |
| fscache_end_cache_access(cache, fscache_access_unlive); | |
| return false; | |
| } | |
| return true; | |
| } | |
| /** | |
| * fscache_end_cache_access - Unpin a cache at the end of an access. | |
| * @cache: The cache-level cookie | |
| * @why: An indication of the circumstances of the access for tracing | |
| * | |
| * Unpin a cache after we've accessed it. The @why indicator is merely | |
| * provided for tracing purposes. | |
| */ | |
| void fscache_end_cache_access(struct fscache_cache *cache, enum fscache_access_trace why) | |
| { | |
| int n_accesses; | |
| smp_mb__before_atomic(); | |
| n_accesses = atomic_dec_return(&cache->n_accesses); | |
| trace_fscache_access_cache(cache->debug_id, refcount_read(&cache->ref), | |
| n_accesses, why); | |
| if (n_accesses == 0) | |
| wake_up_var(&cache->n_accesses); | |
| } | |
| /** | |
| * fscache_io_error - Note a cache I/O error | |
| * @cache: The record describing the cache | |
| * | |
| * Note that an I/O error occurred in a cache and that it should no longer be | |
| * used for anything. This also reports the error into the kernel log. | |
| * | |
| * See Documentation/filesystems/caching/backend-api.rst for a complete | |
| * description. | |
| */ | |
| void fscache_io_error(struct fscache_cache *cache) | |
| { | |
| if (fscache_set_cache_state_maybe(cache, | |
| FSCACHE_CACHE_IS_ACTIVE, | |
| FSCACHE_CACHE_GOT_IOERROR)) | |
| pr_err("Cache '%s' stopped due to I/O error\n", | |
| cache->name); | |
| } | |
| EXPORT_SYMBOL(fscache_io_error); | |
| /** | |
| * fscache_withdraw_cache - Withdraw a cache from the active service | |
| * @cache: The cache cookie | |
| * | |
| * Begin the process of withdrawing a cache from service. This stops new | |
| * cache-level and volume-level accesses from taking place and waits for | |
| * currently ongoing cache-level accesses to end. | |
| */ | |
| void fscache_withdraw_cache(struct fscache_cache *cache) | |
| { | |
| int n_accesses; | |
| pr_notice("Withdrawing cache \"%s\" (%u objs)\n", | |
| cache->name, atomic_read(&cache->object_count)); | |
| fscache_set_cache_state(cache, FSCACHE_CACHE_IS_WITHDRAWN); | |
| /* Allow wakeups on dec-to-0 */ | |
| n_accesses = atomic_dec_return(&cache->n_accesses); | |
| trace_fscache_access_cache(cache->debug_id, refcount_read(&cache->ref), | |
| n_accesses, fscache_access_cache_unpin); | |
| wait_var_event(&cache->n_accesses, | |
| atomic_read(&cache->n_accesses) == 0); | |
| } | |
| EXPORT_SYMBOL(fscache_withdraw_cache); | |
| #ifdef CONFIG_PROC_FS | |
| static const char fscache_cache_states[NR__FSCACHE_CACHE_STATE] __nonstring = "-PAEW"; | |
| /* | |
| * Generate a list of caches in /proc/fs/fscache/caches | |
| */ | |
| static int fscache_caches_seq_show(struct seq_file *m, void *v) | |
| { | |
| struct fscache_cache *cache; | |
| if (v == &fscache_caches) { | |
| seq_puts(m, | |
| "CACHE REF VOLS OBJS ACCES S NAME\n" | |
| "======== ===== ===== ===== ===== = ===============\n" | |
| ); | |
| return 0; | |
| } | |
| cache = list_entry(v, struct fscache_cache, cache_link); | |
| seq_printf(m, | |
| "%08x %5d %5d %5d %5d %c %s\n", | |
| cache->debug_id, | |
| refcount_read(&cache->ref), | |
| atomic_read(&cache->n_volumes), | |
| atomic_read(&cache->object_count), | |
| atomic_read(&cache->n_accesses), | |
| fscache_cache_states[cache->state], | |
| cache->name ?: "-"); | |
| return 0; | |
| } | |
| static void *fscache_caches_seq_start(struct seq_file *m, loff_t *_pos) | |
| __acquires(fscache_addremove_sem) | |
| { | |
| down_read(&fscache_addremove_sem); | |
| return seq_list_start_head(&fscache_caches, *_pos); | |
| } | |
| static void *fscache_caches_seq_next(struct seq_file *m, void *v, loff_t *_pos) | |
| { | |
| return seq_list_next(v, &fscache_caches, _pos); | |
| } | |
| static void fscache_caches_seq_stop(struct seq_file *m, void *v) | |
| __releases(fscache_addremove_sem) | |
| { | |
| up_read(&fscache_addremove_sem); | |
| } | |
| const struct seq_operations fscache_caches_seq_ops = { | |
| .start = fscache_caches_seq_start, | |
| .next = fscache_caches_seq_next, | |
| .stop = fscache_caches_seq_stop, | |
| .show = fscache_caches_seq_show, | |
| }; | |
| #endif /* CONFIG_PROC_FS */ |