mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH v4 0/5] Fix easy bits of the negative dentry problem
@ 2026-10-19  0:00 NeilBrown
  2026-10-19  0:00 ` [PATCH v4 1/5] VFS: don't count references through ->d_parent NeilBrown
                   ` (5 more replies)
  0 siblings, 6 replies; 9+ messages in thread
From: NeilBrown @ 2026-10-19  0:00 UTC (permalink / raw)
  To: Alexander Viro, Christian Brauner, Jan Kara, Amir Goldstein
  Cc: linux-fsdevel, linux-kernel, Ian Kent, Jake Edge

For this v4 I decided to focus on just fsnotify, as that is where I've
received the most useful review, and that is where problem reports seem
to focus.

Other places that walk the ->d_children list (and don't already use
cond_resched()) are specific to just one file-system-type.  fsnotify can
affect (almost) any file-system.  So it is reasonable that it gets priority.

So here we just fix the refcount overflow, and the ->d_children walk for
fsnotify.

If/when this lands, I can suggest patches to various file-system maintainers.

Thanks,
NeilBrown


Original intro - revised:

 I was reading about the negative dentry problem in
 https://lwn.net/Articles/1079407/ and thought I would have a go at the
 easy bits.  Given how involved some of that was I'm glad I didn't try
 the tricky bits!

 So I didn't try to reduce the number of negative dentries or handle
 them differently to positive dentries.  I just tried to address
 specific symptoms.

 This series addresses two reported symptoms.
  1/ refcounts on parents can potentially overflow if there are billions
     of (negative) children.  This was the easiest easy bit.  The first
     patch is all that is needed.

  2/ Walking the ->d_children list can take arbitrarily long and can
     trigger soft lockups.  I haven't addresses the "arbitrarily long"
     part, but the "soft lockups" is relatively easy.
     With this in place I would feel more confident addressing
     the "arbitrarily long" part if that seemed worth while.

 The core idea is to use a common helper for all walking of
 ->d_children, and teach it to drop the lock and schedule when needed,
 inserting a cursor to keep its place.  This simple idea requires that
 nothing should be holding any other lock while walking ->d_children


 [PATCH v4 1/5] VFS: don't count references through ->d_parent
 [PATCH v4 2/5] fsnotify: update children flags only when adding mask
 [PATCH v4 3/5] fsnotify: don't hold i_lock while walking d_children
 [PATCH v4 4/5] Add d_for_each_positive_child family of iterators
 [PATCH v4 5/5] Use d_for_each_positive_child() in fsnotify.

^ permalink raw reply	[flat|nested] 9+ messages in thread

* [PATCH v4 1/5] VFS: don't count references through ->d_parent
  2026-10-19  0:00 [PATCH v4 0/5] Fix easy bits of the negative dentry problem NeilBrown
@ 2026-10-19  0:00 ` NeilBrown
  2026-10-19  0:00 ` [PATCH v4 2/5] fsnotify: update children flags only when adding mask bits NeilBrown
                   ` (4 subsequent siblings)
  5 siblings, 0 replies; 9+ messages in thread
From: NeilBrown @ 2026-10-19  0:00 UTC (permalink / raw)
  To: Alexander Viro, Christian Brauner, Jan Kara, Amir Goldstein
  Cc: linux-fsdevel, linux-kernel, Ian Kent, Jake Edge

From: NeilBrown <neil@brown.name>

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 child added, 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().  When a
cursor is deliberately removed from the d_children list the refcount
must be decremented if that leaves the list empty.

hlist_move_behind() and hlist_move_before() are added to simplify moving
a cursor within the d_children list.  This results in some duplicate code
as they share a common prefix with d_detach_cursor() (hlist_del)
which is currently factored out.  We can expect the compiler to optimise
this away and don't need to clutter the C code with it.

Signed-off-by: NeilBrown <neil@brown.name>
---
 fs/dcache.c            | 40 +++++++++++++++++++++++++++++++++-------
 fs/internal.h          | 10 ++++++++++
 fs/libfs.c             | 14 +++++++-------
 include/linux/dcache.h | 17 ++++++++++++++++-
 include/linux/list.h   | 30 ++++++++++++++++++++++++++++++
 5 files changed, 96 insertions(+), 15 deletions(-)

diff --git a/fs/dcache.c b/fs/dcache.c
index f44c7b39c3b3..3a4d0a025447 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;
 	}
@@ -1997,7 +1999,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);
 
@@ -2016,7 +2020,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;
 }
@@ -2778,7 +2782,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;
@@ -3106,12 +3112,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);
@@ -3124,6 +3139,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/fs/internal.h b/fs/internal.h
index c658c8a5ebd5..59b2b8030b6c 100644
--- a/fs/internal.h
+++ b/fs/internal.h
@@ -235,6 +235,16 @@ extern struct dentry *__d_lookup(const struct dentry *, const struct qstr *);
 extern struct dentry *__d_lookup_rcu(const struct dentry *parent,
 				const struct qstr *name, unsigned *seq);
 
+static inline void d_detach_cursor(struct dentry *cursor)
+{
+	if (hlist_unhashed(&cursor->d_sib))
+		return;
+	hlist_del_init(&cursor->d_sib);
+	if (hlist_empty(&cursor->d_parent->d_children))
+		/* That was the last child, must drop implied reference */
+		dput_dlock(cursor->d_parent);
+}
+
 /*
  * pipe.c
  */
diff --git a/fs/libfs.c b/fs/libfs.c
index 27d7dc16fcb0..60586fe65895 100644
--- a/fs/libfs.c
+++ b/fs/libfs.c
@@ -128,9 +128,7 @@ static struct dentry *scan_positives(struct dentry *cursor,
 			count = 1;
 		}
 		if (need_resched()) {
-			if (!hlist_unhashed(&cursor->d_sib))
-				__hlist_del(&cursor->d_sib);
-			hlist_add_behind(&cursor->d_sib, &d->d_sib);
+			hlist_move_behind(&cursor->d_sib, &d->d_sib);
 			p = &cursor->d_sib.next;
 			spin_unlock(&dentry->d_lock);
 			cond_resched();
@@ -166,9 +164,10 @@ loff_t dcache_dir_lseek(struct file *file, loff_t offset, int whence)
 			to = scan_positives(cursor, &dentry->d_children.first,
 					    offset - 2, NULL);
 		spin_lock(&dentry->d_lock);
-		hlist_del_init(&cursor->d_sib);
 		if (to)
-			hlist_add_behind(&cursor->d_sib, &to->d_sib);
+			hlist_move_behind(&cursor->d_sib, &to->d_sib);
+		else
+			d_detach_cursor(cursor);
 		spin_unlock(&dentry->d_lock);
 		dput(to);
 
@@ -210,9 +209,10 @@ int dcache_readdir(struct file *file, struct dir_context *ctx)
 		p = &next->d_sib.next;
 	}
 	spin_lock(&dentry->d_lock);
-	hlist_del_init(&cursor->d_sib);
 	if (next)
-		hlist_add_before(&cursor->d_sib, &next->d_sib);
+		hlist_move_before(&cursor->d_sib, &next->d_sib);
+	else
+		d_detach_cursor(cursor);
 	spin_unlock(&dentry->d_lock);
 	dput(next);
 
diff --git a/include/linux/dcache.h b/include/linux/dcache.h
index 4b1ff99608e0..3b595adff7b4 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 <= 1))
+		dentry->d_lockref.count--;
+	return dentry;
+}
 
 /**
  * dget - get a reference to a dentry
diff --git a/include/linux/list.h b/include/linux/list.h
index 77fb62f79928..a94eec20275f 100644
--- a/include/linux/list.h
+++ b/include/linux/list.h
@@ -1123,6 +1123,36 @@ static inline void hlist_add_behind(struct hlist_node *n,
 		WRITE_ONCE(n->next->pprev, &n->next);
 }
 
+/**
+ * hlist_move_behind - move an hlist node to a new location in list
+ * @n: hlist node to be moved
+ * @prev: hlist node after which @n should be added.
+ *
+ * @n may be on the list or unhashed but must not be @prev.
+ * It is removed if necessary and re-added after @prev.
+ */
+static inline void hlist_move_behind(struct hlist_node *n,
+				     struct hlist_node *prev)
+{
+	hlist_del_init(n);
+	hlist_add_behind(n, prev);
+}
+
+/**
+ * hlist_move_before - move an hlist node to a new location in list
+ * @n: hlist node to be moved
+ * @next: hlist node before which @n should be added.
+ *
+ * @n may be on the list or unhashed but must not be @next.
+ * It is removed if necessary and re-added before @next.
+ */
+static inline void hlist_move_before(struct hlist_node *n,
+				     struct hlist_node *next)
+{
+	hlist_del_init(n);
+	hlist_add_before(n, next);
+}
+
 /**
  * hlist_add_fake - create a fake hlist consisting of a single headless node
  * @n: Node to make a fake list out of

base-commit: f909e534f5e34feb2ea2701d54df63e21aa506fc
-- 
2.50.0.107.gf914562f5916.dirty


^ permalink raw reply	[flat|nested] 9+ messages in thread

* [PATCH v4 2/5] fsnotify: update children flags only when adding mask bits
  2026-10-19  0:00 [PATCH v4 0/5] Fix easy bits of the negative dentry problem NeilBrown
  2026-10-19  0:00 ` [PATCH v4 1/5] VFS: don't count references through ->d_parent NeilBrown
