[PATCH v1 01/12] VFS: don't count references through ->d_parent
NeilBrown <[email protected]> Mon, 3 Aug 2026 11:21:10 +1000
| Newsgroups | org.kernel.vger.linux-nfs,org.kernel.vger.autofs,org.kernel.vger.ceph-devel,org.kernel.vger.linux-fsdevel,org.kernel.vger.linux-kernel |
|---|---|
| Message-ID | <[email protected]> |
From: NeilBrown <[email protected]> The number of negative child dentries that a given dentry can have is effectively unbounded, so counting the references through ->d_parent can overflow ->d_lockref.count. We only use this count so that we can know when there are no remaining references including from children. However most children appear on the ->d_children list and the few remaining children (inactive DCACHE_DENTRY_CURSORs) only exist in a context were a separate reference is held on the parent. So if we count whether d_children is nonempty (i.e. dget() when it becomes non-empty, dput() when it comes empty) and require children not on ->d_children to have a reference on the parent some other way (which they already do), then ->d_lockref.count will not overflow and we can still know exactly when all references, including through ->d_parent, are gone. __d_move() allows that dentries might not be on any d_children list as is the case for IS_ROOT() dentries. Rather than track when this is the case we dget() either parent if they have an empty d_children before children are moved around, then dput() if they turn out to be empty afterwards. This will increment dentry if it IS_ROOT(), and then decrement it afterwards, and will decrement the refcount on old_parent if its ->d_children becomes empty. To help with this, dput_dlock() is added which warns rather than decrementing to zero (which should never happen). When a DCACHE_CURSOR dentry is added to d_children, it is only ever added "before" or "behind" an existing child, so it will never be the first and so never needs to adjust the refcount on the parent. If it remains on the list until dput() it could be the last child to be removed, in which case normal handling applies in dentry_unlist() (where it has been moved from dentry_kill(). Signed-off-by: NeilBrown <[email protected]> --- fs/dcache.c | 40 +++++++++++++++++++++++++++++++++------- include/linux/dcache.h | 17 ++++++++++++++++- 2 files changed, 49 insertions(+), 8 deletions(-) diff --git a/fs/dcache.c b/fs/dcache.c index 3e9af9de7074..ae726f3ff0cb 100644 --- a/fs/dcache.c +++ b/fs/dcache.c @@ -670,6 +670,8 @@ static inline void dentry_unlist(struct dentry *dentry) return; } __hlist_del(&dentry->d_sib); + if (hlist_empty(&dentry->d_parent->d_children)) + dentry->d_parent->d_lockref.count--; /* * Cursors can move around the list of children. While we'd been * a normal list member, it didn't matter - ->d_sib.next would've @@ -843,7 +845,7 @@ static struct dentry *dentry_kill(struct dentry *dentry) spin_unlock(&dentry->d_lock); if (likely(can_free)) dentry_free(dentry); - if (parent && --parent->d_lockref.count) { + if (parent && parent->d_lockref.count) { spin_unlock(&parent->d_lock); return NULL; } @@ -1986,7 +1988,9 @@ struct dentry *d_alloc(struct dentry * parent, const struct qstr *name) * don't need child lock because it is not subject * to concurrency here */ - dentry->d_parent = dget_dlock(parent); + dentry->d_parent = parent; + if (hlist_empty(&parent->d_children)) + dget_dlock(parent); hlist_add_head(&dentry->d_sib, &parent->d_children); spin_unlock(&parent->d_lock); @@ -2005,7 +2009,7 @@ struct dentry *d_alloc_cursor(struct dentry * parent) struct dentry *dentry = d_alloc_anon(parent->d_sb); if (dentry) { dentry->d_flags |= DCACHE_DENTRY_CURSOR | DCACHE_NORCU; - dentry->d_parent = dget(parent); + dentry->d_parent = parent; } return dentry; } @@ -2767,7 +2771,9 @@ struct dentry *d_alloc_parallel(struct dentry *parent, new->d_flags |= DCACHE_PAR_LOOKUP; spin_lock(&parent->d_lock); - new->d_parent = dget_dlock(parent); + new->d_parent = parent; + if (hlist_empty(&parent->d_children)) + dget_dlock(parent); hlist_add_head(&new->d_sib, &parent->d_children); if (parent->d_flags & DCACHE_DISCONNECTED) new->d_flags |= DCACHE_DISCONNECTED; @@ -3095,12 +3101,21 @@ static void __d_move(struct dentry *dentry, struct dentry *target, /* ... and switch them in the tree */ dentry->d_parent = target->d_parent; + + /* + * Ensure ref count on parents reflect d_children being non-empty, + * which they almost certainly are. If either end up being empty, + * this is handled below after the moves. + */ + if (hlist_empty(&old_parent->d_children)) + dget_dlock(old_parent); + if (dentry->d_parent != old_parent && + hlist_empty(&dentry->d_parent->d_children)) + dget_dlock(dentry->d_parent); + if (!exchange) { copy_name(dentry, target); target->d_hash.pprev = NULL; - dentry->d_parent->d_lockref.count++; - if (dentry != old_parent) /* wasn't IS_ROOT */ - WARN_ON(!--old_parent->d_lockref.count); } else { target->d_parent = old_parent; swap_names(dentry, target); @@ -3113,6 +3128,17 @@ static void __d_move(struct dentry *dentry, struct dentry *target, if (!hlist_unhashed(&dentry->d_sib)) __hlist_del(&dentry->d_sib); hlist_add_head(&dentry->d_sib, &dentry->d_parent->d_children); + + /* + * Adjust parent refcounts if either d_children ended up empty. + * This should only ever be old_parent. + */ + if (hlist_empty(&old_parent->d_children)) + dput_dlock(old_parent); + if (dentry->d_parent != old_parent && + hlist_empty(&dentry->d_parent->d_children)) + dput_dlock(dentry->d_parent); + __d_rehash(dentry); fsnotify_update_flags(dentry); fscrypt_handle_d_move(dentry); diff --git a/include/linux/dcache.h b/include/linux/dcache.h index 4b1ff99608e0..64d64bab16fe 100644 --- a/include/linux/dcache.h +++ b/include/linux/dcache.h @@ -324,7 +324,7 @@ extern char *dentry_path(const struct dentry *, char *, int); /* Allocation counts.. */ /** - * dget_dlock - get a reference to a dentry + * dget_dlock - get a reference to a dentry while locked * @dentry: dentry to get a reference to * * Given a live dentry, increment the reference count and return the dentry. @@ -339,6 +339,21 @@ static inline struct dentry *dget_dlock(struct dentry *dentry) return dentry; } +/** + * dput_dlock - put a reference to a dentry while locked + * @dentry: dentry to get a reference to + * + * Given a live dentry, decrement the reference count and return the dentry. + * Caller must hold @dentry->d_lock. The dentry must still have + * a reference after the decrement. This can be used when two + * references are held and one must be dropped. + */ +static inline struct dentry *dput_dlock(struct dentry *dentry) +{ + if (!WARN_ON(dentry->d_lockref.count == 0)) + dentry->d_lockref.count--; + return dentry; +} /** * dget - get a reference to a dentry -- 2.50.0.107.gf914562f5916.dirty