diff --git a/fs/dcache.c b/fs/dcache.c index cf865c12cdf9..cc34bf70511a 100644 --- a/fs/dcache.c +++ b/fs/dcache.c @@ -605,6 +605,19 @@ void d_drop(struct dentry *dentry) } EXPORT_SYMBOL(d_drop); +struct select_data { + struct dentry *start; + union { + long found; + struct { + struct dentry *victim; + struct select_data *next; + struct completion completion; + }; + }; + struct list_head dispose; +}; + static inline void dentry_unlist(struct dentry *dentry) { struct dentry *next; @@ -613,6 +626,16 @@ static inline void dentry_unlist(struct dentry *dentry) * attached to the dentry tree */ dentry->d_flags |= DCACHE_DENTRY_KILLED; + if (unlikely(dentry->d_u.waiters)) { + /* some shrink_dcache_tree() instances are waiting */ + struct select_data *p = dentry->d_u.waiters; + dentry->d_u.waiters = NULL; + while (p) { + struct completion *r = &p->completion; + p = p->next; + complete(r); + } + } if (unlikely(hlist_unhashed(&dentry->d_sib))) return; __hlist_del(&dentry->d_sib); @@ -1499,15 +1522,6 @@ int d_set_mounted(struct dentry *dentry) * constraints. */ -struct select_data { - struct dentry *start; - union { - long found; - struct dentry *victim; - }; - struct list_head dispose; -}; - static enum d_walk_ret select_collect(void *_data, struct dentry *dentry) { struct select_data *data = _data; @@ -1559,6 +1573,10 @@ static enum d_walk_ret select_collect2(void *_data, struct dentry *dentry) return D_WALK_QUIT; } to_shrink_list(dentry, &data->dispose); + } else if (dentry->d_lockref.count < 0) { + rcu_read_lock(); + data->victim = dentry; + return D_WALK_QUIT; } /* * We can return to the caller if we have found some (this @@ -1598,12 +1616,28 @@ static void shrink_dcache_tree(struct dentry *parent, bool for_umount) data.victim = NULL; d_walk(parent, &data, select_collect2); if (data.victim) { - spin_lock(&data.victim->d_lock); - if (!lock_for_kill(data.victim)) { - spin_unlock(&data.victim->d_lock); + struct dentry *v = data.victim; + + spin_lock(&v->d_lock); + if (v->d_lockref.count < 0 && + !(v->d_flags & DCACHE_DENTRY_KILLED)) { + // It's busy dying; have it notify us once + // it becomes invisible to d_walk(). + init_completion(&data.completion); + data.next = v->d_u.waiters; + v->d_u.waiters = &data; + spin_unlock(&v->d_lock); + rcu_read_unlock(); + if (!list_empty(&data.dispose)) + shrink_dentry_list(&data.dispose); + wait_for_completion(&data.completion); + continue; + } + if (!lock_for_kill(v)) { + spin_unlock(&v->d_lock); rcu_read_unlock(); } else { - shrink_kill(data.victim); + shrink_kill(v); } } if (!list_empty(&data.dispose)) diff --git a/include/linux/dcache.h b/include/linux/dcache.h index 898c60d21c92..d6e973489a0a 100644 --- a/include/linux/dcache.h +++ b/include/linux/dcache.h @@ -88,6 +88,7 @@ union shortname_store { #define d_lock d_lockref.lock #define d_iname d_shortname.string +struct select_data; struct dentry { /* RCU lookup touched fields */ @@ -127,6 +128,11 @@ struct dentry { union { struct hlist_node d_alias; /* inode alias list */ struct hlist_bl_node d_in_lookup_hash; /* only for in-lookup ones */ + struct select_data *waiters; /* + * d_count already negative, + * inode (already) NULL, + * DENTRY_KILLED is yet to be set. + */ struct rcu_head d_rcu; } d_u; };