@ 2026-10-19  0:00 ` NeilBrown
  2026-10-19  0:00 ` [PATCH v4 3/5] fsnotify: don't hold i_lock while walking d_children in fsnotify_set_children_dentry_flags() NeilBrown
                   ` (3 subsequent siblings)
  5 siblings, 0 replies; 9+ messages in thread
From: NeilBrown @ 2026-10-19  0:00 UTC (permalink / raw)
  To: Alexander Viro, Christian Brauner, Jan Kara, Amir Goldstein
  Cc: linux-fsdevel, linux-kernel, Ian Kent, Jake Edge

From: Amir Goldstein <amir73il@gmail.com>

Since we only ever set the PARENT_WATCHED flag from
fsnotify_recalc_mask(), there is never a need to update children flags
when removing mark mask bits only when adding them.

Add a helper fsnotify_recalc_mask_and_update_children() to be called
in cases where updating children flags may be needed (i.e. add mark).

Also avoid update children walk with mark spinlock held.
This was only done by dnotify and can be easily avoided.

neilb: added update_children flag in fcntl_dirnotify() so we only
  do the update when needed.

Fixes: 172e422ffea20 ("fsnotify: clear PARENT_WATCHED flags lazily")
Signed-off-by: Amir Goldstein <amir73il@gmail.com>
Signed-off-by: NeilBrown <neil@brown.name>
---
 fs/notify/dnotify/dnotify.c        | 12 ++++++---
 fs/notify/fanotify/fanotify_user.c |  2 +-
 fs/notify/inotify/inotify_user.c   |  2 +-
 fs/notify/mark.c                   | 40 +++++++++++++++++++++---------
 include/linux/fsnotify_backend.h   |  7 +++++-
 5 files changed, 44 insertions(+), 19 deletions(-)

diff --git a/fs/notify/dnotify/dnotify.c b/fs/notify/dnotify/dnotify.c
index 9fb73bafd41d..ccd93928ea86 100644
--- a/fs/notify/dnotify/dnotify.c
+++ b/fs/notify/dnotify/dnotify.c
@@ -58,8 +58,9 @@ struct dnotify_mark {
  * the set of all those events.  After it updates what dnotify is interested in
  * it calls the fsnotify function so it can update the set of all events relevant
  * to this inode.
+ * Returns "true" if fsnotify_conn_set_children_dentry_flags() needs to be called.
  */
-static void dnotify_recalc_inode_mask(struct fsnotify_mark *fsn_mark)
+static bool dnotify_recalc_inode_mask(struct fsnotify_mark *fsn_mark)
 {
 	__u32 new_mask = 0;
 	struct dnotify_struct *dn;
@@ -72,10 +73,10 @@ static void dnotify_recalc_inode_mask(struct fsnotify_mark *fsn_mark)
 	for (dn = dn_mark->dn; dn != NULL; dn = dn->dn_next)
 		new_mask |= (dn->dn_mask & ~FS_DN_MULTISHOT);
 	if (fsn_mark->mask == new_mask)
-		return;
+		return false;
 	fsn_mark->mask = new_mask;
 
-	fsnotify_recalc_mask(fsn_mark->connector);
+	return fsnotify_recalc_mask(fsn_mark->connector);
 }
 
 /*
@@ -265,6 +266,7 @@ int fcntl_dirnotify(int fd, struct file *filp, unsigned int arg)
 	fl_owner_t id = current->files;
 	struct file *f = NULL;
 	int destroy = 0, error = 0;
+	bool update_children = false;
 	__u32 mask;
 
 	/* we use these to tell if we need to kfree */
@@ -377,9 +379,11 @@ int fcntl_dirnotify(int fd, struct file *filp, unsigned int arg)
 	else if (error == -EEXIST)
 		error = 0;
 
-	dnotify_recalc_inode_mask(fsn_mark);
+	update_children = dnotify_recalc_inode_mask(fsn_mark);
 out:
 	spin_unlock(&fsn_mark->lock);
+	if (update_children)
+		fsnotify_conn_set_children_dentry_flags(fsn_mark->connector);
 
 	if (destroy)
 		fsnotify_detach_mark(fsn_mark);
diff --git a/fs/notify/fanotify/fanotify_user.c b/fs/notify/fanotify/fanotify_user.c
index a32c6634d592..d7c03d25ffe1 100644
--- a/fs/notify/fanotify/fanotify_user.c
+++ b/fs/notify/fanotify/fanotify_user.c
@@ -1538,7 +1538,7 @@ static int fanotify_add_mark(struct fsnotify_group *group,
 
 	recalc = fanotify_mark_add_to_mask(fsn_mark, mask, fan_flags);
 	if (recalc)
-		fsnotify_recalc_mask(fsn_mark->connector);
+		fsnotify_recalc_mask_and_update_children(fsn_mark->connector);
 
 out:
 	fsnotify_group_unlock(group);
diff --git a/fs/notify/inotify/inotify_user.c b/fs/notify/inotify/inotify_user.c
index 5f19c24ec187..6018c2d717ac 100644
--- a/fs/notify/inotify/inotify_user.c
+++ b/fs/notify/inotify/inotify_user.c
@@ -562,7 +562,7 @@ static int inotify_update_existing_watch(struct fsnotify_group *group,
 	fsn_mark->flags |= inotify_arg_to_flags(arg);
 	spin_unlock(&fsn_mark->lock);
 
-	fsnotify_recalc_mask(fsn_mark->connector);
+	fsnotify_recalc_mask_and_update_children(fsn_mark->connector);
 
 	/* return the wd */
 	ret = i_mark->wd;
diff --git a/fs/notify/mark.c b/fs/notify/mark.c
index cb07f42d3422..d289843ea149 100644
--- a/fs/notify/mark.c
+++ b/fs/notify/mark.c
@@ -307,7 +307,7 @@ static bool fsnotify_conn_watches_children(
 	return fsnotify_inode_watches_children(fsnotify_conn_inode(conn));
 }
 
-static void fsnotify_conn_set_children_dentry_flags(
+void fsnotify_conn_set_children_dentry_flags(
 					struct fsnotify_mark_connector *conn)
 {
 	if (conn->type != FSNOTIFY_OBJ_TYPE_INODE)
@@ -321,25 +321,41 @@ static void fsnotify_conn_set_children_dentry_flags(
  * connector and connector->obj cannot disappear under us.  Callers achieve
  * this by holding a mark->lock or mark->group->mark_mutex for a mark on this
  * list.
+ *
+ * Returns true if the connector started watching children as a result of
+ * the mask recalculation.  Callers that may be adding watch bits should
+ * use fsnotify_recalc_mask_and_update_children() or call
+ * fsnotify_conn_set_children_dentry_flags() when safe to do so.
  */
-void fsnotify_recalc_mask(struct fsnotify_mark_connector *conn)
+bool fsnotify_recalc_mask(struct fsnotify_mark_connector *conn)
 {
 	bool update_children;
 
 	if (!conn)
-		return;
+		return false;
 
 	spin_lock(&conn->lock);
 	update_children = !fsnotify_conn_watches_children(conn);
 	fsnotify_recalc_mask_set_iref(conn);
 	update_children &= fsnotify_conn_watches_children(conn);
 	spin_unlock(&conn->lock);
-	/*
-	 * Set children's PARENT_WATCHED flags only if parent started watching.
-	 * When parent stops watching, we clear false positive PARENT_WATCHED
-	 * flags lazily in __fsnotify_parent().
-	 */
-	if (update_children)
+
+	return update_children;
+}
+
+/*
+ * Recalculate mask and update children dentry PARENT_WATCHED flags if the
+ * connector started watching children.  When parent stops watching, we
+ * clear false positive PARENT_WATCHED flags lazily in __fsnotify_parent().
+ *
+ * The caller must make sure connector and connector->obj cannot disappear
+ * under us.  This is achieved by holding mark->group->mark_mutex for a
+ * mark on this list.  mark->lock must not be held by the caller.
+ */
+void fsnotify_recalc_mask_and_update_children(
+					struct fsnotify_mark_connector *conn)
+{
+	if (fsnotify_recalc_mask(conn))
 		fsnotify_conn_set_children_dentry_flags(conn);
 }
 
@@ -367,8 +383,8 @@ void fsnotify_modify_mark_mask(struct fsnotify_mark *mark, u32 set, u32 clear)
 		recalc = true;
 	spin_unlock(&mark->lock);
 
-	if (recalc)
-		fsnotify_recalc_mask(mark->connector);
+	if (recalc && fsnotify_recalc_mask(mark->connector) && set)
+		fsnotify_conn_set_children_dentry_flags(mark->connector);
 }
 EXPORT_SYMBOL_GPL(fsnotify_modify_mark_mask);
 
@@ -1007,7 +1023,7 @@ int fsnotify_add_mark_locked(struct fsnotify_mark *mark,
 	if (ret)
 		goto err;
 
-	fsnotify_recalc_mask(mark->connector);
+	fsnotify_recalc_mask_and_update_children(mark->connector);
 
 	return ret;
 err:
diff --git a/include/linux/fsnotify_backend.h b/include/linux/fsnotify_backend.h
index a6d5c1a89cff..e637bad698a5 100644
--- a/include/linux/fsnotify_backend.h
+++ b/include/linux/fsnotify_backend.h
@@ -887,7 +887,12 @@ static inline __u32 fsnotify_calc_mask(struct fsnotify_mark *mark)
 /* Get mask of events for a list of marks */
 extern __u32 fsnotify_conn_mask(struct fsnotify_mark_connector *conn);
 /* Calculate mask of events for a list of marks */
-extern void fsnotify_recalc_mask(struct fsnotify_mark_connector *conn);
+extern bool fsnotify_recalc_mask(struct fsnotify_mark_connector *conn);
+/* Recalculate mask and update children dentry flags if needed */
+extern void fsnotify_recalc_mask_and_update_children(
+					struct fsnotify_mark_connector *conn);
+extern void fsnotify_conn_set_children_dentry_flags(
+					struct fsnotify_mark_connector *conn);
 extern void fsnotify_init_mark(struct fsnotify_mark *mark,
 			       struct fsnotify_group *group);
 /* Find mark belonging to given group in the list of marks */
-- 
2.50.0.107.gf914562f5916.dirty


^ permalink raw reply	[flat|nested] 9+ messages in thread

* [PATCH v4 3/5] fsnotify: don't hold i_lock while walking d_children in  fsnotify_set_children_dentry_flags()
  2026-10-19  0:00 [PATCH v4 0/5] Fix easy bits of the negative dentry problem NeilBrown
  2026-10-19  0:00 ` [PATCH v4 1/5] VFS: don't count references through ->d_parent NeilBrown
  2026-10-19  0:00 ` [PATCH v4 2/5] fsnotify: update children flags only when adding mask bits NeilBrown
@ 2026-10-19  0:00 ` NeilBrown
  2026-10-19  0:00 ` [PATCH v4 4/5] Add d_for_each_positive_child family of iterators NeilBrown
                   ` (2 subsequent siblings)
  5 siblings, 0 replies; 9+ messages in thread
From: NeilBrown @ 2026-10-19  0:00 UTC (permalink / raw)
  To: Alexander Viro, Christian Brauner, Jan Kara, Amir Goldstein
  Cc: linux-fsdevel, linux-kernel, Ian Kent, Jake Edge

From: NeilBrown <neil@brown.name>

Rather than hold i_lock across a d_children walk, dget() a reference to
the dentry and drop the i_lock before the walk.  This is easily done
with d_find_alias().

This requires that we dput() which can sleep.  Now that no locks are
held across calls to fsnotify_set_children_dentry_flags(), this is safe.

Signed-off-by: NeilBrown <neil@brown.name>
---
 fs/notify/fsnotify.c | 40 ++++++++++++++++++++--------------------
 1 file changed, 20 insertions(+), 20 deletions(-)

diff --git a/fs/notify/fsnotify.c b/fs/notify/fsnotify.c
index 7e2f330fd283..465e875dec57 100644
--- a/fs/notify/fsnotify.c
+++ b/fs/notify/fsnotify.c
@@ -71,32 +71,32 @@ void fsnotify_sb_free(struct super_block *sb)
  */
 void fsnotify_set_children_dentry_flags(struct inode *inode)
 {
-	struct dentry *alias;
+	struct dentry *alias, *child;
 
 	if (!S_ISDIR(inode->i_mode))
 		return;
 
-	spin_lock(&inode->i_lock);
-	/* run all of the dentries associated with this inode.  Since this is a
-	 * directory, there damn well better only be one item on this list */
-	for_each_alias(alias, inode) {
-		struct dentry *child;
-
-		/* run all of the children of the original inode and fix their
-		 * d_flags to indicate parental interest (their parent is the
-		 * original inode) */
-		spin_lock(&alias->d_lock);
-		hlist_for_each_entry(child, &alias->d_children, d_sib) {
-			if (!child->d_inode)
-				continue;
+	/* Find the dentry for inode - there can only be one */
+	alias = d_find_alias(inode);
+	if (!alias)
+		return;
 
-			spin_lock_nested(&child->d_lock, DENTRY_D_LOCK_NESTED);
-			child->d_flags |= DCACHE_FSNOTIFY_PARENT_WATCHED;
-			spin_unlock(&child->d_lock);
-		}
-		spin_unlock(&alias->d_lock);
+	/*
+	 * run all of the children of the original inode and fix their
+	 * d_flags to indicate parental interest (their parent is the
+	 * original inode)
+	 */
+	spin_lock(&alias->d_lock);
+	hlist_for_each_entry(child, &alias->d_children, d_sib) {
+		if (!child->d_inode)
+			continue;
+
+		spin_lock_nested(&child->d_lock, DENTRY_D_LOCK_NESTED);
+		child->d_flags |= DCACHE_FSNOTIFY_PARENT_WATCHED;
+		spin_unlock(&child->d_lock);
 	}
-	spin_unlock(&inode->i_lock);
+	spin_unlock(&alias->d_lock);
+	dput(alias);
 }
 
 /*
-- 
2.50.0.107.gf914562f5916.dirty


^ permalink raw reply	[flat|nested] 9+ messages in thread

* [PATCH v4 4/5] Add d_for_each_positive_child family of iterators
  2026-10-19  0:00 [PATCH v4 0/5] Fix easy bits of the negative dentry problem NeilBrown
                   ` (2 preceding siblings ...)
  2026-10-19  0:00 ` [PATCH v4 3/5] fsnotify: don't hold i_lock while walking d_children in fsnotify_set_children_dentry_flags() NeilBrown
@ 2026-10-19  0:00 ` NeilBrown
  2026-10-19  0:00 ` [PATCH v4 5/5] Use d_for_each_positive_child() in fsnotify NeilBrown
  2026-10-19  1:59 ` [PATCH v4 0/5] Fix easy bits of the negative dentry problem Ian Kent
  5 siblings, 0 replies; 9+ messages in thread
From: NeilBrown @ 2026-10-19  0:00 UTC (permalink / raw)
  To: Alexander Viro, Christian Brauner, Jan Kara, Amir Goldstein
  Cc: linux-fsdevel, linux-kernel, Ian Kent, Jake Edge

From: NeilBrown <neil@brown.name>

Provide iterators for the d_children/d_sib lists.  These iterators only
report dentries that are positive (though they could be negative by the
time they are used).  As DCACHE_DENTRY_CURSOR dentries are never
positive they are never reported.

d_for_each_positive_child() takes the parent lock and sets the iterator
to each positive child in turn.

d_for_each_positive_child_continue() can continue after an existing
dentry, or (when initialised to NULL) behave like
d_for_each_positive_child().  This requires that something prevent the
start dentry from being moved before
d_for_each_positive_child_continue() can take the parent lock -
typically ->i_rwsem.

The use of scoped_guard() in these macros makes it safe to "break" or
"goto" out of the loop - the lock will be dropped in that case.

As there are reports that large numbers of negative dentries can lead to
soft lockups when walking the d_children/d_sib list, particularly in
fsnotify_set_children_dentry_flags(), we call cond_resched() as needed
during the walk.

To achieve this we use an on-stack dentry (about 330 bytes) to place a
cursor while calling cond_resched().  On-stack dentries are not standard
practice but should be safe since

Commit 3df5153c5f12 ("make cursors NORCU")

made cursors NORCU, confirming that they can safely be released without
waiting.  Also importantly nothing ever takes a reference on a cursor
that it doesn't own.

To facilitate this, a new __d_init() is split out of __d_alloc() and
used in d_init_cursor() which can be called on an on-stack dentry as is
done in the new d_next_sibling_sched().

Link https://lwn.net/Articles/1079407/
Signed-off-by: NeilBrown <neil@brown.name>
---
 fs/dcache.c            | 107 ++++++++++++++++++++++++++++++++++-------
 include/linux/dcache.h |  67 ++++++++++++++++++++++++++
 2 files changed, 157 insertions(+), 17 deletions(-)

diff --git a/fs/dcache.c b/fs/dcache.c
index 3a4d0a025447..8bfcb52eff9e 100644
--- a/fs/dcache.c
+++ b/fs/dcache.c
@@ -1892,6 +1892,24 @@ void d_invalidate(struct dentry *dentry)
 }
 EXPORT_SYMBOL(d_invalidate);
 
+static void __d_init(struct dentry *dentry, struct super_block *sb)
+{
+	dentry->d_flags = 0;
+	lockref_init(&dentry->d_lockref);
+	seqcount_spinlock_init(&dentry->d_seq, &dentry->d_lock);
+	dentry->d_inode = NULL;
+	dentry->d_parent = dentry;
+	dentry->d_sb = sb;
+	dentry->d_op = sb->__s_d_op;
+	dentry->d_flags = sb->s_d_flags;
+	dentry->d_fsdata = NULL;
+	INIT_HLIST_BL_NODE(&dentry->d_hash);
+	INIT_LIST_HEAD(&dentry->d_lru);
+	INIT_HLIST_HEAD(&dentry->d_children);
+	dentry->waiters = NULL;
+	INIT_HLIST_NODE(&dentry->d_sib);
+}
+
 /**
  * __d_alloc - allocate a dcache entry
  * @sb: filesystem it will belong to
@@ -1901,7 +1919,6 @@ EXPORT_SYMBOL(d_invalidate);
  * available. On a success the dentry is returned. The name passed in is
  * copied and the copy passed in may be reused after this call.
  */
- 
 static struct dentry *__d_alloc(struct super_block *sb, const struct qstr *name)
 {
 	struct dentry *dentry;
@@ -1933,14 +1950,14 @@ static struct dentry *__d_alloc(struct super_block *sb, const struct qstr *name)
 		p = kmalloc_flex(*p, name, name->len + 1,
 				 GFP_KERNEL_ACCOUNT | __GFP_RECLAIMABLE);
 		if (!p) {
-			kmem_cache_free(dentry_cache, dentry); 
+			kmem_cache_free(dentry_cache, dentry);
 			return NULL;
 		}
 		atomic_set(&p->count, 1);
 		dname = p->name;
 	} else  {
 		dname = dentry->d_shortname.string;
-	}	
+	}
 
 	dentry->__d_name.len = name->len;
 	dentry->__d_name.hash = name->hash;
@@ -1950,20 +1967,7 @@ static struct dentry *__d_alloc(struct super_block *sb, const struct qstr *name)
 	/* Make sure we always see the terminating NUL character */
 	smp_store_release(&dentry->__d_name.name, dname); /* ^^^ */
 
-	dentry->d_flags = 0;
-	lockref_init(&dentry->d_lockref);
-	seqcount_spinlock_init(&dentry->d_seq, &dentry->d_lock);
-	dentry->d_inode = NULL;
-	dentry->d_parent = dentry;
-	dentry->d_sb = sb;
-	dentry->d_op = sb->__s_d_op;
-	dentry->d_flags = sb->s_d_flags;
-	dentry->d_fsdata = NULL;
-	INIT_HLIST_BL_NODE(&dentry->d_hash);
-	INIT_LIST_HEAD(&dentry->d_lru);
-	INIT_HLIST_HEAD(&dentry->d_children);
-	dentry->waiters = NULL;
-	INIT_HLIST_NODE(&dentry->d_sib);
+	__d_init(dentry, sb);
 
 	if (dentry->d_op && dentry->d_op->d_init) {
 		err = dentry->d_op->d_init(dentry);
@@ -1980,6 +1984,75 @@ static struct dentry *__d_alloc(struct super_block *sb, const struct qstr *name)
 	return dentry;
 }
 
+/**
+ * d_init_cursor - initialise an on-stack dentry cursor
+ * @dentry: the dentry to be initialised
+ * @parent: dentry of parent where cursor will be used.
+ *
+ * A DCACHE_DENTRY_CURSOR is initalised for use in marking
+ * a place in the d_children/d_sib list.  While sb-related
+ * fields are filled in, they should never be used.
+ * The filesystems d_init is not called and a final d_put()
+ * should not be called, else d_release would be called.
+ */
+static void d_init_cursor(struct dentry *dentry, struct dentry *parent)
+{
+	const struct qstr *name = &slash_name;
+	char *dname;
+
+	/*
+	 * We guarantee that the inline name is always NUL-terminated.
+	 * This way the memcpy() done by the name switching in rename
+	 * will still always have a NUL at the end, even if we might
+	 * be overwriting an internal NUL character
+	 */
+	dentry->d_shortname.string[DNAME_INLINE_LEN-1] = 0;
+	name = &slash_name;
+	dname = dentry->d_shortname.string;
+
+	dentry->__d_name.len = name->len;
+	dentry->__d_name.hash = name->hash;
+	memcpy(dname, name->name, name->len);
+	dname[name->len] = 0;
+
+	/* Make sure we always see the terminating NUL character */
+	smp_store_release(&dentry->__d_name.name, dname); /* ^^^ */
+
+	__d_init(dentry, parent->d_sb);
+	dentry->d_parent = parent;
+	dentry->d_flags |= DCACHE_DENTRY_CURSOR | DCACHE_NORCU;
+}
+
+/**
+ * d_next_sibling_sched - return next sibling, but schedule() first
+ * @child:  current child
+ *
+ * This behaves like d_next_sibling(), but drops the parent d_lock
+ * and calls cond_resched() to avoid any soft-lockup.  A cursor
+ * is inserted as place holder.
+ *
+ * Caller must hold the parents d_lock, and must hold a counted
+ * reference on the parent.
+ *
+ * Returns: the next sibling, or NULL if there is none.
+ */
+struct dentry *d_next_sibling_sched(struct dentry *child)
+__must_hold(&child->d_parent->d_lock)
+{
+	struct dentry cursor;
+
+	d_init_cursor(&cursor, child->d_parent);
+	hlist_add_behind(&cursor.d_sib, &child->d_sib);
+	spin_unlock(&cursor.d_parent->d_lock);
+	cond_resched();
+	spin_lock(&cursor.d_parent->d_lock);
+	child = d_next_sibling(&cursor);
+	d_detach_cursor(&cursor);
+	WARN_ON(d_count(&cursor) != 1);
+	return child;
+}
+EXPORT_SYMBOL(d_next_sibling_sched);
+
 /**
  * d_alloc - allocate a dcache entry
  * @parent: parent of entry to allocate
diff --git a/include/linux/dcache.h b/include/linux/dcache.h
index 3b595adff7b4..55e3b10650aa 100644
--- a/include/linux/dcache.h
+++ b/include/linux/dcache.h
@@ -656,6 +656,73 @@ static inline struct dentry *d_next_sibling(const struct dentry *dentry)
 	return hlist_entry_safe(dentry->d_sib.next, struct dentry, d_sib);
 }
 
+struct dentry *d_next_sibling_sched(struct dentry *child);
+
+static inline struct dentry *d_next_positive(struct dentry *child)
+{
+	do {
+		if (need_resched())
+			child = d_next_sibling_sched(child);
+		else
+			child = d_next_sibling(child);
+	} while (child && !d_is_positive(child));
+	return child;
+}
+
+static inline struct dentry *d_first_positive(const struct dentry *parent,
+					      struct dentry *child)
+{
+	if (!child)
+		child = d_first_child(parent);
+	else
+		child = d_next_sibling(child);
+	if (child && !d_is_positive(child))
+		child = d_next_positive(child);
+	return child;
+}
+
+/**
+ * d_for_each_positive_child - iterate over positive children in the dcache
+ * @child: iterator dentry
+ * @parent: dentry of parent
+ *
+ * Iteratively set @child to each positive child of @parent.
+ * @parent->d_lock should NOT be held.
+ * @child may no longer be positive when the caller examines it
+ * so care is still needed which could involve locking the child
+ * or using d_inode_rcu() to access the inode.
+ *
+ * DCACHE_DENTRY_CURSOR dentries will never be returned, only true children
+ * which have at some point in the past been positive.
+ */
+#define d_for_each_positive_child(child, parent)			\
+	if (({might_sleep();0;})) ; else				\
+		scoped_guard(spinlock, &parent->d_lock)			\
+			for (child = d_first_positive(parent, NULL);	\
+			     child;					\
+			     child = d_next_positive(child))
+
+/**
+ * d_for_each_positive_child_continue - iterate over remaining positive children
+ * @child: iterator dentry and starting point.
+ * @parent: dentry of parent
+ *
+ * If @child is %NULL this behaves identically to d_for_each_positive_child().
+ * Otherwise @child must be an existing child of %parent and subsequent children
+ * in the d_children list of @parent are returned.
+ *
+ * Safely using this requires that something prevents @child from being
+ * renamed to a different directory before we get the lock.  Holding
+ * i_rwsem on the @parent is sufficient.
+ *
+ */
+#define d_for_each_positive_child_continue(child, parent)		\
+	if (({might_sleep();0;})) ; else				\
+		scoped_guard(spinlock, &parent->d_lock)			\
+			for (child = d_first_positive(parent, child);	\
+			     child;					\
+			     child = d_next_positive(child))
+
 void set_default_d_op(struct super_block *, const struct dentry_operations *);
 struct dentry *d_make_persistent(struct dentry *, struct inode *);
 void d_make_discardable(struct dentry *dentry);
-- 
2.50.0.107.gf914562f5916.dirty


^ permalink raw reply	[flat|nested] 9+ messages in thread

* [PATCH v4 5/5] Use d_for_each_positive_child() in fsnotify.
  2026-10-19  0:00 [PATCH v4 0/5] Fix easy bits of the negative dentry problem NeilBrown
                   ` (3 preceding siblings ...)
  2026-10-19  0:00 ` [PATCH v4 4/5] Add d_for_each_positive_child family of iterators NeilBrown
@ 2026-10-19  0:00 ` NeilBrown
  2026-10-19  1:59 ` [PATCH v4 0/5] Fix easy bits of the negative dentry problem Ian Kent
  5 siblings, 0 replies; 9+ messages in thread
From: NeilBrown @ 2026-10-19  0:00 UTC (permalink / raw)
  To: Alexander Viro, Christian Brauner, Jan Kara, Amir Goldstein
  Cc: linux-fsdevel, linux-kernel, Ian Kent, Jake Edge

From: NeilBrown <neil@brown.name>

fsnotify has been reported as source of soft-lockups when iterating a
long ->d_children list.  Change it to use d_for_each_positive_child()
which will call cond_resched() as needed to avoid soft-lockups.

Link https://lwn.net/Articles/1079407/
Signed-off-by: NeilBrown <neil@brown.name>
---
 fs/notify/fsnotify.c | 6 +-----
 1 file changed, 1 insertion(+), 5 deletions(-)

diff --git a/fs/notify/fsnotify.c b/fs/notify/fsnotify.c
index 465e875dec57..62c17e4e4798 100644
--- a/fs/notify/fsnotify.c
+++ b/fs/notify/fsnotify.c
@@ -86,16 +86,12 @@ void fsnotify_set_children_dentry_flags(struct inode *inode)
 	 * d_flags to indicate parental interest (their parent is the
 	 * original inode)
 	 */
-	spin_lock(&alias->d_lock);
-	hlist_for_each_entry(child, &alias->d_children, d_sib) {
-		if (!child->d_inode)
-			continue;
 
+	d_for_each_positive_child(child, alias) {
 		spin_lock_nested(&child->d_lock, DENTRY_D_LOCK_NESTED);
 		child->d_flags |= DCACHE_FSNOTIFY_PARENT_WATCHED;
 		spin_unlock(&child->d_lock);
 	}
-	spin_unlock(&alias->d_lock);
 	dput(alias);
 }
 
-- 
2.50.0.107.gf914562f5916.dirty


^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH v4 0/5] Fix easy bits of the negative dentry problem
  2026-10-19  0:00 [PATCH v4 0/5] Fix easy bits of the negative dentry problem NeilBrown
                   ` (4 preceding siblings ...)
  2026-10-19  0:00 ` [PATCH v4 5/5] Use d_for_each_positive_child() in fsnotify NeilBrown
@ 2026-10-19  1:59 ` Ian Kent
  2026-10-19  2:29   ` NeilBrown
  5 siblings, 1 reply; 9+ messages in thread
From: Ian Kent @ 2026-10-19  1:59 UTC (permalink / raw)
  To: NeilBrown, Alexander Viro, Christian Brauner, Jan Kara, Amir Goldstein
  Cc: linux-fsdevel, linux-kernel, Jake Edge

On Mon, 2026-10-19 at 11:00 +1100, NeilBrown wrote:
> For this v4 I decided to focus on just fsnotify, as that is where
> I've
> received the most useful review, and that is where problem reports
> seem
> to focus.
> 
> Other places that walk the ->d_children list (and don't already use
> cond_resched()) are specific to just one file-system-type.  fsnotify
> can
> affect (almost) any file-system.  So it is reasonable that it gets
> priority.
> 
> So here we just fix the refcount overflow, and the ->d_children walk
> for
> fsnotify.
> 
> If/when this lands, I can suggest patches to various file-system
> maintainers.
> 
> Thanks,
> NeilBrown
> 
> 
> Original intro - revised:
> 
>  I was reading about the negative dentry problem in
>  https://lwn.net/Articles/1079407/ and thought I would have a go at
> the
>  easy bits.  Given how involved some of that was I'm glad I didn't
> try
>  the tricky bits!
> 
>  So I didn't try to reduce the number of negative dentries or handle
>  them differently to positive dentries.  I just tried to address
>  specific symptoms.
> 
>  This series addresses two reported symptoms.
>   1/ refcounts on parents can potentially overflow if there are
> billions
>      of (negative) children.  This was the easiest easy bit.  The
> first
>      patch is all that is needed.
> 
>   2/ Walking the ->d_children list can take arbitrarily long and can
>      trigger soft lockups.  I haven't addresses the "arbitrarily
> long"
>      part, but the "soft lockups" is relatively easy.
>      With this in place I would feel more confident addressing
>      the "arbitrarily long" part if that seemed worth while.
> 
>  The core idea is to use a common helper for all walking of
>  ->d_children, and teach it to drop the lock and schedule when
> needed,
>  inserting a cursor to keep its place.  This simple idea requires
> that
>  nothing should be holding any other lock while walking ->d_children

As is so often the case you've come up with an interesting and useful
proposal, ;)

Nothing stood to me out as problematic given the scope of the series.

> 
> 
>  [PATCH v4 1/5] VFS: don't count references through ->d_parent
>  [PATCH v4 2/5] fsnotify: update children flags only when adding mask
>  [PATCH v4 3/5] fsnotify: don't hold i_lock while walking d_children
>  [PATCH v4 4/5] Add d_for_each_positive_child family of iterators
>  [PATCH v4 5/5] Use d_for_each_positive_child() in fsnotify.

Did you have any thoughts on how to contain the number of negative
dentries, (perhaps) including directory entry counts, in an intelligent
way (meaning "as you go" rather than "brute force" when needed)?

A significant problem, particularly on systems that count their memory
in TBs rather than GBs.

Ian

^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH v4 0/5] Fix easy bits of the negative dentry problem
  2026-10-19  1:59 ` [PATCH v4 0/5] Fix easy bits of the negative dentry problem Ian Kent
@ 2026-10-19  2:29   ` NeilBrown
  2026-10-20  1:29     ` Ian Kent
  0 siblings, 1 reply; 9+ messages in thread
From: NeilBrown @ 2026-10-19  2:29 UTC (permalink / raw)
  To: Ian Kent
  Cc: Alexander Viro, Christian Brauner, Jan Kara, Amir Goldstein,
	linux-fsdevel, linux-kernel, Jake Edge

On Mon, 19 Oct 2026, Ian Kent wrote:

> 
> As is so often the case you've come up with an interesting and useful
> proposal, ;)
> 
> Nothing stood to me out as problematic given the scope of the series.

Thanks :-)


> 
> > 
> > 
> >  [PATCH v4 1/5] VFS: don't count references through ->d_parent
> >  [PATCH v4 2/5] fsnotify: update children flags only when adding mask
> >  [PATCH v4 3/5] fsnotify: don't hold i_lock while walking d_children
> >  [PATCH v4 4/5] Add d_for_each_positive_child family of iterators
> >  [PATCH v4 5/5] Use d_for_each_positive_child() in fsnotify.
> 
> Did you have any thoughts on how to contain the number of negative
> dentries, (perhaps) including directory entry counts, in an intelligent
> way (meaning "as you go" rather than "brute force" when needed)?
> 
> A significant problem, particularly on systems that count their memory
> in TBs rather than GBs.

I would need to understand *why* this is a significant problem, which as
yet I don't.
Allocating things that don't get used much isn't intrinsically a
problem.  We have a shrinker which will gradually removed them - or less
gradually when memory is short.

But maybe shrinker activity itself is a problem?  Is that it?

I think it could make sense to separate positive and negative dentries
on the d_children list.  This is quite do-able, though there is a small
cost when a dentry transitions from one to the other.

The only other thing I remember from the lwn article and the various
other articles and emails it referenced, was something about poor LRU
performance.  Lots of largely useless negative dentries push more useful
positive dentries to the end of the LRU and we lose them too soon.
I think that can be addressed by having separate LRUs for positive and
negative dentries, and giving them different weights.  I think the
cost of this would be tiny, but I'm not confident of the benefit.

Is there some other concrete symptom that I didn't notice (or have
forgotten)?

Thanks,
NeilBrown


> 
> Ian
> 


^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH v4 0/5] Fix easy bits of the negative dentry problem
  2026-10-19  2:29   ` NeilBrown
@ 2026-10-20  1:29     ` Ian Kent
  0 siblings, 0 replies; 9+ messages in thread
From: Ian Kent @ 2026-10-20  1:29 UTC (permalink / raw)
  To: NeilBrown
  Cc: Alexander Viro, Christian Brauner, Jan Kara, Amir Goldstein,
	linux-fsdevel, linux-kernel, Jake Edge

On Mon, 2026-10-19 at 13:29 +1100, NeilBrown wrote:
> On Mon, 19 Oct 2026, Ian Kent wrote:
> 
> > 
> > As is so often the case you've come up with an interesting and
> > useful
> > proposal, ;)
> > 
> > Nothing stood to me out as problematic given the scope of the
> > series.
> 
> Thanks :-)
> 
> 
> > 
> > > 
> > > 
> > >  [PATCH v4 1/5] VFS: don't count references through ->d_parent
> > >  [PATCH v4 2/5] fsnotify: update children flags only when adding
> > > mask
> > >  [PATCH v4 3/5] fsnotify: don't hold i_lock while walking
> > > d_children
> > >  [PATCH v4 4/5] Add d_for_each_positive_child family of iterators
> > >  [PATCH v4 5/5] Use d_for_each_positive_child() in fsnotify.
> > 
> > Did you have any thoughts on how to contain the number of negative
> > dentries, (perhaps) including directory entry counts, in an
> > intelligent
> > way (meaning "as you go" rather than "brute force" when needed)?
> > 
> > A significant problem, particularly on systems that count their
> > memory
> > in TBs rather than GBs.
> 
> I would need to understand *why* this is a significant problem, which
> as
> yet I don't.

I think there is something more needed to understand the problem.

The source of my concern was originally the traversal of the entire
list of child directory dentries in the notification code to set a
flag that depended on a parent dentry setting.

From the information I got (which is usually somewhat limited) it
was obvious the number of dentries was very large and there was a
large proportion of negative dentries. The obvious assumption is
that the implicated list traversal was causing the lock held too
long messages due to the number of entries. There was evidence in
the vmcore analysis the proportion negative dentries in the child
list was unusually large too, similar to the proportion of system
negative dentries.

I should be able to grab the vmcore again and get more details if
that would be useful to you.

While the unbounded accumulation of negative dentries has often
appeared to be a problem, and the case Miklos and I were looking
at implicated that, I also thought that traversing the entire list
of children to set a flag based on the parent dentry was not good
design. After mentioning that Jan replied (wrt. using the parent
dentry instead of setting a flag on the child) saying dget_parent()
was high overhead. I let that go even though I didn't understand
why dget_parent() was needed. Using dget_parent() would only ensure
the parent dentry didn't disappear from under you (it wouldn't
protect you from deletes or renames) but dentries are rcu managed
so an rcu_read_lock() should do the same.

So what am I saying, I'm saying that I think the list traversal
that was causing this particular problem can be eliminated. But
that doesn't do anything to come up with a mechanism to better
manage negative dentry accumulation which I think would be better
done as part of the dentry life cycle than as a shrink operation.
That's just my preference.

> Allocating things that don't get used much isn't intrinsically a
> problem.  We have a shrinker which will gradually removed them - or
> less
> gradually when memory is short.

But if there are Terabytes of memory it often doesn't become in
short supply.

This might be able to be better managed by updated or possibly
expanded settings based more on number of (negative) dentries
than memory size.

TBH I haven't looked at that aspect.

> 
> But maybe shrinker activity itself is a problem?  Is that it?

Mostly only in so much as it not happening.

Problems with shrinker operation don't come up as much as they
used to so I don't see that as a problem myself.

> 
> I think it could make sense to separate positive and negative
> dentries
> on the d_children list.  This is quite do-able, though there is a
> small
> cost when a dentry transitions from one to the other.

Not sure about that.

One thing a negative dentry needs to do is maintain it's
linkages in directories and such so it can be located on
lookup to effect caching benefit. So separate lists might
be cumbersome and possibly problematic.

> 
> The only other thing I remember from the lwn article and the various
> other articles and emails it referenced, was something about poor LRU
> performance.  Lots of largely useless negative dentries push more
> useful
> positive dentries to the end of the LRU and we lose them too soon.
> I think that can be addressed by having separate LRUs for positive
> and
> negative dentries, and giving them different weights.  I think the
> cost of this would be tiny, but I'm not confident of the benefit.

Or maybe adding negative dentries to the end and positives to the
front might be simpler.

A ttl might be useful too, use it to discard stale entries as they
are encountered (although the discard policy would probably be
somewhat more involved taking into account positive vs. negatives
etc.).

This in line with my preference to mange these "on-the-go" so to
speak, not necessarily related to any functional parameter
improvement that might be possible.


Ian

^ permalink raw reply	[flat|nested] 9+ messages in thread

end of thread, other threads:[~2026-10-20  1:29 UTC | newest]

Thread overview: 9+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-19  0:00 [PATCH v4 0/5] Fix easy bits of the negative dentry problem NeilBrown
2026-10-19  0:00 ` [PATCH v4 1/5] VFS: don't count references through ->d_parent NeilBrown
2026-10-19  0:00 ` [PATCH v4 2/5] fsnotify: update children flags only when adding mask bits NeilBrown
2026-10-19  0:00 ` [PATCH v4 3/5] fsnotify: don't hold i_lock while walking d_children in fsnotify_set_children_dentry_flags() NeilBrown
2026-10-19  0:00 ` [PATCH v4 4/5] Add d_for_each_positive_child family of iterators NeilBrown
2026-10-19  0:00 ` [PATCH v4 5/5] Use d_for_each_positive_child() in fsnotify NeilBrown
2026-10-19  1:59 ` [PATCH v4 0/5] Fix easy bits of the negative dentry problem Ian Kent
2026-10-19  2:29   ` NeilBrown
2026-10-20  1:29     ` Ian Kent

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®